Skip to content

Add terminal title candidate diagnostics and CWD-aware Storybook coverage#59

Draft
nedtwigg wants to merge 46 commits intomainfrom
codex/auto-title-and-cwd
Draft

Add terminal title candidate diagnostics and CWD-aware Storybook coverage#59
nedtwigg wants to merge 46 commits intomainfrom
codex/auto-title-and-cwd

Conversation

@nedtwigg
Copy link
Copy Markdown
Member

@nedtwigg nedtwigg commented May 8, 2026

Summary

  • Add per-channel terminal title candidate tracking with timestamps so OSC, user, and fallback sources can be inspected independently.
  • Keep OSC 9 as the header override path while preserving OSC 99/777 as diagnostics and TODO-notification data.
  • Add a Storybook story for the title-candidate popup and expand shell/CWD coverage across headers, doors, and grouping behavior.
  • Update terminal-state, protocol, lifecycle, and adapter code to carry the new semantic model through the app and VS Code path.

Testing

  • Added and updated unit tests for terminal state, protocol parsing, command input, and state store behavior.
  • Verified the new Storybook story in the browser and checked the popup renders the expected channel rows.
  • Ran the mouseterm-lib build and focused test suite successfully.

nedtwigg and others added 6 commits May 7, 2026 17:59
Short-circuit no-op semantic events in the reducer and skip listener
notification when state is unchanged so prompt/CWD/title events on busy
shells stop triggering Baseboard and TerminalPaneHeader re-render
storms. Memoize the all-pane-states array in those components so the
per-render allocation is amortized. Clean up replay parsers and pane
state on natural pty:exit in the vscode and tauri adapters.

Collapse the triplicated "derived === 'shell' && X !== '<unnamed>'"
ternary into a shared resolveDisplayPrimary helper, export
DEFAULT_COMMAND_TITLE and a new UNNAMED_PANEL_TITLE constant, and
replace the magic strings across the components and lifecycle. Hoist
the duplicated getCwd().then(fillTerminalProcessCwd) into a helper.
Add getSessionIdByPtyId so terminal-state-store no longer iterates the
registry directly. Unify parseOsc133 and parseOsc633 prompt boundary
cases via a parsePromptBoundary helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop defensive null guards on the non-optional titleCandidates field,
collapse the seed-helper indirection in createTerminalPaneState, and
swap sort-pick-first for linear max-scans across the candidate
accessors. Stabilize the popover dismissal effect on a boolean so the
window listeners stop re-registering on every rect change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 8, 2026

Deploying mouseterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: cad20e4
Status: ✅  Deploy successful!
Preview URL: https://b5be12a0.mouseterm.pages.dev
Branch Preview URL: https://codex-auto-title-and-cwd.mouseterm.pages.dev

View logs

nedtwigg and others added 23 commits May 8, 2026 14:51
Why: getCwd() resolves async after spawn; if the session is disposed
before then, updateCwdIfAllowed would resurrect a phantom pane state
that no listener cleans up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Why: the rename input now defaults to the derived header label (e.g.
<idle> or <unnamed>), so pressing Enter without editing would pin the
sentinel as a permanent user title that overrides everything.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Why: the previous regex matched any line ending in $/#/%/>, which
fires on TUI output (lazygit alt-screen render, progress lines like
"step 1: 95% complete") and flickers the pane between running and
idle. Now we strip alt-screen spans, require the prompt to start on
a fresh line, and demand a path/user context signal before treating
a trailing $/#/%/> + space as a returned prompt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Why: idleLabel never reads the {shellName} options it accepts. The
_options parameter is dead weight that obscures the function's
contract; either implement it or remove it. Removing for now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Why: pty:replay used to forward raw bytes to xterm; it now runs the
protocol parser to extract CWD/prompt/title state, which means OSCs
recognized by the parser are silently stripped. Future readers
adding xterm-side OSC handling need to know this happens.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
collectTerminalSemanticEvents already sets updatedAt in stream order, so
the inline Date.now() is dead-write. Use 0 and document the contract.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The parser intentionally drops everything after the first unescaped `;`,
matching VS Code shell integration's encoding (semicolons inside the
command must be escaped as \x3b). Emitters that don't follow that
encoding will see truncated commands; this is by design.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The heuristic is bounded to user_input fallback commands, so false
negatives just mean the header doesn't auto-clear; explain that this
is preferable to false positives that would flip a real running
command back to idle prematurely.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both functions now accept title?: string | null, and restoreTerminal
trims its input before checking against the UNNAMED_PANEL_TITLE
sentinel — matching resumeTerminal's behaviour and avoiding pinning
whitespace-only saved titles.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
nedtwigg and others added 17 commits May 8, 2026 17:29
Live pty:data is pre-parsed by the extension host, so the parser only
needs to run on the one-shot pty:replay buffer. Instantiate it
transiently inside the message handler instead of keeping a
per-id map that's never read after init.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The appTitleForPane resolver returns the alert manager's current OSC 9
notification body, which previously bypassed the same staleness check
that activeTerminalTitle applies to title candidates. An OSC 9 emitted
before the current command would leak through as the header label.

