Skip to content

refactor(i18n): show agent chats through Fluent - #943

Merged
penso merged 12 commits into
mainfrom
i18n/chat
Nov 1, 2026
Merged

penso merged 12 commits into
mainfrom
i18n/chat

Conversation

@penso

@penso penso commented Oct 31, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #942 (base i18n/rest-chat-browser). Merge #942 first; this PR's base then becomes main. Both branches are merged with main and no longer depend on #937, which main's own fix (3f18590) made redundant.

Summary

Part of Phase 2 of docs/i18n.md (string migration), following #880, #924, #925, #926, and #928. This PR covers agent chats. With it and #942, chat/ and browser/ are covered by // i18n: migrated markers.

  • chat/ is marked // i18n: migrated as a whole. Its text comes from chat.ftl (header, composer, tool cards, pages and pictures, the plan rail, what still runs, forms) and a new chat-text.ftl (durations and turn summaries).
  • Plurals are now Fluent selectors: files copying, pictures, earlier draft lines, sub-agent steps, more lines, "and N more", HTML page lines, changed files, and what still runs (sub-agents, background shells, tasks). Several of these were lowercase literals the checker can't see, so I reviewed every file by hand.
  • Errors: every error a chat shows goes through localize().
    • chat::pasted::Error, which docs/i18n.md listed as unnameable, is now pub(crate) and mapped.
    • So are chat::page::Error, which is new: a page's own words such as "the page is too large" used to be io::Error::other text. Also mapped: chat::visual::Error (pictures, diagrams, formulas), chat::widgets::Error (interactive cards), and herdr-chat-protocol's Rejection, which the composer shows when the program refuses a command.
    • All of these are in i18n/errors/chat.rs and error-chat.ftl, with per-variant tests.
  • Durations and turn summaries: herdr_chat_protocol::display::{duration, TurnSummary::describe} write English for herdr-acp's own terminal view. The window now writes them through a new chat::text, which uses the protocol's typed values. A test checks that English output reads exactly as the protocol writes it. Token counts keep the protocol's short form (18.6k).
  • Typed state: a tool call's state was a &'static str matched for its color. It is now a ToolState enum.
  • Data: the agent's own words that the app matches (compaction section titles), shell scripts, the diagram font list, and the opt-in rich-chat preview fixture's sample conversation are marked // i18n: data.
  • Placeholders: the composer and the settings search use set_placeholder_with, so they follow a language change.
  • Leak test: chat/tests/en_xa.rs draws a working turn in every layout, with a user prompt, an answer, a tool call and its output, a plan, and a shell left running, then opens the running table. It caught the working row adding "…" in Rust, which is now part of the message.
  • Flaky test: the_row_opens_a_table_and_clears_once_everything_ended read the time after drawing the frame it checked, so a slow frame under load could fail it. It now reads the time first. It failed once in a full just ci run during this work.
  • docs/i18n.md: "What is left" no longer lists chats or chat::pasted::Error, and the leak-test list is updated.

Screenshots

Native macOS captures from a debug build of this branch, run against an isolated sandbox daemon. herdr-acp --name Claude wraps a stub ACP agent that answers with a plan, a tool call, and a reply. The prompt was sent with herdr pane send-text, and the language was switched with herdr-gpui language set; no synthetic input was used. Message text, tool titles, output, and plan steps are the agent's own, so they stay as written.

en-XA en-XB
chat en-XA chat en-XB

Under en-XB, the timeline and composer mirror, but the plan rail stays on the right. The reversed "Worked 1ms" also loses the space around its isolated duration, because pseudo-bidi reverses each run separately. Neither is changed here; RTL layout of the chat pane is left for a Phase 4 follow-up.

QA

  • just format
  • just ci on fdf37f70b (macOS arm64, after merging main): passed
  • just lint-linux and just lint-windows on the same commit: passed
  • en-XA leak test: every chat layout and the running table
  • Per-variant error tests: pasted, page, visual, widgets, and Rejection
  • chat::text: English reads as herdr_chat_protocol::display writes it
  • Native en-XA and en-XB chat with a stub ACP agent (screenshots above)
  • Native: a picture on the web ("Load from …"), an HTML fence ("Show page"), and an interactive card under en-XA
  • Native: the read-only transcript ("Reply in terminal") and the agent-input bar under a chat shown in its terminal
  • English: chats read as before

🤖 Generated with Claude Code

https://claude.ai/code/session_01BFavh8DuqfoT8UKWKk97Kq

penso and others added 5 commits October 31, 2026 10:39
#905 (iOS app, one pairing for every machine) and #926 (localized
errors) merged independently. #905 moved herdr-automate's binary,
checksum, and platform failures into host_build::Error and added
herdr_client::Error::TerminalAttach, remote control's app::Error::Enroll,
and the root Error::Nearby, none of which had a Localize mapping, so main
no longer compiled.

Each new enum gets its own mapping and .ftl file, as #926 set out:
host_build (error-host-build.ftl, which takes over the three automation
messages), terminal_attach (error-client-terminal-attach.ftl), nearby
(error-nearby.ftl), and enroll (in error-remote-control.ftl). Per-variant
tests cover each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFavh5CsF9aiVsJURUwioh
…gh Fluent

Part of Phase 2 of docs/i18n.md. browser/ is marked migrated: every
text it shows comes from browser.ftl, browser-notes.ftl,
browser-vscode.ftl, and browser-px0.ftl. Errors a page, VS Code's server,
or px0 report go through localize() instead of to_string().

