Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
ab08cfd
fix(native-chat): a /compact sent while the agent works waits in line…
brennanb2025 Oct 5, 2026
463daba
test(native-chat): build the admission test's submissions without a t…
brennanb2025 Oct 5, 2026
159b6ab
fix(native-chat): a /clear refused while the agent works says so plai…
brennanb2025 Oct 5, 2026
10389f0
test(native-chat): name the admission test's cases for the sends they…
brennanb2025 Oct 5, 2026
6c20ec0
fix(native-chat): a /compact refused while the agent works reads plai…
brennanb2025 Oct 6, 2026
0069210
test(native-chat): a refused /compact reads the plain sentence
brennanb2025 Oct 6, 2026
b67b317
fix(native-chat): a refused command card is spent and said once; a co…
brennanb2025 Oct 6, 2026
6efc87d
test(native-chat): keep the queued receipt's narrowing inside the wait
brennanb2025 Oct 6, 2026
02df320
Merge remote-tracking branch 'origin/main' into brennanb2025/chat-com…
brennanb2025 Oct 6, 2026
030c988
fix(native-chat): spend a command card only when its own turn says wh…
brennanb2025 Oct 6, 2026
f4a8fbf
fix(native-chat): a recorded send is no sending card; queue lists lan…
brennanb2025 Oct 6, 2026
3b4d8f9
feat(native-chat): a /clear sent while the agent works waits in line …
brennanb2025 Oct 6, 2026
185781d
refactor(native-chat): derive the sending cards inside the queued-mes…
brennanb2025 Oct 6, 2026
aef3e8b
Merge remote-tracking branch 'origin/main' into brennanb2025/chat-com…
brennanb2025 Oct 6, 2026
2620f1a
feat(native-chat): clients ask a capable host to hold /clear in line;…
brennanb2025 Oct 6, 2026
46d5b81
test(native-chat): the draft hand-over hook lives with the forwarding…
brennanb2025 Oct 6, 2026
dc02a35
test(native-chat): the chat-tab table's host moves a cleared tab as t…
brennanb2025 Oct 6, 2026
09700a1
Merge remote-tracking branch 'origin/main' into brennanb2025/chat-com…
brennanb2025 Oct 6, 2026
090d1fb
Merge branch 'brennanb2025/chat-command-waits-in-line' of https://git…
brennanb2025 Oct 6, 2026
234f7c5
fix(native-chat): keep protocol-version and the agent-session params …
brennanb2025 Oct 6, 2026
48c8098
fix(native-chat): a /clear card that cannot run says why as its capti…
brennanb2025 Oct 6, 2026
2ed4e06
Merge branch 'brennanb2025/chat-command-waits-in-line' of https://git…
brennanb2025 Oct 6, 2026
c1ff9f7
fix(native-chat): with the agent idle, a command behind this window's…
brennanb2025 Oct 6, 2026
a058ffc
fix(native-chat): a /clear on a chat already cleared says so plainly;…
brennanb2025 Oct 6, 2026
0f62036
Merge remote-tracking branch 'origin/main' into brennanb2025/chat-com…
brennanb2025 Oct 6, 2026
f91a680
fix(native-chat): the queued-commands capability stays a plain consta…
brennanb2025 Oct 6, 2026
120a9ea
fix(native-chat): a /clear card waits out background tasks and a hand…
brennanb2025 Oct 6, 2026
3f6885b
refactor(native-chat): lease events the store tells listeners about l…
brennanb2025 Oct 6, 2026
4bb7d20
fix(native-chat): what a window held for a chat /clear replaced follo…
brennanb2025 Oct 6, 2026
3776b83
Merge remote-tracking branch 'origin/main' into brennanb2025/chat-com…
brennanb2025 Oct 6, 2026
ab0b289
test(native-chat): keep the rewind suite under its line limit after m…
brennanb2025 Oct 6, 2026
47f9858
fix(native-chat): one import per module in the command send; a rewind…
brennanb2025 Oct 6, 2026
a586d91
fix(native-chat): a message left for a cleared chat comes back only o…
brennanb2025 Oct 6, 2026
3330fb9
fix(native-chat): asking about a cleared chat's messages again owns i…
brennanb2025 Oct 6, 2026
70246d8
test(native-chat): type the rewind module mock with a type import
brennanb2025 Oct 6, 2026
dc94eaa
Merge commit 'ab0b289f230' into brennanb2025/chat-clear-waits-in-line
brennanb2025 Oct 6, 2026
02cf47a
Merge remote-tracking branch 'origin/main' into brennanb2025/chat-com…
brennanb2025 Oct 6, 2026
de818d7
test(native-chat): a command card cut short by a restart waits or is …
brennanb2025 Oct 6, 2026
b81775d
fix(native-chat): a cleared chat's leftover says its own cause, and o…
brennanb2025 Oct 6, 2026
338a57c
fix(native-chat): a held command card, which the queue skips, no long…
brennanb2025 Oct 6, 2026
bf018c9
test(native-chat): a reused message id is refused as invalid, as host…
brennanb2025 Oct 6, 2026
799e8b0
Merge commit 'de818d7be40c97ef42a45fcbe53189694c247beb' into brennanb…
brennanb2025 Oct 6, 2026
fa5a568
test(native-chat): kept cards carried by a queued /clear stay kept; i…
brennanb2025 Oct 6, 2026
5e73515
Merge commit '338a57c898bf50b2734cba77967df460fa9eaf99' into brennanb…
brennanb2025 Oct 6, 2026
634c1f5
test(native-chat): a waiting /clear card queues a later send; a held …
brennanb2025 Oct 6, 2026
ff9bc80
fix(native-chat): a send that failed after its request went out is as…
brennanb2025 Oct 6, 2026
5608e5f
fix(native-chat): a cleared chat's send being asked about shows once …
brennanb2025 Oct 6, 2026
7ad4347
fix(native-chat): a cleared chat reopened beside its replacement keep…
brennanb2025 Oct 6, 2026
137cdc8
fix(native-chat): an older host's refusal is never proof; a restored …
brennanb2025 Oct 6, 2026
f1f69c8
fix(mobile): a cleared chat reopened beside its replacement keeps its…
brennanb2025 Oct 6, 2026
2443d72
fix(native-chat): a /clear the queue is about to run offers no Send, …
brennanb2025 Oct 6, 2026
48599fc
fix(native-chat): a command's refusal line goes once what it waits on…
brennanb2025 Oct 6, 2026
2df8b2d
fix(mobile): a command's refusal banner goes once what it waits on is…
brennanb2025 Oct 6, 2026
f30c5d1
Merge commit '2df8b2dcd1c990a1398407cfe29aba4e45061af2' into brennanb…
brennanb2025 Oct 6, 2026
f6480f0
fix(native-chat): a refusal behind an earlier message names whether i…
brennanb2025 Oct 6, 2026
ded6331
test(native-chat): a /clear refused here stands only while the agent …
brennanb2025 Oct 6, 2026
536a85c
test(native-chat): keep the queued-gating test under its line limit
brennanb2025 Oct 6, 2026
479a732
Merge remote-tracking branch 'origin/main' into brennanb2025/chat-com…
brennanb2025 Oct 6, 2026
1d6da76
Merge remote-tracking branch 'origin/main' into brennanb2025/chat-com…
brennanb2025 Oct 6, 2026
a967125
test(native-chat): a Stop behind a typed /compact keeps the taken-bac…
brennanb2025 Oct 6, 2026
655749b
Merge commit 'a967125c97b523ff6bc429d624c56003655b0476' into brennanb…
brennanb2025 Oct 6, 2026
4f73f5e
Merge remote-tracking branch 'origin/main' into brennanb2025/chat-com…
brennanb2025 Oct 7, 2026
ca5705e
Merge remote-tracking branch 'origin/main' into brennanb2025/chat-com…
brennanb2025 Oct 7, 2026
27bc442
Merge remote-tracking branch 'origin/main' into brennanb2025/chat-com…
brennanb2025 Oct 7, 2026
fc7a251
fix(native-chat): a command refused while the queue is about to send …
brennanb2025 Oct 7, 2026
09a24c6
Merge origin/main, keeping recovered messages and commands in one queue
brennanb2025 Oct 7, 2026
c79a243
Update held command fixture for the merged queue model
brennanb2025 Oct 7, 2026
a4c3aa0
Merge remote-tracking branch 'origin/main' into brennanb2025/chat-com…
brennanb2025 Oct 7, 2026
b6cbc76
Restart one host at a time in the recovery ordering test
brennanb2025 Oct 7, 2026
47f4087
Merge origin/main and preserve ordered chat commands with the desktop…
brennanb2025 Oct 8, 2026
b15c597
Merge origin/main and preserve chat command capabilities
brennanb2025 Oct 8, 2026
a3114b7
Merge origin/main and preserve queued chat commands
brennanb2025 Oct 8, 2026
62bde78
fix: preserve person queue reserve for chat commands
brennanb2025 Oct 8, 2026
28c3ea3
Merge final command queue changes into clear queue branch
brennanb2025 Oct 8, 2026
43ff3c1
Merge main after command queue changes landed
brennanb2025 Oct 8, 2026
03a8e70
Preserve queued clear behavior on current host lifecycle
brennanb2025 Oct 8, 2026
252d4ab
Commit clears and queued message transfers atomically
brennanb2025 Oct 8, 2026
2e98f49
Sync current main after clear review fixes
brennanb2025 Oct 8, 2026
88edf9c
Say a /clear waits on background tasks when only they hold it
brennanb2025 Oct 9, 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
14 changes: 14 additions & 0 deletions config/scripts/vitest-sqlite-runtime-files.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,19 @@ export const SQLITE_RUNTIME_INCLUDE = [
'src/main/native-chat/agent-session-wire/structured-agent-session-provider-restore.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-provider-started-catalog.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-provider-started.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-queued-card-holds.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-queued-clear.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-queued-clear-atomic.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-queued-command.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-queued-compact.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-queued-gate.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-queued-messages.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-queued-newer-orca.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-queued-pause-lift.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-queued-published-bytes.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-queued-quit.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-queued-repeat-press.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-queued-withdrawn-draft.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-read-restore.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-readable-restorer.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-reasoning-sweep.test.ts',
Expand Down Expand Up @@ -158,7 +170,9 @@ export const SQLITE_RUNTIME_INCLUDE = [
'src/main/native-chat/agent-session-wire/structured-agent-session-view-start-after-failed-start.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-wedged-profile-migration.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-wire-admission.test.ts',
'src/main/native-chat/agent-session-wire/structured-conversation-command-child-work.test.ts',
'src/main/native-chat/agent-session-wire/structured-conversation-command.test.ts',
'src/main/native-chat/agent-session-wire/structured-conversation-compaction.test.ts',
'src/main/opencode/opencode-acp-stored-messages.test.ts',
'src/main/orcad/orcad-automations.test.ts',
'src/main/orcad/orcad-local-serve-selection.test.ts',
Expand Down
10 changes: 10 additions & 0 deletions mobile/src/session/MobileNativeChatQueuedMessages.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,16 @@ describe('MobileNativeChatQueuedMessages', () => {
expect(nodeTypes(rows[1]!)).toContain('CornerDownRight')
})

it('a /clear the queue is about to run offers Delete but no Send', async () => {
const mounted = await mount({
cards: [card({ messageId: 'clear', text: '/clear', command: true, runsOnItsOwn: true })]
})
expect(texts(mounted).filter((text) => text === 'Send' || text === 'Steer')).toEqual([])
expect(
mounted.root.findAllByProps({ accessibilityLabel: 'Delete this queued message' })
).toHaveLength(1)
})

it('a command card never steers: Send only while the agent is idle, and no menu', async () => {
const onSend = vi.fn(async () => true)
const mounted = await mount({
Expand Down
5 changes: 3 additions & 2 deletions mobile/src/session/MobileNativeChatQueuedMessages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,9 @@ export function MobileNativeChatQueuedMessages({
</Text>
) : null}
</View>
{/* A command never steers: its Send shows only while the agent is idle. */}
{card.waitsForAgent ? null : (
{/* A command never steers: its Send shows only while the agent is idle, and never on
a /clear the queue is about to run. */}
{card.waitsForAgent || card.runsOnItsOwn ? null : (
<Pressable
accessibilityRole="button"
accessibilityState={{ disabled: busy || steerHeld }}
Expand Down
2 changes: 2 additions & 0 deletions mobile/src/session/mobile-native-chat-eligibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export type MobileNativeChatTab = {
launchDraftCreatedAt?: number
sessionId?: string | null
agent?: string | null
/** The conversation a /clear replaced with this one. */
replacesSessionId?: string
}

/** Resolve a session tab to the transcript identity native chat needs, or
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ const HOST_SUPPORT = {
questionAnswers: true,
queuedMessages: true,
queuedCommands: false,
queuedClear: false,
statusFeed: true,
quietRepeatedStop: true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
import {
AGENT_SESSION_PROMPT_CANCEL_RUNTIME_CAPABILITY,
AGENT_SESSION_QUESTION_ANSWERS_RUNTIME_CAPABILITY,
AGENT_SESSION_QUEUED_CLEAR_RUNTIME_CAPABILITY,
AGENT_SESSION_QUEUED_COMMANDS_RUNTIME_CAPABILITY,
AGENT_SESSION_QUEUED_MESSAGES_RUNTIME_CAPABILITY,
AGENT_SESSION_STATUS_FEED_RUNTIME_CAPABILITY,
Expand All @@ -16,6 +17,7 @@ describe('structuredAgentSessionHostSupport', () => {
questionAnswers: false,
queuedMessages: false,
queuedCommands: false,
queuedClear: false,
quietRepeatedStop: false,
statusFeed: false
}
Expand All @@ -38,5 +40,8 @@ describe('structuredAgentSessionHostSupport', () => {
expect(
structuredAgentSessionHostSupport([AGENT_SESSION_STATUS_FEED_RUNTIME_CAPABILITY])
).toEqual({ ...none, statusFeed: true })
expect(
structuredAgentSessionHostSupport([AGENT_SESSION_QUEUED_CLEAR_RUNTIME_CAPABILITY])
).toEqual({ ...none, queuedClear: true })
})
})
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
AGENT_SESSION_PROMPT_CANCEL_RUNTIME_CAPABILITY,
AGENT_SESSION_QUEUED_CLEAR_RUNTIME_CAPABILITY,
AGENT_SESSION_QUESTION_ANSWERS_RUNTIME_CAPABILITY,
AGENT_SESSION_QUEUED_COMMANDS_RUNTIME_CAPABILITY,
AGENT_SESSION_QUEUED_MESSAGES_RUNTIME_CAPABILITY,
Expand All @@ -15,6 +16,8 @@ export type StructuredAgentSessionHostSupport = {
queuedMessages: boolean
/** A /compact sent while the agent works waits as a card; an older host refuses it. */
queuedCommands: boolean
/** A /clear sent while the agent works waits as a card the host runs; an older host refuses it. */
queuedClear: boolean
/** The host publishes every session's status on one stream. A host from before phones could
* read it refuses the call, which reads the same as its absence. */
statusFeed: boolean
Expand All @@ -30,6 +33,7 @@ export function structuredAgentSessionHostSupport(
questionAnswers: capabilities.includes(AGENT_SESSION_QUESTION_ANSWERS_RUNTIME_CAPABILITY),
queuedMessages: capabilities.includes(AGENT_SESSION_QUEUED_MESSAGES_RUNTIME_CAPABILITY),
queuedCommands: capabilities.includes(AGENT_SESSION_QUEUED_COMMANDS_RUNTIME_CAPABILITY),
queuedClear: capabilities.includes(AGENT_SESSION_QUEUED_CLEAR_RUNTIME_CAPABILITY),
statusFeed: capabilities.includes(AGENT_SESSION_STATUS_FEED_RUNTIME_CAPABILITY),
quietRepeatedStop: capabilities.includes(AGENT_SESSION_REPEATED_STOP_RUNTIME_CAPABILITY)
}
Expand Down
25 changes: 24 additions & 1 deletion mobile/src/session/mobile-structured-composer-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ const asyncStorage = vi.hoisted(() => ({

vi.mock('@react-native-async-storage/async-storage', () => ({ default: asyncStorage }))

import { dispatchMobileStructuredCommand } from './mobile-structured-composer-command'
import {
dispatchMobileStructuredCommand,
mobileStructuredCommandWaitsInLine
} from './mobile-structured-composer-command'

function setup() {
const sendRequest = vi.fn<
Expand Down Expand Up @@ -204,3 +207,23 @@ describe('mobile structured conversation commands', () => {
expect(sendRequest).not.toHaveBeenCalled()
})
})

describe('which commands wait in line on this host', () => {
const host = { commandsWait: true, clearWaits: false, promptsUnanswerableHere: false }

it('each command by its own capability: a host that holds only /compact still refuses /clear', () => {
expect(mobileStructuredCommandWaitsInLine('compact', host)).toBe(true)
expect(mobileStructuredCommandWaitsInLine('clear', host)).toBe(false)
expect(mobileStructuredCommandWaitsInLine('clear', { ...host, clearWaits: true })).toBe(true)
})

it('never behind a prompt nothing here can answer', () => {
expect(
mobileStructuredCommandWaitsInLine('clear', {
commandsWait: true,
clearWaits: true,
promptsUnanswerableHere: true
})
).toBe(false)
})
})
12 changes: 12 additions & 0 deletions mobile/src/session/mobile-structured-composer-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ function busyCommandText(
)
}

/** Whether the host holds this command as a card while the agent works: /compact and /clear each
* need their own capability (and the queue), and a prompt nothing here can answer would hold the
* card forever. */
export function mobileStructuredCommandWaitsInLine(
command: AgentSessionConversationCommand,
host: { commandsWait: boolean; clearWaits: boolean; promptsUnanswerableHere: boolean }
): boolean {
return (
(command === 'compact' ? host.commandsWait : host.clearWaits) && !host.promptsUnanswerableHere
)
}

export async function dispatchMobileStructuredCommand(input: {
text: string
hasAttachments: boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,11 +254,11 @@ describe('what a structured refusal says on the phone', () => {
clientOperationId: `1900000000000-${'e'.repeat(32)}`
})

// The phone hands a refused send's text back, so it says where that text is.
expect(result).toEqual({
status: 'refused',
code: 'agent_session_operation_invalid',
message:
'This conversation has been cleared. Your message was not sent. Open the current conversation to continue.'
message: "The chat was cleared before your message went out. It's back in the composer."
})
})

Expand Down
91 changes: 91 additions & 0 deletions mobile/src/session/mobile-structured-queued-message-cards.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,97 @@ describe('mobileQueuedMessageCards', () => {
).toMatchObject({ command: true, waitsForAgent: true })
})

it('a /clear next in line that nothing holds offers no Send: the queue is about to run it', () => {
const clear = (overrides: Partial<AgentSessionQueuedMessage> = {}) =>
draft({
messageId: 'c',
body: {
kind: 'message',
role: 'user',
blocks: [{ type: 'text', text: '/clear' }],
command: { name: 'clear' }
},
...overrides
})
const idle = { pendingPrompt: false }
expect(mobileQueuedMessageCards([clear()], [], idle)[0]).toMatchObject({ runsOnItsOwn: true })
// Anything the person must act on keeps its Send, as does a /clear behind another card.
for (const [cards, facts] of [
[[clear()], { ...idle, queuePaused: true }],
[[clear()], { pendingPrompt: true }],
[[clear({ paused: true, pausedReason: QUEUED_MESSAGE_PAUSED_SEND_FAILED })], idle],
[[draft({ messageId: 'a' }), clear()], idle]
] as const) {
expect(mobileQueuedMessageCards(cards, [], facts).some((card) => card.runsOnItsOwn)).toBe(
false
)
}
})

it('a /clear next in line that only background tasks hold says so; nothing else does', () => {
const clear = draft({
messageId: 'c',
body: {
kind: 'message',
role: 'user',
blocks: [{ type: 'text', text: '/clear' }],
command: { name: 'clear' }
}
})
const tasks = { pendingPrompt: false, backgroundTasksRunning: true }
const [first, second] = mobileQueuedMessageCards(
[clear, draft({ messageId: 'm', position: 2 })],
[],
tasks
)
expect(first).toMatchObject({
caption: 'Waiting for background tasks to finish',
runsOnItsOwn: true
})
expect(second?.caption).toBeNull()
// Behind a turn or an answer, that is what it waits on; behind another card, its turn.
expect(
mobileQueuedMessageCards([clear], [], { ...tasks, agentWorking: true })[0]?.caption
).toBeNull()
expect(
mobileQueuedMessageCards([clear], [], { ...tasks, pendingPrompt: true })[0]?.caption
).toBe('Waiting for your answer')
expect(
mobileQueuedMessageCards([draft({ messageId: 'm' }), { ...clear, position: 2 }], [], tasks)[1]
?.caption
).toBeNull()
})

it('skips a failed card ahead of clear, respecting returned and queue-pause barriers', () => {
const clear = draft({
messageId: 'clear',
position: 2,
body: {
kind: 'message',
role: 'user',
blocks: [{ type: 'text', text: '/clear' }],
command: { name: 'clear' }
}
})
const held = draft({
messageId: 'held',
paused: true,
pausedReason: QUEUED_MESSAGE_PAUSED_SEND_FAILED
})
expect(
mobileQueuedMessageCards([held, clear], [], { pendingPrompt: false })[1]?.runsOnItsOwn
).toBe(true)
expect(
mobileQueuedMessageCards([{ ...held, state: 'returned' }, clear], [], {
pendingPrompt: false
})[1]?.runsOnItsOwn
).toBeUndefined()
expect(
mobileQueuedMessageCards([held, clear], [], { pendingPrompt: false, queuePaused: true })[1]
?.runsOnItsOwn
).toBeUndefined()
})

it("a send-failed command card's caption names Send only when Send is there", () => {
const failed = draft({
messageId: 'c',
Expand Down
Loading
Loading