Now: if titleCandidates.osc9 exists and predates currentCommand.startedAt,
ignore the appTitle and fall back to the command's own display label.
When there is no osc9 candidate (notification injected without going
through the parser), trust appTitle to preserve legacy behaviour.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds two cases to vscode-adapter.test.ts:

- pty:replay runs raw data through TerminalProtocolParser, forwards
  semantic events (e.g. OSC 7 CWD) to applyTerminalSemanticEventsByPtyId,
  and emits the OSC-stripped visible data to replay handlers.
- terminal:semanticEvents passes the host-parsed events straight through
  under the same PTY id.

Both paths were uncovered.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
vscode-ext has no test runner configured, so cover the contract from
the lib side: parse a PTY chunk through the same TerminalProtocolParser
+ collectTerminalSemanticEvents pipeline message-router.ts uses, JSON
round-trip the result (proxy for structured-clone), and dispatch it
through VSCodeAdapter. Asserts the webview applies exactly what the
host emitted, end to end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
setTerminalUserTitle now returns { accepted, reason? } so the rename
flow can distinguish accepted, empty, and reserved-sentinel inputs.
Wall.onFinishRename forwards that result; TerminalPaneHeader anchors
an auto-dismissing warning popover under the input when it's rejected.

Also adds a TerminalPaneHeader Storybook story
("RenameRejectedReserved") that submits "<idle>" and shows the
warning, plus a layout.md note documenting the new behaviour.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…alert

iTerm2.md had three unrelated concerns (iTerm2 identity, notification
machinery, and a registry of every OSC parsed). vscode.md mixed VS
Code-specific behavior with adapter-agnostic transport.

- OSC.md (new): every supported OSC plus iTerm2 identity and known-
  unimplemented sequences. Single source for parsing-location and
  pty:data strip semantics.
- transport.md (new): adapter-agnostic PTY lifecycle, message protocol,
  and persisted-session types shared across VS Code, standalone, and
  fake adapters.
- alert.md: absorbs notification machinery (OSC 9/9;4/99/777/BEL,
  ActivityNotification model, text handling, security, scenarios,
  verification checklist).
- vscode.md: trimmed to VS Code-specific layer (manifest, persistence
  flow, theme, CSP, build, dream-architecture commands).
- terminal-state.md: header cross-ref points at alert.md and OSC.md.
- iTerm2.md: deleted.

Also adds SPEC-CONFLICTS.md capturing a prior audit; this commit closes
items #3 (OSC 9 timing) and #4 (pty:data strip semantics) from it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the iTerm2.md entry with OSC.md (registry/identity) and
transport.md (adapter-agnostic protocol). Expands alert.md to mention
the folded-in notification machinery, trims vscode.md to its VS
Code-specific layer with a pointer to transport.md, and drops stale
"soft/hard" TODO wording.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The docs/specs reorganization in 096a3d5 closed:
- #3 (OSC 9 title-override timing stated three ways) — iTerm2.md is
  gone; terminal-state.md is the single canonical source.
- #4 (pty:data strip semantics vs "the same streaming parser") —
  OSC.md is the single source for parsing-location rules.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves SPEC-CONFLICTS #1 (ShellActivity 5 kinds vs. DerivedHeader.status
4 values).

DerivedHeader.status and DerivedHeader.exitCode were dead in production:
only consumers were a Storybook debug badge and the headerStatus()
helper that produced them. The real header (TerminalPaneHeader.tsx,
Baseboard.tsx) reads only .primary and .secondary. Drop both fields.

While reshaping the header rules, also collapse "freshly finished"
panes onto <idle>:

- A finished command no longer keeps lastCommand.displayCommand in the
  header until the next prompt. The header returns to <idle>
  immediately. Exit code, just-finished context, and TODO/notification
  detail still flow through the alert/TODO machinery, which is the
  surface designed for that.
