Skip to content

Let agents name their own tab, with a user-set name winning for good - #340

Merged
frenchie4111 merged 3 commits into
mainfrom
feat/agent-tab-rename
Oct 30, 2026
Merged

frenchie4111 merged 3 commits into
mainfrom
feat/agent-tab-rename

Conversation

@frenchie4111

@frenchie4111 frenchie4111 commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

Adds a set_tab_name ness-control MCP tool so an agent can rename the tab it's running in, and makes a name the user typed by hand permanently final.

The tool

set_tab_name takes only a name. There is no tabId or worktreePath parameter — the target is resolved from the caller's scope (x-harness-terminal-id → resolveCallerScope), so an agent can only ever rename itself. Backed by a new POST /tab-name route on the control server, following the tab-scoped shape of /shells (400 without the header, 404 when it resolves to nothing).

Provenance, in one optional field

TerminalTab gains customLabelSource?: 'agent', present only on agent-set names. That polarity is what makes the migration free: a customLabel with no marker means the user typed it, so every label already persisted on disk reads as user-set and locks agents out — which is the behavior we want, with zero migration code.

terminals/tabRenamed now carries source: 'user' | 'agent':

source label result
user non-empty sets customLabel, drops the marker — always wins, name is now final
user empty drops both fields; deliberately re-opens agent naming, since there's no other UI to unlock it
agent non-empty refused when a user-set name exists; otherwise sets both

PanesFSM.renameTab now returns whether the rename applied, so the tool result can tell the agent its rename was refused and to stop retrying. It mirrors the reducer's lock rule explicitly rather than inferring refusal from a no-op dispatch — otherwise an idempotent re-set of the same name would read as a refusal.

Agent names go through a new normalizeTabLabel (trim, clamp to 32 chars, null for empty), applied only on the agent path. Clamping the user's typed input would silently change long-standing rename behavior.

The injected system prompt gains the tool and a paragraph on keeping the name current: set it once you know what the session is about, Title Case and a few words, re-set on a genuine change of focus, don't churn it, and a user-set name is final.

The setting

agentTabNamingEnabled, default on, under Agents → General. It follows browserToolsEnabled rather than the experimental opt-ins: only an explicit false is persisted, so absence means enabled and there's no migration.

Disabling hides the feature rather than rejecting it, matching the conversation-fork precedent — telling an agent about a tool whose only outcome is a rejection wastes a turn. Three layers:

  • the bridge filters set_tab_name out of tools/list
  • buildClaudeLaunchSettings strips the prompt guidance, now extracted as HARNESS_SYSTEM_PROMPT_TAB_NAMING_SECTION
  • POST /tab-name still 403s, as a backstop for a bridge started before the toggle flipped

It's deliberately not in Experimental: it's agent-agnostic, default-on, and gates a cosmetic reversible action. The card says explicitly that it governs agents only and that your own rename always works — "turn this off to protect my tab name" is the plausible misreading, and the per-tab lock already covers that case.

One test pins that stripping the section leaves the surrounding bullet list intact. The section is the tail of a list, so it's interpolated after a single \n and removed with one; getting that wrong leaves a dangling blank line in the prompt.

Two adjacent fixes

Both were pre-existing, but this feature turned them from "inconsistent" into "actively wrong":

  • tabTypeChanged dropped customLabel on agent↔chat conversion, because both branches rebuild the tab from scratch. With provenance in play that didn't just lose the name — it also handed naming rights back to the agent, so a user-set name would appear to have been overridden. Now carried across with its marker.
  • The mobile tab strip rendered tab.label, ignoring custom names entirely.

Not touched: MobileTerminal.tsx's sessionName={tab.label} looks like a third instance of the same bug but isn't — that value feeds the agent spawn args, not the display.

Verification

npm run typecheck, npx electron-vite build, and npx vitest run (1999 tests) all clean. New tests cover every reducer path, the FSM return value, and the route (applies / refused / clamps / 400 / 404).

The mcp-bridge.js JSON-RPC surface was also exercised directly against a stub control server, since it has no vitest coverage: tools/list advertises set_tab_name with a valid schema, and tools/call returns the right text for a plain rename, a clamped over-long name, a refusal, and a rejected empty name. Re-run with the gate off, the tool drops out of tools/list (27 → 26 tools) and the call surfaces the 403 — so both layers of the setting hold.

Two caveats worth stating plainly. I have not exercised the tool inside a real Ness session — that needs the app rebuilt and restarted, which I can't do from inside a session the restart would kill, so the unverified link is the live app wiring (panesFSM.renameTab reached through the real control server), not the tool or route logic. And I have not seen the new Settings card rendered; it typechecks and builds, and its HotkeyBadge / resolvedHotkeys.renameTab references resolve, but nobody has looked at it.

I also saw a single test failure in one full-suite run that did not reproduce across four subsequent runs and that I could not attribute to these files — the suite emits git cherry-pick conflict noise in passing runs too, so a git-touching test is the likely source. Flagging it rather than claiming the suite is deterministic.

🤖 Generated with Claude Code

frenchie4111 and others added 3 commits October 30, 2026 10:10
Adds a `set_tab_name` MCP tool so a chat or terminal agent can rename the
tab it's running in. The tool takes only a name — the target tab comes
from the caller's scope, so there's no way to rename anyone else's.

Provenance is tracked with a single optional field, `customLabelSource:
'agent'`, present only on agent-set names. The polarity is what makes
this free: a `customLabel` with no marker means the user typed it, so
every label already persisted on disk reads as user-set and locks agents
out — the correct default with no migration code.

- `source: 'user'` always wins and drops the marker, marking the name final
- `source: 'user'` with an empty label (the existing "Reset Name") clears
  both fields, which deliberately re-opens agent naming — there'd be no
  other way to unlock it
- `source: 'agent'` is refused outright once a user-set name exists, and
  `PanesFSM.renameTab` now returns whether it applied so the tool result
  can tell the agent to stop trying

Agent-supplied names go through a new `normalizeTabLabel` (trim + clamp to
32 chars, null for empty). It's applied only on the agent path — clamping
the user's typed input would silently change long-standing behavior.

Two adjacent fixes that this feature made worse rather than merely
inconsistent: `tabTypeChanged` rebuilt the tab from scratch and dropped
`customLabel` on agent↔chat conversion, which would have also handed
naming rights back to the agent; and the mobile tab strip rendered
`tab.label`, ignoring custom names entirely.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The original wording said to set the name "once you actually know what
this session is about", which is a judgment call an agent can defer
indefinitely — the likely outcome being that the tool never gets called
and every tab stays "Claude". Make it an explicit first-turn
instruction instead, from whatever context is available, with
permission to correct it later if the kickoff prompt turned out to be
vague. Split the don't-churn guidance into its own paragraph so it
reads as "then leave it alone" rather than hedging the first-turn rule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Gates the set_tab_name tool behind `agentTabNamingEnabled`, defaulting
to enabled. Follows `browserToolsEnabled` rather than the experimental
opt-ins: only an explicit `false` is persisted, so absence means on and
no migration is needed.

Disabling hides the feature rather than rejecting it, matching the
conversation-fork precedent — being told about a tool whose only
outcome is a rejection wastes an agent's turn:

- the bridge filters `set_tab_name` out of tools/list
- `buildClaudeLaunchSettings` strips the prompt section, which is now
  extracted as HARNESS_SYSTEM_PROMPT_TAB_NAMING_SECTION
- the route still 403s as a backstop for a stale bridge

The setting lives under Agents → General, not Experimental: it's
agent-agnostic, default-on, and gates a cosmetic reversible action. The
card is explicit that it governs agents only — the user's own rename is
never gated — because "turn this off to protect my tab name" is the
plausible misreading, and the per-tab lock already covers that.

A test pins that stripping the section leaves the surrounding bullet
list intact, since it's the tail of a list and a sloppy strip would
leave a dangling blank line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@frenchie4111
frenchie4111 merged commit d43b345 into main Oct 30, 2026
1 check passed
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