Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
6d88232
feat(native-chat): a new chat starts at rest; its first message start…
brennanb2025 Oct 1, 2026
b1bb116
test(native-chat): tests for a chat created at rest and started by it…
brennanb2025 Oct 1, 2026
67468b7
test(native-chat): split the provider-row integration tests, and sett…
brennanb2025 Oct 1, 2026
cba838f
refactor(native-chat): a record is founded only by its create; tests …
brennanb2025 Oct 1, 2026
51387f9
fix(native-chat): a create founds again a record whose agent never ra…
brennanb2025 Oct 1, 2026
e4ef29c
fix(native-chat): a create stays pending until its journal is open an…
brennanb2025 Oct 1, 2026
e6c256a
fix(native-chat): a quit drains a create in flight before it closes c…
brennanb2025 Oct 1, 2026
c7afc67
fix(native-chat): the agentSession.create span measures a chat's firs…
brennanb2025 Oct 1, 2026
8c58a72
fix(orchestration): a structured worker's preamble waits across a ret…
brennanb2025 Oct 1, 2026
42f770b
fix(native-chat): a launch prompt counts as delivered only once the h…
brennanb2025 Oct 1, 2026
a4b3dd2
test(native-chat): a create over agentSession.* stays at rest on repl…
brennanb2025 Oct 1, 2026
3ece387
test(native-chat): exact fences after a restart, and tab tests that t…
brennanb2025 Oct 1, 2026
4df1580
docs(native-chat): comments say a create starts nothing and ensure is…
brennanb2025 Oct 1, 2026
f97fa4d
docs(orchestration): a structured worker is created at rest and start…
brennanb2025 Oct 1, 2026
a8f1670
test(native-chat): type the seeded location as an execution location
brennanb2025 Oct 1, 2026
19ace35
test(native-chat): type the launch prompt handover fixtures as the wi…
brennanb2025 Oct 1, 2026
9569ccb
test(native-chat): build the comment-resolution hook model without ty…
brennanb2025 Oct 1, 2026
4fb0da0
fix(native-chat): a create retried after its operation row aged out a…
brennanb2025 Oct 1, 2026
d2a25c5
fix(native-chat): a launch prompt waits for its message's own final s…
brennanb2025 Oct 1, 2026
6922ca6
fix(native-chat): a Claude chat at rest names no model until one runs
brennanb2025 Oct 1, 2026
f50792b
test(native-chat): type the first-message endings table
brennanb2025 Oct 1, 2026
11db617
fix(native-chat): "Started an AI agent" toasts wait for the agent to …
brennanb2025 Oct 1, 2026
9b48e2a
test(native-chat): closing a chat publishes its waiting first message…
brennanb2025 Oct 1, 2026
1c49e81
fix(native-chat): a refounded record keeps its lease's fence floor an…
brennanb2025 Oct 1, 2026
c483625
fix(orchestration): a structured worker's preamble waits only as long…
brennanb2025 Oct 1, 2026
e98777e
test(native-chat): the launch-prompt ordering test creates at rest an…
brennanb2025 Oct 1, 2026
dac6af5
test(native-chat): start existing chats with startAgentForTests inste…
brennanb2025 Oct 1, 2026
c4a87d5
fix(native-chat): a chat at rest with no model listing keeps the stat…
brennanb2025 Oct 1, 2026
0d2940b
fix(native-chat): a launch prompt keeps waiting through an unknown an…
brennanb2025 Oct 1, 2026
e2a3301
test(native-chat): give each already-settled launch prompt case its o…
brennanb2025 Oct 1, 2026
e4806e9
fix(native-chat): a create adjudicates an unreconciled lease before i…
brennanb2025 Oct 1, 2026
f6312da
fix(agent-launch): a phone's launch prompt counts as delivered only o…
brennanb2025 Oct 1, 2026
463fac6
refactor(agent-launch): name every state a launch prompt's settlement…
brennanb2025 Oct 1, 2026
c0a419c
fix(agent-launch): a launch prompt still waiting on a retried start a…
brennanb2025 Oct 1, 2026
47fb51d
fix(native-chat): a launch prompt rejected for good is sent again fro…
brennanb2025 Oct 1, 2026
8c21d7e
fix(agent-launch): a launch prompt whose answer was lost keeps waitin…
brennanb2025 Oct 1, 2026
25fec28
test(agent-launch): type the adapter close mock as the defined method
brennanb2025 Oct 1, 2026
8e01693
fix(native-chat): notes sent to a chat count as sent only once its ag…
brennanb2025 Oct 1, 2026
fba2e4c
test(native-chat): a refused message from another surface keeps its id
brennanb2025 Oct 1, 2026
58ad6d3
test(native-chat): type the held refusal fixture as a refusal
brennanb2025 Oct 1, 2026
96c4502
test(native-chat): read the started chat's fence instead of the at-re…
brennanb2025 Oct 1, 2026
2bf5b0e
fix(i18n): name the agent's prompt in each catalog's own terms
brennanb2025 Oct 1, 2026
5fff793
fix(native-chat): a Stop keeps a launch prompt as not sent instead of…
brennanb2025 Oct 1, 2026
bbe4b35
fix(native-chat): an uninstalled agent's notice says "Install it firs…
brennanb2025 Oct 1, 2026
13d8314
Merge #24340 (brennanb2025/chat-start-failure-on-message 7b87ac87fca)…
brennanb2025 Oct 1, 2026
edac8df
test(native-chat): line up #24340's tests with a chat created at rest…
brennanb2025 Oct 2, 2026
6ed5e6b
revert(native-chat): sends into an existing chat keep the chat's Retr…
brennanb2025 Oct 2, 2026
798dd8b
fix(native-chat): a Claude chat's first start names no model until th…
brennanb2025 Oct 2, 2026
871f6dd
fix(native-chat): a composer's launch prompt keeps the chat's Retry a…
brennanb2025 Oct 2, 2026
59e4d21
fix(native-chat): a session continuation's prompt keeps the chat's Retry
brennanb2025 Oct 2, 2026
1385b3d
test(native-chat): the launch plan hands on a caller's explicit promp…
brennanb2025 Oct 2, 2026
f4e6b36
fix(native-chat): a desktop that reads a pending send as delivered is…
brennanb2025 Oct 2, 2026
ecdd84e
test(native-chat): read the baseline client's functions and the creat…
brennanb2025 Oct 2, 2026
58395b6
Revert "test(native-chat): read the baseline client's functions and t…
brennanb2025 Oct 2, 2026
2b65c04
Revert "fix(native-chat): a desktop that reads a pending send as deli…
brennanb2025 Oct 2, 2026
0b105f9
Merge #24340 (brennanb2025/chat-start-failure-on-message) into chat-s…
brennanb2025 Oct 3, 2026
743f57d
test(native-chat): main's Stop-event and preamble tests write at the …
brennanb2025 Oct 3, 2026
059444d
revert(native-chat): a launch prompt counts as delivered once the hos…
brennanb2025 Oct 3, 2026
e1e89b4
fix(mobile): an AI button says the prompt was sent to an agent, not t…
brennanb2025 Oct 3, 2026
27ec99b
feat(native-chat): a launch prompt's review reply runs on the host on…
brennanb2025 Oct 3, 2026
cbe7222
feat(checks-panel): Resolve comments with AI hands a structured chat …
brennanb2025 Oct 3, 2026
48b2995
refactor(native-chat): the record store's commit listeners and the co…
brennanb2025 Oct 3, 2026
d178fc6
Merge #24340 (brennanb2025/chat-start-failure-on-message 61845dcd424)…
brennanb2025 Oct 3, 2026
ec979b4
fix(native-chat): a review reply's receipt is written in its chat's o…
brennanb2025 Oct 3, 2026
34fe8e7
fix(native-chat): a review reply re-run after a restart skips only re…
brennanb2025 Oct 3, 2026
f36c641
fix(native-chat): a review reply rides a launch prompt only to a host…
brennanb2025 Oct 3, 2026
fe7636a
fix(native-chat): a review reply re-run reads the reply threads newes…
brennanb2025 Oct 3, 2026
b0048a9
test(github): a thread read GitHub answers with errors beside data st…
brennanb2025 Oct 3, 2026
fe83eb7
test(cross-version): a send takes a review reply exactly where the ho…
brennanb2025 Oct 3, 2026
f73c65c
fix(native-chat): a review reply that wrote to a GitHub PR tells the …
brennanb2025 Oct 3, 2026
db7edca
fix(checks-panel): a structured chat that still holds an unrecorded l…
brennanb2025 Oct 3, 2026
710cdf6
fix(checks-panel): Resolve comments with AI says once when a structur…
brennanb2025 Oct 3, 2026
6d829bf
fix(i18n): desktop AI-fix toasts say the work was sent to an agent, n…
brennanb2025 Oct 3, 2026
e8229ac
fix(mobile): a launch whose prompt or notes weren't sent no longer sa…
brennanb2025 Oct 3, 2026
66d7862
docs(checks-panel): onLaunched fires once the prompt is delivered, wh…
brennanb2025 Oct 3, 2026
f513831
test(cross-version): the agent-session suite's runtime stub and call …
brennanb2025 Oct 3, 2026
c214620
fix(checks-panel): a failed create hands the comments back and leaves…
brennanb2025 Oct 3, 2026
8ea74b1
fix(checks-panel): the panel writes the review reply unless a structu…
brennanb2025 Oct 3, 2026
562d588
docs(native-chat): a refused review-reply receipt is derived again on…
brennanb2025 Oct 3, 2026
532794e
fix(checks-panel): the panel refetches the PR once a structured chat'…
brennanb2025 Oct 3, 2026
a43c439
fix(checks-panel): an armed review-reply watch keeps its chat's read …
brennanb2025 Oct 3, 2026
40d2e8c
test(checks-panel): the read-hold fixture types its list without an a…
brennanb2025 Oct 3, 2026
599fd1c
fix(checks-panel): a review-reply watch reads the chat on the host th…
brennanb2025 Oct 3, 2026
dc7f371
fix(native-chat): a failed review reply's line stays in sight after t…
brennanb2025 Oct 3, 2026
5fa3b98
test(native-chat): the failed review reply's line is written while th…
brennanb2025 Oct 3, 2026
436eeac
test(mobile): re-record the notes-not-delivered golden with the launc…
brennanb2025 Oct 3, 2026
9217694
fix(checks-panel): the missing-PR reply toast says what to do
brennanb2025 Oct 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,6 @@
"sessionTabs": [],
"terminals": []
},
"776182f604c7": {
"name": "toast",
"ordinal": 4,
"value": {
"durationMs": 2400,
"message": "The agent started, but the notes weren't sent."
}
},
"8f06d46b0825": {
"name": "agent.launchReplay#1",
"ordinal": 1,
Expand Down Expand Up @@ -129,6 +121,14 @@
"status": "pending",
"startedAt": 0
},
"ac15d5ff2168": {
"name": "toast",
"ordinal": 4,
"value": {
"durationMs": 2400,
"message": "The notes weren't sent to the agent."
}
},
"ad660413fab0": {
"name": "agent.launchReplay#1",
"ordinal": 1,
Expand Down Expand Up @@ -207,7 +207,7 @@
"create": "eb79a9b3682a"
},
"state": "73449f52ee06",
"effects": ["6248a3dd0710", "776182f604c7"]
"effects": ["6248a3dd0710", "ac15d5ff2168"]
}
},
{
Expand All @@ -220,7 +220,7 @@
"create": "eb79a9b3682a"
},
"state": "73449f52ee06",
"effects": ["6248a3dd0710", "776182f604c7"]
"effects": ["6248a3dd0710", "ac15d5ff2168"]
}
}
]
Expand Down
14 changes: 7 additions & 7 deletions mobile/src/components/AgentLaunchNotice.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,8 @@ describe('AgentLaunchNotice', () => {
})

