Skip to content
1 change: 1 addition & 0 deletions packages/coding-agent/.changes/use-message-count.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Fixed new-chat hints to use the session message count.
33 changes: 18 additions & 15 deletions packages/coding-agent/src/modes/interactive/interactive-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -990,7 +990,6 @@ export class InteractiveMode {
private connectionModelsRefreshInFlight: { version: number; promise: Promise<AgentConnectionModel[]> } | undefined;
private connectionState: AgentConnectionState | undefined;
private connectionResourceSnapshot: AgentConnectionResourceSnapshot | undefined;
private sessionHasMessages = false;
private heartbeatCatalog: AgentConnectionHeartbeat[] = [];
private heartbeats: AgentConnectionHeartbeat[] = [];
private heartbeatRefreshPromise: Promise<void> | undefined;
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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 });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium interactive/interactive-mode.ts:2662

messageCount is incremented for every message_end, so a transient durable-command error result makes an otherwise empty session appear non-new. This suppresses fresh-session hints and deferred startup notices until a snapshot or rebind corrects the state; only increment the count for messages that were actually persisted, or expose that distinction on the event.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/src/modes/interactive/interactive-mode.ts around line 2662:

`messageCount` is incremented for every `message_end`, so a transient durable-command error result makes an otherwise empty session appear non-new. This suppresses fresh-session hints and deferred startup notices until a snapshot or rebind corrects the state; only increment the count for messages that were actually persisted, or expose that distinction on the event.

Evidence trail:
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2660-2666,6056-6057 @ 786da59ea9f208faada21c519a826ced622d011f
packages/coding-agent/src/core/agent-session.ts:6095-6116,6139-6149 @ 786da59ea9f208faada21c519a826ced622d011f
packages/coding-agent/src/modes/agent-connection/snapshot.ts:27-48 @ 786da59ea9f208faada21c519a826ced622d011f

if (wasNewChat) {
this.builtInHeader?.invalidate();
this.subagentSummaryLine.invalidate();
}
break;
Comment thread
snimu marked this conversation as resolved.
}
case "agent_end":
this.patchConnectionState({ isStreaming: false, activeToolNames: [] });
break;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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, {
Expand Down
53 changes: 40 additions & 13 deletions packages/coding-agent/test/interactive-mode-startup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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<string, unknown>) => 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);

Expand All @@ -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 },
);

Expand All @@ -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 },
);

Expand All @@ -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(),
Expand All @@ -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;

Expand All @@ -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;

Expand All @@ -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 },
);

Expand All @@ -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,
Expand All @@ -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");
Expand All @@ -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");
Expand All @@ -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");
Expand Down
4 changes: 1 addition & 3 deletions packages/coding-agent/test/interactive-mode-status.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -711,7 +711,6 @@ describe("InteractiveMode working timer", () => {
model: null,
})),
seedSubagentSummary: vi.fn(),
setSessionHasMessages: vi.fn(),
applyConnectionStateSnapshot: vi.fn((state: AgentConnectionState) => {
streaming = state.isStreaming;
}),
Expand Down Expand Up @@ -1501,7 +1500,6 @@ describe("InteractiveMode connection events", () => {
model: null,
})),
seedSubagentSummary: vi.fn(),
setSessionHasMessages: vi.fn(),
applyConnectionStateSnapshot: vi.fn(),
renderSessionContext: renderSessionContextMock,
restoreStreamingMessageFromSnapshot,
Expand Down Expand Up @@ -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,
};
}
Expand Down
2 changes: 0 additions & 2 deletions packages/coding-agent/test/interactive-mode-streaming.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ type HandleEventThis = {
checkShutdownRequested(): Promise<void>;
applyOptimisticContextUsage(): void;
refreshConnectionContextUsage(): Promise<void>;
setSessionHasMessages(hasMessages: boolean): void;
clearShortcutGuide(): void;
addMessageToChat(): void;
};
Expand Down Expand Up @@ -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(),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(),
Expand Down
Loading