- This simplifies headerPrimary, isAppTitleFresh, activeTerminalTitle,
  and cwdForHeader: they only branch on currentCommand, not on
  finished + lastCommand.
- Disambiguator rules collapse from "running and finished use
  cwdAtStart, idle uses pane.cwd" to "running uses cwdAtStart,
  everything else uses pane.cwd."

Status grouping keeps its 4 buckets (unknown | idle | running |
finished) via an inline statusBucket() helper; the prompt/editing → idle
projection is now documented explicitly in terminal-state.md.

Storybook debug badge updated to read pane.activity.kind /
pane.activity.exitCode directly. All 402 lib tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves SPEC-CONFLICTS #2.

layout.md was restating the full title priority chain, the
disambiguator formula, and the OSC-channel list — all of which already
live in terminal-state.md. Replace those paragraphs with a one-way
delegation pointer. Layout keeps the rendering concerns it owns
(truncation, click/right-click, secondary label visual treatment) and
defers semantics to terminal-state.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves SPEC-CONFLICTS #5.

Dead in production (no producer outside the type definition):
- CommandRunSource: 'foreground_process', 'title'
- TerminalTitleSource: 'notification', 'profile', 'derived'

Removed from both the TypeScript types and the spec, plus the
exhaustive titleSourceLabel switch and the HEADER_APP_TITLE_SOURCES
constant. One Storybook story was using 'derived' as a placeholder
title source — switched to 'osc0'.

Live but undocumented in the spec:
- CwdSource 'manual': cwdFromManualPath(), used by session restore.
- TerminalTitleSource 'user': setTerminalUserTitle(), inline rename UI.

Both now have explicit production rules in terminal-state.md so
readers can reason about where each value comes from.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Auto-resolved by the prior commits: cwdForHeader in terminal-state.ts
collapsed to "running uses cwdAtStart, everything else uses pane.cwd"
(da25a4c), and layout.md now delegates the disambiguator rule to
terminal-state.md (2aaa16c). No remaining duplication or
mismatch — every kind, including unknown, is covered by the
"everything else" branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Auto-resolved: dead enum values (notification/profile/derived) were
removed in fe09ab4, and layout.md was rewritten in 2aaa16c to delegate
the channel list to terminal-state.md instead of enumerating it. The
type now has exactly the six sources the popup shows, and layout.md
no longer hardcodes a list — it shows whatever the canonical type
defines.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves SPEC-CONFLICTS #8.

Both layout.md and transport.md now describe the resume seeding flow
in terms of setTerminalUserTitle() — the canonical helper that
rejects reserved sentinels (<idle>, <unnamed>). This is what the code
actually does (resumeTerminal/restoreTerminal in
terminal-lifecycle.ts), and it removes the prior wording divergence
where transport.md said "non-unnamed" and layout.md said nothing
about a filter at all.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves SPEC-CONFLICTS #9.

The reducer in terminal-state.ts unconditionally clears
pendingCommandLine on promptStart and promptEnd — there's no actual
"staleness" condition, the spec wording was just imprecise. Replace
"clears stale pending command-line fallback" with the concrete rule:
a fresh prompt boundary drops any pending input that was not yet
consumed by a commandStart, because a fresh prompt is the unambiguous
signal that no command is in flight.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves SPEC-CONFLICTS #10.

The prior wording mixed a numbered fallback list ("OSC > process >
manual > null") with a separate caveat ("process may fill null or
replace manual/restored, but not OSC"). Read together they were
ambiguous about manual-vs-process tiebreaks. Replace with one rule
per source, matching what updateCwdIfAllowed() in
terminal-state-store.ts actually enforces:

- OSC always wins (only a later OSC can replace it).
- process updates only when current source is null/manual/process.
- manual is the initial seed and is replaceable by any later source.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves SPEC-CONFLICTS #11 — the final item.

Add an explicit note to terminal-state.md's title-candidate table so a
reader does not have to infer that "OSC 9" in the candidate row means
only the message form, not the progress form. The progress form
(OSC 9;4) carries no text and is fully specified in alert.md.

With every audit item now closed (#1 through #11), SPEC-CONFLICTS.md
is removed; the audit lived as a working document and has served its
purpose. The resolution trail is preserved in commits 096a3d5,
b5896bb, da25a4c, 2aaa16c, fe09ab4, 8a79062, 0a437c5, c4fc723,
9cbb0a4, and 1cf31b6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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