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.
25 changes: 11 additions & 14 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 @@ -2645,6 +2644,15 @@ export class InteractiveMode {
case "agent_start":
this.patchConnectionState({ isStreaming: true, activeToolNames: [] });
break;
case "message_start": {
const wasNewChat = this.connectionState.messageCount === 0;
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 @@ -5321,7 +5329,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 @@ -6037,16 +6044,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;
}

private getModelTrayLabel(): string {
Expand Down Expand Up @@ -6566,7 +6564,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
26 changes: 13 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,14 @@ 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,
},
};
Object.setPrototypeOf(mode, InteractiveMode.prototype);
Expand Down Expand Up @@ -85,7 +85,7 @@ describe("InteractiveMode startup hints", () => {

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 +96,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 +110,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 +127,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 +152,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 +164,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 +176,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 +189,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 +203,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 +214,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 +230,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
29 changes: 26 additions & 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 @@ -1435,6 +1434,31 @@ describe("InteractiveMode pending bash components", () => {
});

describe("InteractiveMode connection events", () => {
test("updates the session message count from message starts", () => {
const builtInHeader = { invalidate: vi.fn() };
const subagentSummaryLine = { invalidate: vi.fn() };
const harness = {
connectionState: createConnectionState(),
builtInHeader,
subagentSummaryLine,
patchConnectionState(patch: Partial<AgentConnectionState>) {
this.connectionState = { ...this.connectionState, ...patch };
},
};
const updateConnectionStateFromEvent = (
InteractiveMode.prototype as unknown as {
updateConnectionStateFromEvent(this: typeof harness, event: AgentConnectionSessionEvent): void;
}
).updateConnectionStateFromEvent;

updateConnectionStateFromEvent.call(harness, { type: "message_start", message: userMessage("one", 1) });
updateConnectionStateFromEvent.call(harness, { type: "message_start", message: userMessage("two", 2) });

expect(harness.connectionState.messageCount).toBe(2);
expect(builtInHeader.invalidate).toHaveBeenCalledOnce();
expect(subagentSummaryLine.invalidate).toHaveBeenCalledOnce();
});

test("rendering a switched session updates the pending display from its snapshot", async () => {
const harness = {
resetCurrentSessionRenderState: vi.fn(),
Expand Down Expand Up @@ -1501,7 +1525,6 @@ describe("InteractiveMode connection events", () => {
model: null,
})),
seedSubagentSummary: vi.fn(),
setSessionHasMessages: vi.fn(),
applyConnectionStateSnapshot: vi.fn(),
renderSessionContext: renderSessionContextMock,
restoreStreamingMessageFromSnapshot,
Expand Down Expand Up @@ -3262,7 +3285,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