diff --git a/packages/coding-agent/.changes/use-message-count.md b/packages/coding-agent/.changes/use-message-count.md new file mode 100644 index 0000000000..26bf261cc8 --- /dev/null +++ b/packages/coding-agent/.changes/use-message-count.md @@ -0,0 +1 @@ +- Fixed new-chat hints to use the session message count. diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 59de91fc4c..48eabad798 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -990,7 +990,6 @@ export class InteractiveMode { private connectionModelsRefreshInFlight: { version: number; promise: Promise } | undefined; private connectionState: AgentConnectionState | undefined; private connectionResourceSnapshot: AgentConnectionResourceSnapshot | undefined; - private sessionHasMessages = false; private heartbeatCatalog: AgentConnectionHeartbeat[] = []; private heartbeats: AgentConnectionHeartbeat[] = []; private heartbeatRefreshPromise: Promise | undefined; @@ -1609,7 +1608,7 @@ export class InteractiveMode { // The agents view owns these for daemon sessions. When there is no agents view, // show them once at the top of a fresh session, but never append them under a // restored conversation where they read as disconnected clutter. - if (!ownsGlobalStartupNotices || this.sessionHasMessages) { + if (!ownsGlobalStartupNotices || !this.isNewChat()) { return; } @@ -2649,9 +2648,24 @@ export class InteractiveMode { return; } switch (event.type) { - case "agent_start": + case "agent_start": { + const wasNewChat = this.isNewChat(); this.patchConnectionState({ isStreaming: true, activeToolNames: [] }); + if (wasNewChat) { + this.builtInHeader?.invalidate(); + this.subagentSummaryLine.invalidate(); + } + break; + } + case "message_end": { + const wasNewChat = this.isNewChat(); + this.patchConnectionState({ messageCount: this.connectionState.messageCount + 1 }); + if (wasNewChat) { + this.builtInHeader?.invalidate(); + this.subagentSummaryLine.invalidate(); + } break; + } case "agent_end": this.patchConnectionState({ isStreaming: false, activeToolNames: [] }); break; @@ -5325,7 +5339,6 @@ export class InteractiveMode { } if (event.type === "message_start" && (event.message.role === "user" || isAgentSessionMessage(event.message))) { this.contextUsageTokenBaseline = 0; - this.setSessionHasMessages(true); this.clearShortcutGuide(); this.agentRunFileChanges.clear(); this.renderRecap(); @@ -6041,16 +6054,7 @@ export class InteractiveMode { } private isNewChat(): boolean { - return !this.sessionHasMessages; - } - - private setSessionHasMessages(hasMessages: boolean): void { - if (this.sessionHasMessages === hasMessages) { - return; - } - this.sessionHasMessages = hasMessages; - this.builtInHeader?.invalidate(); - this.subagentSummaryLine.invalidate(); + return (this.connectionState?.messageCount ?? 0) === 0 && this.connectionState?.isStreaming !== true; } private getModelTrayLabel(): string { @@ -6570,7 +6574,6 @@ export class InteractiveMode { const streamingMessage = snapshot.streamingMessage; this.rlmNodeId = snapshot.parent?.childId; this.seedSubagentSummary(snapshot.children); - this.setSessionHasMessages(context.messages.length > 0); this.applyConnectionStateSnapshot(state); this.restoreTurnStartFromMessages(context.messages); await this.renderSessionContext(context, { diff --git a/packages/coding-agent/test/interactive-mode-startup.test.ts b/packages/coding-agent/test/interactive-mode-startup.test.ts index c29a166a87..35444ce746 100644 --- a/packages/coding-agent/test/interactive-mode-startup.test.ts +++ b/packages/coding-agent/test/interactive-mode-startup.test.ts @@ -20,14 +20,15 @@ describe("InteractiveMode startup hints", () => { setKeybindings(new KeybindingsManager()); }); - function createMode(sessionHasMessages = false, returnToAgentsView = false, getEditorText = () => "") { + function createMode(messageCount = 0, returnToAgentsView = false, getEditorText = () => "") { const mode = { - sessionHasMessages, options: { returnToAgentsView }, editor: { getText: getEditorText }, connectionState: { model: { name: "test-model", reasoning: true }, thinkingLevel: "high", + messageCount, + isStreaming: false, }, }; Object.setPrototypeOf(mode, InteractiveMode.prototype); @@ -83,9 +84,35 @@ describe("InteractiveMode startup hints", () => { expect(stripAnsi(label)).toBe("test-model • high ? for shortcuts"); }); + it("keeps fresh-chat guidance hidden when a mid-turn snapshot still has no committed messages", () => { + const mode = createMode(); + const patchConnectionState = (patch: Record) => Object.assign(mode.connectionState, patch); + Object.assign(mode, { + patchConnectionState, + builtInHeader: { invalidate: vi.fn() }, + subagentSummaryLine: { invalidate: vi.fn() }, + }); + const updateConnectionStateFromEvent = Reflect.get( + InteractiveMode.prototype, + "updateConnectionStateFromEvent", + ) as (event: unknown) => void; + const getLabel = () => stripAnsi(Reflect.get(InteractiveMode.prototype, "getTrayLocationLabel").call(mode)); + const message = { role: "user", content: "hello", timestamp: 1 }; + + updateConnectionStateFromEvent.call(mode, { type: "agent_start" }); + updateConnectionStateFromEvent.call(mode, { type: "message_start", message }); + Object.assign(mode.connectionState, { messageCount: 0, isStreaming: true }); + + expect(getLabel()).not.toContain("for shortcuts"); + + updateConnectionStateFromEvent.call(mode, { type: "message_end", message }); + updateConnectionStateFromEvent.call(mode, { type: "agent_end", messages: [message] }); + expect(getLabel()).not.toContain("for shortcuts"); + }); + it("routes session-view requests through the existing agents-view return path", async () => { const returnToAgentsView = vi.fn(async () => {}); - const mode = Object.assign(createMode(false, true), { returnToAgentsView }); + const mode = Object.assign(createMode(0, true), { returnToAgentsView }); await Reflect.get(InteractiveMode.prototype, "requestAgentsView").call(mode); @@ -96,7 +123,7 @@ describe("InteractiveMode startup hints", () => { const returnToAgentsView = vi.fn(async () => {}); const showStatus = vi.fn(); const mode = Object.assign( - createMode(false, true, () => "draft prompt"), + createMode(0, true, () => "draft prompt"), { returnToAgentsView, showStatus }, ); @@ -110,7 +137,7 @@ describe("InteractiveMode startup hints", () => { const returnToAgentsView = vi.fn(async () => {}); const showStatus = vi.fn(); const mode = Object.assign( - createMode(false, true, () => "scoped draft"), + createMode(0, true, () => "scoped draft"), { returnToAgentsView, showStatus }, ); @@ -127,7 +154,7 @@ describe("InteractiveMode startup hints", () => { resolveDispose = resolve; }); const mode = Object.assign( - createMode(false, true, () => "draft prompt"), + createMode(0, true, () => "draft prompt"), { promptStashState, pastedImages: new Map(), @@ -152,7 +179,7 @@ describe("InteractiveMode startup hints", () => { it("opens the shared session view on back navigation for process-local chats", async () => { const requestAgentsView = vi.fn(async () => {}); const returnToAgentsView = vi.fn(async () => {}); - const mode = Object.assign(createMode(false, false), { requestAgentsView, returnToAgentsView }); + const mode = Object.assign(createMode(0, false), { requestAgentsView, returnToAgentsView }); const handled = Reflect.get(InteractiveMode.prototype, "handleAgentsBack").call(mode) as boolean; @@ -164,7 +191,7 @@ describe("InteractiveMode startup hints", () => { it("returns to the daemon agents view on back navigation for daemon chats", async () => { const requestAgentsView = vi.fn(async () => {}); const returnToAgentsView = vi.fn(async () => {}); - const mode = Object.assign(createMode(false, true), { requestAgentsView, returnToAgentsView }); + const mode = Object.assign(createMode(0, true), { requestAgentsView, returnToAgentsView }); const handled = Reflect.get(InteractiveMode.prototype, "handleAgentsBack").call(mode) as boolean; @@ -176,7 +203,7 @@ describe("InteractiveMode startup hints", () => { it("leaves back navigation to the editor while a draft exists", async () => { const requestAgentsView = vi.fn(async () => {}); const mode = Object.assign( - createMode(false, false, () => "draft prompt"), + createMode(0, false, () => "draft prompt"), { requestAgentsView }, ); @@ -189,7 +216,7 @@ describe("InteractiveMode startup hints", () => { it("explains that the agents view needs the daemon for non-daemon chats", async () => { const showStatus = vi.fn(); const shutdown = vi.fn(async () => {}); - const mode = Object.assign(createMode(false, false), { + const mode = Object.assign(createMode(0, false), { returnToAgentsView: vi.fn(async () => {}), showStatus, shutdown, @@ -203,7 +230,7 @@ describe("InteractiveMode startup hints", () => { it("keeps the lowercase agents hint while typing", () => { let editorText = ""; - const mode = createMode(false, true, () => editorText); + const mode = createMode(0, true, () => editorText); const getLabel = () => Reflect.get(InteractiveMode.prototype, "getTrayLocationLabel").call(mode); expect(stripAnsi(getLabel())).toBe("← agents/resume test-model • high ? for shortcuts"); @@ -214,7 +241,7 @@ describe("InteractiveMode startup hints", () => { it("hides the fresh-chat shortcut hint while the prompt has text", () => { let editorText = ""; - const mode = createMode(false, false, () => editorText); + const mode = createMode(0, false, () => editorText); const getLabel = () => Reflect.get(InteractiveMode.prototype, "getTrayLocationLabel").call(mode); expect(stripAnsi(getLabel())).toBe("test-model • high ? for shortcuts"); @@ -230,7 +257,7 @@ describe("InteractiveMode startup hints", () => { }); it("hides the tray shortcut guidance for chats with history", () => { - const mode = createMode(true); + const mode = createMode(1); const label = Reflect.get(InteractiveMode.prototype, "getTrayLocationLabel").call(mode); expect(stripAnsi(label)).toBe("test-model • high"); diff --git a/packages/coding-agent/test/interactive-mode-status.test.ts b/packages/coding-agent/test/interactive-mode-status.test.ts index c9cac26fce..85837102ca 100644 --- a/packages/coding-agent/test/interactive-mode-status.test.ts +++ b/packages/coding-agent/test/interactive-mode-status.test.ts @@ -711,7 +711,6 @@ describe("InteractiveMode working timer", () => { model: null, })), seedSubagentSummary: vi.fn(), - setSessionHasMessages: vi.fn(), applyConnectionStateSnapshot: vi.fn((state: AgentConnectionState) => { streaming = state.isStreaming; }), @@ -1501,7 +1500,6 @@ describe("InteractiveMode connection events", () => { model: null, })), seedSubagentSummary: vi.fn(), - setSessionHasMessages: vi.fn(), applyConnectionStateSnapshot: vi.fn(), renderSessionContext: renderSessionContextMock, restoreStreamingMessageFromSnapshot, @@ -3322,7 +3320,7 @@ describe("InteractiveMode Prime CLI onboarding", () => { showWarning: vi.fn(), showError: vi.fn(), getCurrentCwd: () => startupRunResult.source.cwd, - sessionHasMessages: false, + connectionState: { messageCount: 0 }, ...overrides, }; } diff --git a/packages/coding-agent/test/interactive-mode-streaming.test.ts b/packages/coding-agent/test/interactive-mode-streaming.test.ts index 6b628a4946..494c1f29c6 100644 --- a/packages/coding-agent/test/interactive-mode-streaming.test.ts +++ b/packages/coding-agent/test/interactive-mode-streaming.test.ts @@ -51,7 +51,6 @@ type HandleEventThis = { checkShutdownRequested(): Promise; applyOptimisticContextUsage(): void; refreshConnectionContextUsage(): Promise; - setSessionHasMessages(hasMessages: boolean): void; clearShortcutGuide(): void; addMessageToChat(): void; }; @@ -102,7 +101,6 @@ function createFakeInteractiveModeThis(): HandleEventThis { checkShutdownRequested: vi.fn(async () => {}), applyOptimisticContextUsage: vi.fn(), refreshConnectionContextUsage: vi.fn(async () => {}), - setSessionHasMessages: vi.fn(), clearShortcutGuide: vi.fn(), addMessageToChat: vi.fn(), }; diff --git a/packages/coding-agent/test/suite/regressions/4533-preserve-recap.test.ts b/packages/coding-agent/test/suite/regressions/4533-preserve-recap.test.ts index 49253a889f..4110fedae0 100644 --- a/packages/coding-agent/test/suite/regressions/4533-preserve-recap.test.ts +++ b/packages/coding-agent/test/suite/regressions/4533-preserve-recap.test.ts @@ -21,7 +21,6 @@ type MessageStartMode = { footer: { invalidate: () => void }; updateConnectionStateFromEvent: (event: unknown) => void; contextUsageTokenBaseline: number; - setSessionHasMessages: (hasMessages: boolean) => void; clearShortcutGuide: () => void; activityTracker: { handleEvent: (event: unknown) => void }; updateWorkingLoaderMessage: () => void; @@ -56,7 +55,6 @@ function createMessageStartMode(): MessageStartMode { footer: { invalidate: vi.fn() }, updateConnectionStateFromEvent: vi.fn(), contextUsageTokenBaseline: 12, - setSessionHasMessages: vi.fn(), clearShortcutGuide: vi.fn(), activityTracker: { handleEvent: vi.fn() }, updateWorkingLoaderMessage: vi.fn(),