Tab titles stay saved in English, as the command line lists them; the
window shows the title the app gives a tab it draws (Review,
Orchestrator, Devices, Terminal, New Tab) in its language. The prompt an
agent receives with page notes stays English, marked as data.

Inline test modules of three files move to their own tests.rs, and the
browser gains an en-XA leak test.
Under en-XB the page toolbar kept its left-to-right order, so Back
pointed away from the side it should lead from. The row now runs from
the start side, and Back and Forward point along it.
Part of Phase 2 of docs/i18n.md. chat/ is marked migrated: everything a
chat pane shows comes from chat.ftl and chat-text.ftl, with Fluent
selectors for its plurals (files, lines, steps, what still runs).

- Errors a chat shows go through localize() instead of to_string().
  chat::pasted, chat::page, chat::visual, and chat::widgets are now
  nameable, so their errors and herdr-chat-protocol's Rejection have
  mappings in error-chat.ftl. A chat page's own words (too large, no
  folder) become a page::Error instead of io::Error::other text.
- Durations and turn summaries: chat::text writes them from
  herdr-chat-protocol's typed values instead of its English display
  helpers, which herdr-acp's own view keeps using.
- A tool call's state is a ToolState enum instead of matched strings.
- The agent's own words (compaction section titles), shell scripts, and
  the opt-in rich chat preview fixture are marked as data.
- A chat's en-XA leak test draws a working turn in every layout and its
  running table. The running table test reads the time before the frame
  it checks, so a slow frame no longer fails it.
@penso

penso commented Oct 31, 2026 •

Copy link
Copy Markdown
Collaborator Author

just ci ran locally and passed on c7461e4a5 (macOS arm64): 16,308 Rust tests passed, 0 failed; check-i18n reports 3,850 messages in 159 files, all valid and used. The reworked herdr-core-client mouse-mode test also passed 50 of 50 repeated runs. just lint-linux and just lint-windows last ran on fdf37f70b (passed); later pushes touch no cfg gate.

@greptile-apps

greptile-apps Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no outstanding findings remain.

Summary

Moves app-owned chat text, durations, turn summaries, and errors into Fluent messages.

  • Chat labels and counts now follow the language shown in the app.
  • Chat durations and turn summaries now follow the shown language.
  • Errors shown by chat panes now use localized messages.
  • Chat migration checks and data markers now cover the remaining text.

Reviews (3) · Last reviewed commit: "perf(i18n): stop idle chat composers fro..." · Reviewed by Greptile

Comment thread crates/herdr-gpui/src/chat/prepared.rs Resolved
penso added 4 commits October 31, 2026 12:22
# Conflicts:
#	crates/herdr-gpui/locales/en-US/error-client-terminal-attach.ftl
#	crates/herdr-gpui/locales/en-US/error-host-build.ftl
#	crates/herdr-gpui/locales/en-US/error-nearby.ftl
#	crates/herdr-gpui/locales/en-US/error-remote-control.ftl
#	crates/herdr-gpui/src/i18n/errors/client/terminal_attach.rs
#	crates/herdr-gpui/src/i18n/errors/host_build.rs
#	crates/herdr-gpui/src/i18n/errors/nearby.rs
#	crates/herdr-gpui/src/i18n/errors/remote_control.rs
#	crates/herdr-gpui/src/i18n/errors/tests/nearby.rs
… own line

rustfmt had pulled the keep-sorted end marker onto the match arm's
closing brace, so just check-sorted failed on main.
Prepared chat bodies and their selectable text were cached by transcript
revision alone, so after a language switch a question's headings and a
cut answer's note stayed in the old language while tool headers and turn
summaries changed, and the drawn label no longer matched its selectable
segment. The window's chat poll now compares the shown locale like it
compares the layout and width, and on a change each pane forgets its
prepared text (keeping decoded pictures) and prepares it again.

The chat visual error test now also covers Base64, Decode, Fetch, and
MathParse.

The native sidebar fixture's 20 second deadline also covered the debug
build's startup and catalog loading, which already took about 17 seconds
on main's windows-2025 runner; the extra chat messages pushed it past
the bound on every attempt. It is a bound on a hung fixture, not a
performance budget, so it is now 40 seconds.
@penso

penso commented Oct 31, 2026

Copy link
Copy Markdown
Collaborator Author

@greptileai review

Comment thread crates/herdr-gpui/src/chat/window.rs Resolved
The window's chat poll sets each composer's localized placeholder every
frame. set_placeholder_with always dropped the input's layout and
notified, so an idle chat kept requesting redraws and laying its text
out again. It now lays out and notifies only when the formatted text
differs, as set_placeholder already did, and still keeps the closure so
a language switch formats it again.
@penso

penso commented Oct 31, 2026

Copy link
Copy Markdown
Collaborator Author

@greptileai review

Each Terminal::next reads one chunk, and the core may split or join what
the mock sends, so the test could read the chunk before "hi" first and
then assert on a batch one step behind, as on CI's windows-2025 leg. It
now reads until the update it expects arrives, within the same bounded
wait, and still checks the mode is reported once.
@penso
penso added this pull request to stack #950 November 1, 2026 01:44
@netlify

netlify Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for herdr-ui canceled.

Name Link
🔨 Latest commit 1715bc2
🔍 Latest deploy log https://app.netlify.com/projects/herdr-ui/deploys/6ae6a741ec599b929d606a8c

Base automatically changed from i18n/rest-chat-browser to main November 1, 2026 02:30
# Conflicts:
#	crates/herdr-core-client/src/tests/backend.rs
#	crates/herdr-gpui/src/browser/store.rs
#	docs/i18n.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant