fix(native-chat): let a structured chat tab be renamed

Renaming a native chat tab accepted the text and silently did nothing:
setTabCustomTitle only scanned terminal tabs and only bridged to unified
tabs whose contentType was 'terminal', so the agent-session tab it was
keyed to never matched. Any label that did land was then re-nulled by the
next host snapshot, which preserved color/createdAt/isPinned but not
customLabel.

Also routes both placeholder sites through one helper so a Claude chat
stops falling back to 'Codex Chat'.
This commit is contained in:
Merge Sim
2026-09-06 16:53:51 -07:00
parent c7bcfa750a
commit 8948e5435c
6 changed files with 101 additions and 6 deletions
+19
View File
@@ -6,6 +6,9 @@ importers:
.:
configDependencies: {}
packageManagerDependencies:
'@pnpm/exe':
specifier: 12.0.0
version: 12.0.0
pnpm:
specifier: 12.0.0
version: 12.0.0
@@ -56,6 +59,11 @@ packages:
cpu: [x64]
os: [win32]
'@pnpm/exe@12.0.0':
resolution: {integrity: sha512-405vw2qYPPghNxoPRt2cvkGtGNU5gnInDcIe5TztQxe73yUZLqFJN/jCJFGs1xbVaIWDXEVuQ71P8npFZeatZQ==}
engines: {node: '>=18.*'}
hasBin: true
pnpm@12.0.0:
resolution: {integrity: sha512-ni49w5EZlYaNyUuBdcIXwn6VQI+gO0oidJd48rNPdzt3zdOznt6BcbIvzVO+ajU0Lp+smUimjvWN9kiM6Jp+Zw==}
engines: {node: '>=18.*'}
@@ -87,6 +95,17 @@ snapshots:
'@pnpm/exe.win32-x64@12.0.0':
optional: true
'@pnpm/exe@12.0.0':
optionalDependencies:
'@pnpm/exe.darwin-arm64': 12.0.0
'@pnpm/exe.darwin-x64': 12.0.0
'@pnpm/exe.linux-arm64': 12.0.0
'@pnpm/exe.linux-arm64-musl': 12.0.0
'@pnpm/exe.linux-x64': 12.0.0
'@pnpm/exe.linux-x64-musl': 12.0.0
'@pnpm/exe.win32-arm64': 12.0.0
'@pnpm/exe.win32-x64': 12.0.0
pnpm@12.0.0:
optionalDependencies:
'@pnpm/exe.darwin-arm64': 12.0.0
@@ -1,4 +1,5 @@
// @ts-nocheck -- mechanically split from OrcaRuntimeService; behavior is covered by AST equivalence and characterization tests.
import { defaultAgentChatLabel } from '../../shared/agent-session-chat-label'
import { OrcaRuntimeWithResolveRecoveredStructuredTuiTranscript } from './orca-runtime-resolve-recovered-structured-tui-transcript'
import { getStructuredAgentSessionHost } from '../native-chat/agent-session-wire/structured-agent-session-registry'
import { collectSavedStructuredAgentSessionIds } from './saved-structured-agent-session-restoration'
@@ -105,7 +106,7 @@ export class OrcaRuntimeWithRestoreStructuredAgentSessionTabsOnce extends OrcaRu
const tab: RuntimeMobileSessionAgentTab = {
type: 'agent-session',
id,
title: input.agent === 'claude' ? 'Claude Chat' : 'Codex Chat',
title: defaultAgentChatLabel(input.agent),
sessionId: input.sessionId,
agent: input.agent,
isActive: input.activate
@@ -0,0 +1,65 @@
import { describe, expect, it } from 'vitest'
import type { RuntimeMobileSessionTabsResult } from '../../../../shared/runtime-types'
import type { Tab } from '../../../../shared/tab-types'
import { buildMirroredAgentTabs } from './terminal-surfaces'
const WORKTREE = 'repo-1::worktree-1'
const GROUP = 'group-1'
function snapshotWith(agent: 'claude' | 'codex', title: string): RuntimeMobileSessionTabsResult {
return {
worktree: WORKTREE,
publicationEpoch: 'epoch-1',
snapshotVersion: 1,
activeGroupId: GROUP,
activeTabId: null,
activeTabType: null,
tabs: [
{
type: 'agent-session',
id: 'host-tab-1',
title,
sessionId: `${agent}-1`,
agent,
isActive: false
}
]
} as RuntimeMobileSessionTabsResult
}
function build(
snapshot: RuntimeMobileSessionTabsResult,
currentUnifiedTabs: readonly Tab[] = []
): Tab {
const [mirrored] = buildMirroredAgentTabs(
snapshot,
new Map(),
GROUP,
0,
currentUnifiedTabs,
1_000
)
return mirrored.unifiedTab
}
describe('buildMirroredAgentTabs', () => {
it('falls back to the agent-specific placeholder when the host publishes no title', () => {
expect(build(snapshotWith('claude', '')).label).toBe('Claude Chat')
expect(build(snapshotWith('codex', ' ')).label).toBe('Codex Chat')
})
it('prefers the host title over the placeholder', () => {
expect(build(snapshotWith('claude', 'Flaky retry test')).label).toBe('Flaky retry test')
})
it('keeps a manual rename across host snapshots', () => {
const snapshot = snapshotWith('codex', 'Codex Chat')
const renamed = build(snapshot)
const existing: Tab = { ...renamed, customLabel: 'My rename' }
expect(build(snapshot, [existing]).customLabel).toBe('My rename')
})
it('leaves customLabel null when the tab was never renamed', () => {
expect(build(snapshotWith('codex', 'Codex Chat')).customLabel).toBeNull()
})
})
@@ -3,6 +3,7 @@ import type {
RuntimeMobileSessionAgentTab
} from '../../../../shared/runtime-types'
import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/terminal-tab-types'
import { defaultAgentChatLabel } from '../../../../shared/agent-session-chat-label'
import { sanitizeTerminalLayoutPaneTitlesForLabels } from '@/lib/terminal-pane-title-sanitization'
import { resolveTerminalLayoutRoot } from '../remote-terminal-layout-resolution'
import { getRemoteRuntimePtyEnvironmentId } from '../runtime-terminal-stream'
@@ -73,8 +74,10 @@ export function buildMirroredAgentTabs(
worktreeId: snapshot.worktree,
contentType: 'agent-session',
agentSessionAgent: tab.agent,
label: tab.title.trim() || 'Codex Chat',
customLabel: null,
label: tab.title.trim() || defaultAgentChatLabel(tab.agent),
// Why: a manual rename lives only on the client; re-nulling it here made
// every host snapshot silently discard the user's title.
customLabel: existing?.customLabel ?? null,
color: tab.color !== undefined ? tab.color : (existing?.color ?? null),
sortOrder: sortOffset + index,
createdAt: existing?.createdAt ?? now + sortOffset + index,
@@ -87,9 +87,12 @@ export function createTerminalTabAttentionActions(
scheduleRuntimeGraphSync()
return { tabsByWorktree: next }
})
const item = Object.values(get().unifiedTabsByWorktree)
.flat()
.find((entry) => entry.contentType === 'terminal' && entry.entityId === tabId)
const unified = Object.values(get().unifiedTabsByWorktree).flat()
// Why: a structured chat tab has no TerminalTab record, and its rename
// arrives keyed by the unified tab id rather than a terminal entityId.
const item =
unified.find((entry) => entry.contentType === 'terminal' && entry.entityId === tabId) ??
unified.find((entry) => entry.contentType === 'agent-session' && entry.id === tabId)
if (item) {
get().setTabCustomLabel(item.id, title, opts)
}
+4
View File
@@ -0,0 +1,4 @@
/** Placeholder tab label for a structured chat that has no conversation name yet. */
export function defaultAgentChatLabel(agent: 'claude' | 'codex' | null | undefined): string {
return agent === 'claude' ? 'Claude Chat' : 'Codex Chat'
}