it('keeps a real failure in the error style beside a warning', () => {
expect(
lines({ error: "The agent started, but the prompt wasn't sent.", warning: 'w' })
).toEqual([
{ text: "The agent started, but the prompt wasn't sent.", isError: true },
expect(lines({ error: "The prompt wasn't sent to the agent.", warning: 'w' })).toEqual([
{ text: "The prompt wasn't sent to the agent.", isError: true },
{ text: 'w', isError: false }
])
})
Expand All @@ -78,14 +76,16 @@ describe('AgentLaunchNotice', () => {
})

it('confirms a launch that went ahead, with any host note beneath it', () => {
expect(lines({ success: 'Agent started in feature-login', warning: 'w' })).toEqual([
{ text: 'Agent started in feature-login', isError: false },
expect(lines({ success: 'Sent to an agent in feature-login', warning: 'w' })).toEqual([
{ text: 'Sent to an agent in feature-login', isError: false },
{ text: 'w', isError: false }
])
})

it('never confirms beside an error', () => {
expect(lines({ success: 'Agent started', error: 'e' })).toEqual([{ text: 'e', isError: true }])
expect(lines({ success: 'Sent to an agent', error: 'e' })).toEqual([
{ text: 'e', isError: true }
])
})

it('renders nothing with nothing to say', () => {
Expand Down
2 changes: 1 addition & 1 deletion mobile/src/components/AgentLaunchNotice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { colors, spacing, typography } from '../theme/mobile-theme'

type Props = {
availability: MobileAgentLaunchAvailability
/** The confirmation that the agent started with its prompt. */
/** The confirmation that the prompt was sent to an agent. */
success: string | null
error: string | null
/** The host's note on a launch that went ahead; secondary text, not an error. */
Expand Down
10 changes: 4 additions & 6 deletions mobile/src/session/new-tab-agent-host-launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,9 @@ import {
type PendingSessionSelection
} from './pending-session-selection'

const NOTES_NOT_SENT_MESSAGE = "The agent started, but the notes weren't sent."
export const PROMPT_UNCONFIRMED_MESSAGE =
"The agent started, but couldn't confirm the prompt was sent."
export const NOTES_UNCONFIRMED_MESSAGE =
"The agent started, but couldn't confirm the notes were sent."
const NOTES_NOT_SENT_MESSAGE = "The notes weren't sent to the agent."
export const PROMPT_UNCONFIRMED_MESSAGE = "Couldn't confirm the prompt was sent to the agent."
export const NOTES_UNCONFIRMED_MESSAGE = "Couldn't confirm the notes were sent to the agent."

export type NewTabAgentLaunchOptions = MobileQuickCommandLaunch['options'] & {
onPromptSent?: () => void
Expand Down Expand Up @@ -80,7 +78,7 @@ export async function launchNewTabAgentThroughHost(args: {
return true
}
if (launched.kind === 'unknown') {
// Why: a listed tab proves the agent started, so only the prompt is in doubt; notes stay unsent.
// Why: a listed tab proves the launch went through, so only the prompt is in doubt; notes stay unsent.
if (isLaunchedSurfaceListed(args.getSessionTabs(), reservation)) {
if (prompt?.trim()) {
triggerError()
Expand Down
16 changes: 8 additions & 8 deletions mobile/src/session/pr-ai-triage-launch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { RpcResponse } from '../transport/types'
import { AGENT_LAUNCH_UPDATE_REQUIRED_MESSAGE } from './mobile-existing-agent-launch'
import {
AGENT_PROMPT_NOT_SENT_MESSAGE,
agentStartedMessage,
agentPromptSentMessage,
launchAgentWithPrompt,
promptedLaunchNotice
} from './pr-ai-triage-launch'
Expand Down Expand Up @@ -265,7 +265,7 @@ describe('promptedLaunchNotice', () => {
})
expect(promptedLaunchNotice({ kind: 'sent' })).toEqual({
succeeded: true,
success: 'Agent started',
success: 'Sent to an agent',
error: null,
warning: null,
undeliveredPrompt: null
Expand All @@ -283,7 +283,7 @@ describe('promptedLaunchNotice', () => {
it('never reports the host warning on a launch that went ahead as an error', () => {
expect(promptedLaunchNotice({ kind: 'sent', warning: 'arguments were ignored' })).toEqual({
succeeded: true,
success: 'Agent started',
success: 'Sent to an agent',
error: null,
warning: 'arguments were ignored',
undeliveredPrompt: null
Expand All @@ -304,16 +304,16 @@ describe('promptedLaunchNotice', () => {
})
})

describe('agentStartedMessage', () => {
describe('agentPromptSentMessage', () => {
// Issue #20543: a launch from the phone must say it happened, and where.
it('names the workspace the agent started in', () => {
it('names the workspace the prompt went to', () => {
expect(promptedLaunchNotice({ kind: 'sent' }, 'feature-login').success).toBe(
'Agent started in feature-login'
'Sent to an agent in feature-login'
)
})

it('still confirms the launch when the screen knows no workspace name', () => {
expect(agentStartedMessage(null)).toBe('Agent started')
expect(agentStartedMessage(' ')).toBe('Agent started')
expect(agentPromptSentMessage(null)).toBe('Sent to an agent')
expect(agentPromptSentMessage(' ')).toBe('Sent to an agent')
})
})
14 changes: 8 additions & 6 deletions mobile/src/session/pr-ai-triage-launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ import { resolveMobileSourceControlLaunchAgent } from './mobile-source-control-l

export type MobilePromptedAgentLaunch =
| { kind: 'sent'; warning?: string }
/** The agent started but `prompt`, the text as sent, did not reach it; the caller offers it. */
/** The launch went through but `prompt`, the text as sent, did not reach the agent; the caller
* offers it. */
| { kind: 'prompt-not-sent'; prompt: string; warning?: string }
/** Nothing started; `message` says why. */
| { kind: 'not-started'; message: string }
/** The agent may be running; do not launch again until the user has looked. */
| { kind: 'unconfirmed'; message: string }

export const AGENT_PROMPT_NOT_SENT_MESSAGE = "The agent started, but the prompt wasn't sent."
export const AGENT_PROMPT_NOT_SENT_MESSAGE = "The prompt wasn't sent to the agent."

export async function launchAgentWithPrompt(args: {
client: RpcClient
Expand Down Expand Up @@ -91,10 +92,11 @@ export async function launchAgentWithPrompt(args: {
}
}

/** The confirmation under an AI button, naming the workspace when the screen knows it. */
export function agentStartedMessage(workspaceLabel: string | null | undefined): string {
/** The confirmation under an AI button, naming the workspace when the screen knows it. Said once
* the host has the prompt: the agent may still be starting, and a failed start shows in its chat. */
export function agentPromptSentMessage(workspaceLabel: string | null | undefined): string {
const label = workspaceLabel?.trim()
return label ? `Agent started in ${label}` : 'Agent started'
return label ? `Sent to an agent in ${label}` : 'Sent to an agent'
}

/** What the button shows after a launch; one mapping so every AI button reads the same.
Expand All @@ -113,7 +115,7 @@ export function promptedLaunchNotice(
case 'sent':
return {
succeeded: true,
success: agentStartedMessage(workspaceLabel),
success: agentPromptSentMessage(workspaceLabel),
error: null,
warning: result.warning ?? null,
undeliveredPrompt: null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ describe('useMobileDiffReviewSendActions', () => {
})
expect(saveCommentsAndReviewState).not.toHaveBeenCalled()
expect(setActionError).toHaveBeenLastCalledWith(
"The agent started, but the notes weren't sent. Use Copy Notes to paste them."
"The notes weren't sent to the agent. Use Copy Notes to paste them."
)
})

Expand Down
4 changes: 1 addition & 3 deletions mobile/src/session/use-mobile-diff-review-send-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,7 @@ export function useMobileDiffReviewSendActions(input: SendActionsInput) {
}
if (result.kind === 'prompt-not-sent') {
// Notes stay unsent so Copy Notes and a later send still carry them.
setActionError(
"The agent started, but the notes weren't sent. Use Copy Notes to paste them."
)
setActionError("The notes weren't sent to the agent. Use Copy Notes to paste them.")
return
}
await markNotesSent(comments)
Expand Down
4 changes: 2 additions & 2 deletions mobile/src/session/use-mobile-pr-ai-triage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ describe('useMobilePrAiTriage', () => {
await triage?.launch('fix-checks', () => 'fix the failing checks')
})
expect(triage?.noticeFor('fix-checks')).toMatchObject({
error: "The agent started, but the prompt wasn't sent."
error: "The prompt wasn't sent to the agent."
})
expect(triage?.noticeFor('fix-checks').undeliveredPrompt).toContain('fix the failing checks')
expect(triage?.noticeFor('fix-checks').success).toBeNull()
Expand All @@ -101,7 +101,7 @@ describe('useMobilePrAiTriage', () => {
await triage?.launch('resolve-conflicts', () => 'resolve the conflicts')
})
expect(triage?.noticeFor('resolve-conflicts')).toEqual({
success: 'Agent started in feature-login',
success: 'Sent to an agent in feature-login',
error: null,
warning: null,
undeliveredPrompt: null
Expand Down
2 changes: 1 addition & 1 deletion mobile/src/session/use-mobile-pr-ai-triage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export type PrAiTriageLaunchNotice = {
success: string | null
error: string | null
warning: string | null
/** The agent started without its prompt; kept so the user can paste it in themselves. */
/** The launch went through without its prompt; kept so the user can paste it in themselves. */
undeliveredPrompt: string | null
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -517,10 +517,7 @@ describe('launches that carry a prompt', () => {
await create_(state, 'codex', { initialPrompt: 'the notes', onPromptSent })

expect(onPromptSent).not.toHaveBeenCalled()
expect(state.showToast).toHaveBeenCalledWith(
"The agent started, but the notes weren't sent.",
2400
)
expect(state.showToast).toHaveBeenCalledWith("The notes weren't sent to the agent.", 2400)
})

it('says a quick command prompt was not sent', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ describe('useMobileCommitFailureRecovery', () => {
expect(succeeded).toBe(true)
expect(recovery?.launchError).toBeNull()
expect(recovery?.launchWarning).toBe('the requested arguments were ignored.')
expect(recovery?.launchSuccess).toBe('Agent started in feature-login')
expect(recovery?.launchSuccess).toBe('Sent to an agent in feature-login')
// A new failure is a new launch; the old one's note does not carry over.
rerender({ ...FAILURE, error: 'another hook failed' })
expect(recovery?.launchWarning).toBeNull()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export function useMobileCommitFailureRecovery({
// would start a second agent.
const inFlightRef = useRef(false)
const [launchError, setLaunchError] = useState<string | null>(null)
// The agent started without its prompt; kept so the user can paste it in themselves. Keyed by the
// The launch went through without its prompt; kept so the user can paste it in themselves. Keyed by the
// failure it was built for, so a new failure never shows the previous one's prompt.
const [undelivered, setUndelivered] = useState<{
failure: MobileCommitFailureRecovery
Expand Down
5 changes: 4 additions & 1 deletion src/main/claude/claude-api-retry-idle-sweep.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from '../native-chat/agent-session-wire/structured-agent-session-host-test-data'
import { createClaudeJournalTranslator } from './claude-structured-journal-translation'
import { openTestJournalHostDatabase } from '../native-chat/agent-session-journal/journal-host-database-test-support'
import { attachForTests } from '../native-chat/agent-session-wire/structured-agent-session-attach-test-support'
import { createStructuredAgentSessionLogger } from '../native-chat/agent-session-wire/structured-agent-session-logger'

const SWEEP_MS = 5
Expand Down Expand Up @@ -96,7 +97,9 @@ afterEach(async () => {

describe('a Claude retrying a refused request', () => {
it('keeps the conversation open past the idle window while retry frames arrive', async () => {
expect(await host.attach({ callerKey: 'client-1' }, hostTestAttachParams(null))).toMatchObject({
expect(
await attachForTests(host, { callerKey: 'client-1' }, hostTestAttachParams(null))
).toMatchObject({
ok: true
})
if (!sink) {
Expand Down
26 changes: 26 additions & 0 deletions src/main/claude/claude-structured-effort-default-at-rest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,32 @@ describe('Claude effort default at rest', () => {
expect(pickerEffort(resting)).toBe('high')
})

// Every new chat sits at rest until its first message. Settings or env may run another model
// than the listing's default, so the picker names none (and offers no effort) until one runs.
it('names no model for a new chat at rest even with a listing, and the picker shows it unknown', async () => {
const store = new AgentModelCatalogStore()
await startChild(store)

const resting = await readAtRest(store, restingRecord({}))

expect(resting.current).toEqual({ model: '' })
const seed = getAgentSessionOptionCatalog('claude')!
const snapshot = structuredAgentSessionOptionSnapshot(
applyStructuredAgentSessionOptions(
createStructuredAgentSessionOptionState('claude', seed),
seed,
resting
)
)
const model = snapshot.find((row) => row.id === 'model')
expect(model).toMatchObject({ valueSource: 'unknown' })
expect(model?.kind.type === 'select' ? model.kind.currentValue : 'named').toBeUndefined()
expect(
model?.kind.type === 'select' ? model.kind.choices.map((c) => c.value) : []
).not.toContain('')
expect(snapshot.find((row) => row.id === 'effort')).toBeUndefined()
})

it('names no model at rest with no pick and no catalog, so the client keeps what the child ran', async () => {
// The CLI's default model, reported live; the chat never picked one.
const child = await startChild(new AgentModelCatalogStore(), undefined, [], {
Expand Down
3 changes: 2 additions & 1 deletion src/main/claude/claude-structured-effort-reporting.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ describe('Claude effort reporting', () => {

const options = await adapter.readOptions({ sessionId: 'session-1', fence: 7 })
expect(options.current.effort).toBeUndefined()
expect(options.current.model).toBeTruthy()
// Neither the agent's report nor its own listing named a model, so none is named.
expect(options.current.model).toBe('')
})

it('keeps the init fixture free of an effort the real frame never sends', async () => {
Expand Down
23 changes: 23 additions & 0 deletions src/main/claude/claude-structured-options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -480,3 +480,26 @@ describe('Claude structured option restore under the request deadline', () => {
).rejects.toBeInstanceOf(ClaudeControlRequestTimeoutError)
})
})

describe("a Claude chat's first start, before the agent names its model", () => {
function startingSession(): ClaudeSession {
return { ...sessionFor(vi.fn()), startup: createClaudeSessionStartup() }
}

it("names no model, not its listing's default, and still lists the models", async () => {
const result = await readClaudeStructuredSessionOptions(startingSession(), undefined)

expect(result.current.model).toBe('')
expect(result.models.length).toBeGreaterThan(0)
expect(result.models.some((model) => model.isDefault)).toBe(true)
})

it('names the pick a start replays', async () => {
const session = startingSession()
session.options.set('model', 'opus')

const result = await readClaudeStructuredSessionOptions(session, undefined)

expect(result.current.model).toBe('opus')
})
})
5 changes: 4 additions & 1 deletion src/main/claude/claude-structured-session-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,10 @@ export function claudeStructuredSessionOptionsFrom(
writeClaudeCatalogThrough(session, discovered)
const listed = discovered.length > 0 ? discovered : seedModels()
const current = readClaudeCurrentModel(session)
const model = currentModelId(listed, current.id)
// The static seed's default is not this agent's: Claude's settings or env may run another, so
// until a pick, the agent's report or its own listing names one (a start still under way), none.
const model =
current.id === undefined && discovered.length === 0 ? '' : currentModelId(listed, current.id)
const models = structuredAgentSessionOptionModels(listed, model, (row) => ({
...row,
resolvedModel: null
Expand Down
Loading
Loading