Skip to content

feat: keep the outer terminal window title in sync with the session - #2627

Merged
ogulcancelik merged 6 commits into
herdrdev:masterfrom
omacom-io:auto-outer-window-title
Aug 11, 2026
Merged

feat: keep the outer terminal window title in sync with the session#2627
ogulcancelik merged 6 commits into
herdrdev:masterfrom
omacom-io:auto-outer-window-title

Conversation

@dhh

@dhh dhh commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Problem

Herdr emulates the terminals in its panes, so an OSC 0/OSC 2 title written inside a pane stops at Herdr and never reaches the terminal Herdr itself runs in. That outer title is what window managers and terminal tab bars read, so it keeps showing whatever the shell or ssh happened to leave behind.

It shows up most clearly over SSH. tmux has set-titles on / set-titles-string, so an SSH'd tmux session labels the window with the remote host and window name and a tiled window manager's tab or group bar follows along. Herdr has no equivalent — the only thing that ever writes an outer title is the manual client.window_title.set API.

Change

Adds ui.window_title, a format string rendered against current session state and pushed to the foreground client whenever it changes:

[ui]
window_title = "{hostname}: {workspace}"   # the default

Tokens are {hostname}, {workspace}, {tab}, {pane} (the focused pane's manual name), and {terminal_title} (the focused pane's own terminal title with spinner frames stripped). {{/}} are literal braces, an unknown token is a config diagnostic that leaves the outer title alone, and window_title = "" disables the feature.

Notes on the design:

  • It renders on the server. {hostname} names the machine the panes actually run on, so it stays correct for herdr --remote thin clients and for running herdr over SSH — same semantics as tmux's #h. This also reuses the existing ServerMessage::WindowTitle path that client.window_title.set already goes through, rather than adding a second one.
  • It costs nothing when idle. The sync is gated on a pending render, since anything that can move the title also asks for a render, and the hostname is resolved once when config is applied rather than per render. It is O(1) per loop, not pane-scaled.
  • A title is only remembered as delivered once a foreground client takes it, so the first client to attach is written to rather than skipped. (This was a real bug in an earlier revision of this branch: the server renders once before any client attaches, so recording that as sent meant the actual terminal never got a title.)
  • client.window_title.set still wins over the configured title. Clearing it now hands the title back to ui.window_title instead of only "herdr".

Verification

just check equivalent locally: cargo fmt --check, clippy, 3392 nextest tests, and the maintenance script suites all pass. Six new tests cover template parsing, rendering, the once-per-change push, the API override and hand-back, first-attach delivery, and client promotion.

Also driven end to end against a real PTY on a throwaway session, reading the OSC bytes off the wire while switching and renaming the active workspace:

OSC0 titles: [b'host: herdr', b'host: omarchy', b'host: prod deploy']

One emission per change, and nothing while idle.

Docs

docs/next configuration guide (English, ja, zh-cn), the config reference data, the generated default config comments, the changelog, and the client.window_title.clear wording in the CLI and socket API docs.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: db72d95c-5e15-4e47-960f-f408b8d0c03a

📥 Commits

Reviewing files that changed from the base of the PR and between 1fc207a and 9e0e527.

📒 Files selected for processing (4)
  • src/app/api.rs
  • src/app/terminal_titles.rs
  • src/render_signal.rs
  • src/server/headless.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/app/api.rs
  • src/render_signal.rs
  • src/server/headless.rs

📝 Walkthrough

Walkthrough

The change adds [ui].window_title template support, renders session metadata, synchronizes titles to the foreground client, supports API overrides, and updates configuration and API documentation.

Changes

Outer terminal window title

Layer / File(s) Summary
Title template configuration and validation
src/config.rs, src/config/model.rs, src/config/window_title.rs
Adds ui.window_title, supported tokens, defaults, parsing, sanitization, diagnostics, and tests.
Application title rendering and reload
src/app/mod.rs, src/app/window_title.rs, src/main.rs
Stores parsed title state, resolves session metadata, renders titles, and reapplies settings during startup and live reload.
Terminal-title change propagation
src/pane/..., src/render_signal.rs, src/app/terminal_titles.rs, src/app/api.rs
Reports completed terminal-title changes, preserves source panes, and synchronizes raw and stripped title updates.
Foreground-client delivery and overrides
src/server/headless.rs, src/server/handoff.rs, src/client/mod.rs, src/terminal_effects.rs
Delivers sanitized titles to the foreground client, handles API overrides and clearing, and preserves overrides across handoff.
Title behavior documentation
docs/next/CHANGELOG.md, docs/next/website/src/content/docs/*, docs/next/website/src/content/docs/ja/*, docs/next/website/src/content/docs/zh-cn/*, docs/next/website/src/data/config-reference.json
Documents title configuration, tokens, clearing behavior, remote behavior, and title disabling.
End-to-end title validation
tests/client_mode.rs
Tests configured titles, metadata updates, terminal-title propagation, focus changes, and hidden-pane behavior.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PTY
  participant App
  participant HeadlessServer
  participant ForegroundClient
  PTY->>App: Report terminal-title change
  App->>HeadlessServer: Provide resolved configured title
  HeadlessServer->>ForegroundClient: Deliver sanitized title
  ForegroundClient->>HeadlessServer: Set or clear API override
  HeadlessServer->>ForegroundClient: Deliver override or configured fallback
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary change: synchronizing the outer terminal window title with the active session.
Description check ✅ Passed The description clearly explains the problem, implementation, behavior, verification, and documentation updates related to the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 96.05% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@kangal-bot kangal-bot added the ai-review Trigger automated AI reviews for pull requests admitted by the PR gate label Aug 10, 2026
@dhh
dhh force-pushed the auto-outer-window-title branch from 48f3df4 to 7f01988 Compare August 10, 2026 18:41
@dhh

dhh commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@greptile-apps

greptile-apps Bot commented Aug 10, 2026

Copy link
Copy Markdown

Greptile Summary

The PR adds configurable synchronization of the outer terminal window title with active server-side session state.

  • Adds template parsing and rendering for hostname, workspace, tab, pane, and terminal-title tokens.
  • Propagates title changes to the foreground client while avoiding redundant emissions.
  • Preserves API title overrides across live server handoffs and restores configured behavior when an override is cleared.
  • Adds configuration documentation and automated coverage for title rendering, client attachment, promotion, and handoff serialization.

Confidence Score: 5/5

The PR appears safe to merge.

The previously reported handoff failure is fixed: the active API title override is exported in the handoff manifest and restored before the replacement server starts rendering or accepting client activity, so no blocking failure remains.

Important Files Changed

Filename Overview
src/server/headless.rs Adds foreground-client title synchronization and now transfers the API override into and out of live handoffs before rendering begins.
src/server/handoff.rs Extends the backward-compatible handoff manifest with the current API window-title override.
src/app/window_title.rs Renders configured title templates from current server-side workspace, tab, pane, terminal, and hostname state.
src/config/window_title.rs Implements bounded template parsing, diagnostics, token handling, and control-character sanitization.
src/app/terminal_titles.rs Restricts terminal-title synchronization to pending pane sources while preserving sidebar and outer-title update signals.
src/render_signal.rs Tracks terminal-title sources independently from generic and PTY render damage.
src/client/mod.rs Routes server-provided window titles through the shared terminal-effect writer.
tests/client_mode.rs Adds end-to-end PTY coverage for configured titles and focused terminal-title changes.

Sequence Diagram

sequenceDiagram
  participant Pane as Pane PTY
  participant App as Server App
  participant Server as Headless Server
  participant Client as Foreground Client
  participant Host as Outer Terminal

  Pane->>App: OSC 0/2 title change
  App->>App: Update terminal title state
  App->>Server: Pending render/title source
  Server->>Server: Render ui.window_title template
  alt API override is active
    Server->>Server: Use api_window_title
  end
  Server->>Client: ServerMessage::WindowTitle
  Client->>Host: Write OSC 0
  Note over Server: Live handoff transfers api_window_title
Loading

Reviews (6): Last reviewed commit: "fix: make outer window title updates eve..." | Re-trigger Greptile

Comment thread src/server/headless.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 74df9e3a-ec3e-4cce-a864-ad93c9e3e78f

📥 Commits

Reviewing files that changed from the base of the PR and between 7e4ab7b and 7f01988.

📒 Files selected for processing (18)
  • docs/next/CHANGELOG.md
  • docs/next/website/src/content/docs/cli-reference.mdx
  • docs/next/website/src/content/docs/configuration.mdx
  • docs/next/website/src/content/docs/ja/cli-reference.mdx
  • docs/next/website/src/content/docs/ja/configuration.mdx
  • docs/next/website/src/content/docs/ja/socket-api.mdx
  • docs/next/website/src/content/docs/socket-api.mdx
  • docs/next/website/src/content/docs/zh-cn/cli-reference.mdx
  • docs/next/website/src/content/docs/zh-cn/configuration.mdx
  • docs/next/website/src/content/docs/zh-cn/socket-api.mdx
  • docs/next/website/src/data/config-reference.json
  • src/app/mod.rs
  • src/app/window_title.rs
  • src/config.rs
  • src/config/model.rs
  • src/config/window_title.rs
  • src/main.rs
  • src/server/headless.rs

Comment thread src/server/headless.rs
@dhh

dhh commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Fixed in ce86589.

send_to_client returns true whenever the client entry exists, and only the inner if let Some(writer) guards the actual queueing, so a detached client with writer: None reported a successful send and got cached in sent_window_title. That defeated the very thing the second commit here added: on reattach the title matched the cache and was skipped, leaving the new terminal with a stale title.

send_window_title now requires a writer before sending and clears the cache otherwise, matching the existing no-foreground-client branch. Regression test: a_foreground_client_without_a_writer_does_not_cache_the_window_title, which fails on the previous code with assertion failed: server.sent_window_title.is_none().

@dhh

dhh commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Fixed in 90fb47a.

You were right that the replacement server started with api_window_title: None, so an override set through client.window_title.set was silently replaced by the configured ui.window_title on the first render after an update.

HandoffManifest now carries api_window_title, the outgoing server populates it, and the import applies it before report_ready, so the override is in place before any client reattaches. The field is #[serde(default)] and the manifest is JSON, so a manifest written by a server that predates the field still loads with None and needs no HANDOFF_VERSION bump.

Tests: a_handoff_carries_an_api_set_window_title and a_manifest_written_before_the_title_field_still_loads.

@dhh
dhh force-pushed the auto-outer-window-title branch from 90fb47a to 547487d Compare August 10, 2026 19:37
dhh added 5 commits August 11, 2026 02:11
Herdr emulates the terminals in its panes, so an OSC 0/OSC 2 title
written inside a pane stops at Herdr and never reaches the terminal
Herdr itself runs in. That outer title is what window managers read for
title bars, tab bars, and group bars, so it kept showing whatever the
shell or ssh happened to leave behind.

Add `ui.window_title`, rendered from {hostname}, {workspace}, {tab},
{pane}, and {terminal_title}, and push it to the foreground client
whenever it changes. It renders on the server, so {hostname} names the
machine the panes actually run on rather than the machine a thin remote
client runs on, and it is gated on a pending render so an idle loop
never pays for it.

A title is only remembered as delivered once a foreground client takes
it, so the first client to attach is written to rather than skipped.
`client.window_title.set` still wins over the configured title, and
clearing it now hands the title back to `ui.window_title` instead of
only "herdr".
ClientConnected assigns the foreground client directly rather than going
through promote_client_to_foreground, so clearing the sent-title cache
there missed the case that matters most: attaching a second terminal to a
running session. The title was usually unchanged, so the sync returned
early and the new terminal kept whatever its shell or ssh had left.

Key the cache on the client that received the title instead of relying on
every foreground assignment to invalidate it.
Output from a hidden or background pane sets needs_render without setting
needs_full_render, and the retained render plan then skips presentation
for it entirely. Syncing the title on needs_render meant every coalesced
hidden-output tick still formatted, sanitized, and allocated a title that
could not have changed, against the hidden-source early exit AGENTS.md
requires.

Every input to the title is app state, which always requests a full
render, so gate on that instead. The one exception is the focused pane's
own terminal title, which arrives through PTY parsing, so ask for a full
render when that changes and the configured title uses it.
@ogulcancelik
ogulcancelik force-pushed the auto-outer-window-title branch from 547487d to 1fc207a Compare August 10, 2026 23:12

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/app/terminal_titles.rs (1)

26-90: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

Consider narrowing the per-pane scan for terminal-title sync.

sync_terminal_titles scans every workspace, tab, and pane and checks sources.contains(pane_id) for each one. sync_all_terminal_titles builds sources as the full pane set, so that containment check is always true for the all-titles path. runtime.terminal_title() locks the pane's terminal-core mutex for every scanned pane.

handle_api_request_after_internal_events_drained calls sync_all_terminal_titles() before dispatching every API request, so every API call now pays this full-pane scan and per-pane terminal-core lock cost, regardless of whether the request needs terminal-title data.

Use a direct pane lookup (for example, an existing find_pane-style index) for the filtered sync_terminal_titles path, and confirm whether sync_all_terminal_titles can skip the redundant contains check when it already knows it wants every pane.

Source: Path instructions


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6d1d0c0e-c2e0-4bd3-afc8-c6023c7b6cec

📥 Commits

Reviewing files that changed from the base of the PR and between ce86589 and 1fc207a.

📒 Files selected for processing (14)
  • src/app/api.rs
  • src/app/mod.rs
  • src/app/terminal_titles.rs
  • src/client/mod.rs
  • src/config.rs
  • src/config/window_title.rs
  • src/pane.rs
  • src/pane/osc.rs
  • src/pane/terminal.rs
  • src/render_signal.rs
  • src/server/handoff.rs
  • src/server/headless.rs
  • src/terminal_effects.rs
  • tests/client_mode.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/config.rs
  • src/app/mod.rs
  • src/server/headless.rs

@ogulcancelik
ogulcancelik force-pushed the auto-outer-window-title branch from 1fc207a to 9e0e527 Compare August 10, 2026 23:33
@ogulcancelik

Copy link
Copy Markdown
Collaborator

Addressed CodeRabbit’s terminal-title sync performance note in 9e0e527. API requests now snapshot only pending title-source pane IDs, and title synchronization resolves those panes directly instead of scanning and locking every pane runtime.

@ogulcancelik
ogulcancelik merged commit 350f001 into herdrdev:master Aug 11, 2026
8 checks passed
@kangal-bot kangal-bot removed the ai-review Trigger automated AI reviews for pull requests admitted by the PR gate label Aug 11, 2026
@ogulcancelik

Copy link
Copy Markdown
Collaborator

this was on backlog for a long while. ty!

@dhh
dhh deleted the auto-outer-window-title branch August 11, 2026 07:56
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.

3 participants