Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
0784bc3
fix(native-chat): a read whose history will not open is refused with …
brennanb2025 Sep 28, 2026
1122921
refactor(native-chat): the idle sweep's stop of a hung start carries …
brennanb2025 Sep 28, 2026
101d4e6
fix(native-chat): a refusal the host throws is worded from its data, …
brennanb2025 Sep 28, 2026
5c9b4cb
fix(native-chat): an unreadable history is worded from its refusal, a…
brennanb2025 Sep 28, 2026
91cda82
fix(native-chat): a rejected message is worded from its stored fact
brennanb2025 Sep 28, 2026
2ec46a9
fix(native-chat): each message that did not go through says why on it…
brennanb2025 Sep 28, 2026
8985ffc
fix(native-chat): say each chat failure's words where its own control…
brennanb2025 Sep 28, 2026
26f0c99
fix(native-chat): a rejected message offers Retry only once the queue…
brennanb2025 Sep 28, 2026
d29a373
fix(native-chat): a chat whose history will not open logs once, not o…
brennanb2025 Sep 28, 2026
55f2513
fix(native-chat): a message's own Retry is its resend step, so its no…
brennanb2025 Sep 28, 2026
5298d8d
fix(native-chat): a message's Retry sends only that message, never th…
brennanb2025 Sep 28, 2026
9e131b2
fix(native-chat): a not-signed-in failure beside Retry still says to …
brennanb2025 Sep 28, 2026
0eb4c54
test(native-chat): a rejected message's hidden Retry only avoids wait…
brennanb2025 Sep 28, 2026
8ac3d2d
fix(native-chat): every Retry beside a notice leaves out the retry st…
brennanb2025 Sep 28, 2026
f3b8d2f
test(native-chat): read the sent message id without a type assertion
brennanb2025 Sep 28, 2026
bac487a
fix(native-chat): a rejected message is worded from the journal's own…
brennanb2025 Sep 28, 2026
8ff7387
fix(native-chat): a message rejected before a restart retries under a…
brennanb2025 Sep 28, 2026
4fd67d7
fix(native-chat): a rejected message older than the loaded history ke…
brennanb2025 Sep 28, 2026
5c1647d
test(native-chat): the chat pane words a rejected message from its lo…
brennanb2025 Sep 28, 2026
0b9a7b0
fix(native-chat): a chat whose history won't load says why in one line
brennanb2025 Sep 28, 2026
e3f23a8
fix(native-chat): a message a failed start rejected says only that it…
brennanb2025 Sep 28, 2026
c2eb689
fix(native-chat): a chat whose history keeps failing to open retries …
brennanb2025 Sep 28, 2026
708e1cc
test(native-chat): the pane harness types its journal rows without a …
brennanb2025 Sep 28, 2026
5e41e50
test(native-chat): the admission test passes no start-failure rows to…
brennanb2025 Sep 28, 2026
f7892dc
test(native-chat): import the journal types once
brennanb2025 Sep 28, 2026
4c03975
fix(native-chat): a remote chat reads again as soon as its host is back
brennanb2025 Sep 28, 2026
55fda33
refactor(native-chat): build each failure sentence from whole pieces
brennanb2025 Sep 28, 2026
6ab9b24
fix(native-chat): translate the failure sentences desktop notices show
brennanb2025 Sep 28, 2026
87c51f2
fix(native-chat): the history pane says only that Orca keeps trying
brennanb2025 Sep 28, 2026
c4d7d93
test(native-chat): type the failure pieces a refusal notice shares
brennanb2025 Sep 28, 2026
dd7bb0c
test(native-chat): the Chinese failure words use no Japanese-only cha…
brennanb2025 Sep 28, 2026
32c9c2d
fix(native-chat): every history pane that says it didn't load says on…
brennanb2025 Sep 28, 2026
1154ce1
Merge branch 'brennanb2025/chat-refusal-copy-desktop' into brennanb20…
brennanb2025 Sep 28, 2026
e645141
test(native-chat): a provider's words with nesting and markup stay as…
brennanb2025 Sep 28, 2026
fca1e46
fix(native-chat): French and Spanish say a withdrawn message was with…
brennanb2025 Sep 28, 2026
527dcda
Merge remote-tracking branch 'origin/main' into brennanb2025/chat-fai…
brennanb2025 Sep 28, 2026
81e3707
fix(native-chat): Japanese and Chinese notices run their sentences on…
brennanb2025 Sep 28, 2026
d387643
fix(native-chat): a failed /clear or /compact says why in the app's l…
brennanb2025 Sep 28, 2026
803184a
fix(native-chat): Spanish says a rate limit, and Korean says a withdr…
brennanb2025 Sep 28, 2026
dd76637
refactor(native-chat): one rule says which words already say the hist…
brennanb2025 Sep 28, 2026
02f8549
fix(native-chat): an image size limit says its unit the way the reade…
brennanb2025 Sep 28, 2026
0b16301
test(native-chat): the option picker's i18n stand-in knows the reader…
brennanb2025 Sep 28, 2026
0529054
fix(native-chat): a sentence a language pack left in English keeps it…
brennanb2025 Sep 28, 2026
2366e87
fix(native-chat): a failed /clear this build only partly understands …
brennanb2025 Sep 28, 2026
b663854
fix(native-chat): a failure fact this build reads only in part shows …
brennanb2025 Sep 30, 2026
1caae0b
Merge remote-tracking branch 'origin/main' into brennanb2025/chat-fai…
brennanb2025 Sep 30, 2026
b04f235
fix(native-chat): a failed /compact names /compact as its next step o…
brennanb2025 Sep 30, 2026
8471f0e
fix(native-chat): a /compact whose start failed no longer says the op…
brennanb2025 Sep 30, 2026
6e522e7
Merge remote-tracking branch 'origin/main' into brennanb2025/chat-fai…
brennanb2025 Sep 30, 2026
29a3832
fix(native-chat): keep the message outbox within its line limit after…
brennanb2025 Sep 30, 2026
f027f19
fix(native-chat): a command's own reply is kept when its start moved …
brennanb2025 Sep 30, 2026
10b8f23
fix(native-chat): a returned queued message shows the host's sentence…
brennanb2025 Sep 30, 2026
e401c22
test(native-chat): a command reply after any fence move, for /clear a…
brennanb2025 Sep 30, 2026
7328975
Merge remote-tracking branch 'origin/main' into brennanb2025/chat-fai…
brennanb2025 Sep 30, 2026
215914f
Merge commit '8b01590847d' into brennanb2025/chat-failure-words-finish
brennanb2025 Sep 30, 2026
82c25a9
fix(native-chat): only the reply a pane still waits on outlives a fen…
brennanb2025 Sep 30, 2026
b75edf8
fix(native-chat): a newer-Orca history error keeps a whole retry line…
brennanb2025 Sep 30, 2026
bf612f2
Merge remote-tracking branch 'origin/main' into brennanb2025/chat-fai…
brennanb2025 Sep 30, 2026
f7771bb
Merge remote-tracking branch 'origin/main' into brennanb2025/chat-fai…
brennanb2025 Sep 30, 2026
63caa6e
fix(native-chat): a command reply this build can't place shows the ho…
brennanb2025 Sep 30, 2026
30bcac2
Merge remote-tracking branch 'origin/main' into brennanb2025/chat-fai…
brennanb2025 Sep 30, 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
22 changes: 22 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 @@ -239,6 +239,28 @@ describe('mobileQueuedMessageCards', () => {
expect(card?.caption).toBe('Words for a kind a newer host added.')
})

it("shows the host's sentence for a fact this build cannot read all of, as the desktop card", () => {
const reason = "Claude couldn't start. Start a new chat to continue."
// As a newer host sends it: a known code with a reason this build doesn't know.
const newer = {
kind: 'startFailed',
refusal: { code: 'agent_session_conflict', details: { reason: 'newerReason' } }
}
const [card] = mobileQueuedMessageCards(
[
draft({
messageId: 'a',
state: 'returned',
returnedReason: reason,
returnedRejection: newer
})
],
[],
{ pendingPrompt: false }
)
expect(card?.caption).toBe(reason)
})

it('maps the send-failed pause marker to English and an unknown marker to a plain pause', () => {
const cards = mobileQueuedMessageCards(
[
Expand Down
8 changes: 4 additions & 4 deletions mobile/src/session/mobile-structured-queued-message-cards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// The wire carries no hold copy on purpose: the caption is derived here from the
// draft's own state plus the live facts the client already holds.

import { readAgentSessionFailureFact } from '../../../src/shared/agent-session-failure'
import { readWholeAgentSessionFailureFact } from '../../../src/shared/agent-session-failure'
import type { AgentJournalSubmission } from '../../../src/shared/agent-session-journal-types'
import { agentSessionWriteNoticeEnglish } from '../../../src/shared/agent-session-refusal-notice'
import { dispatchWasWithdrawn } from '../../../src/shared/structured-agent-session-dispatch-rejection'
Expand Down Expand Up @@ -37,13 +37,13 @@ function returnedCaption(
if (dispatchWasWithdrawn({ dispatchState: 'rejected', reason, rejection })) {
return 'Stopped before it was sent'
}
// Worded as the desktop card words it: the fact decides, the reason is the fallback, and the
// card's own Send is the retry, so the words leave out sending again.
// Worded as the desktop card words it: a fact read whole decides, the host's reason is the
// fallback, and the card's own Send is the retry, so the words leave out sending again.
return agentSessionWriteNoticeEnglish(
structuredAgentSessionAttemptFailureParts(
{ kind: 'rejected', reason },
{ retryControl: true },
readAgentSessionFailureFact(rejection)
readWholeAgentSessionFailureFact(rejection)
)
)
}
Expand Down
30 changes: 21 additions & 9 deletions src/renderer/src/components/native-chat/NativeChatEmptyState.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,33 @@
import { MessageSquare, TriangleAlert } from 'lucide-react'
import { translate } from '@/i18n/i18n'
import { formatAgentTypeLabel } from '@/lib/agent-status'
import { NATIVE_CHAT_EMPTY_STATE_COPY } from '../../../../shared/native-chat-empty-state'
import {
NATIVE_CHAT_EMPTY_STATE_COPY,
NATIVE_CHAT_KEEPS_TRYING_COPY
} from '../../../../shared/native-chat-empty-state'
import type { NativeChatSession } from '../../../../shared/native-chat-types'

export function NativeChatEmptyState({
kind,
message,
headline,
headlineSaysUnread = false,
agent,
retrying = false
}: {
kind: 'loading' | 'empty' | 'error' | 'not-agent'
message?: string
/** The chat's own sentence for the failure, said once: it takes the generic title's place. */
headline?: string
/** The headline already says the history didn't load, so the retrying line says only that
* Orca keeps trying. */
headlineSaysUnread?: boolean
agent?: NativeChatSession['agent']
/** The read retries on its own (structured chat), so the error says so instead of pointing
* back to the terminal. */
retrying?: boolean
}): React.JSX.Element {
const copy = emptyStateCopy(kind, { message, headline }, agent, retrying)
const copy = emptyStateCopy(kind, { message, headline, headlineSaysUnread }, agent, retrying)
return (
<div className="flex h-full w-full flex-col items-center justify-center gap-3 p-6 text-center">
<div
Expand All @@ -46,7 +53,7 @@ export function NativeChatEmptyState({

function emptyStateCopy(
kind: 'loading' | 'empty' | 'error' | 'not-agent',
words: { message?: string; headline?: string },
words: { message?: string; headline?: string; headlineSaysUnread?: boolean },
agent?: NativeChatSession['agent'],
retrying = false
): { title: string; subtitle: string | null } {
Expand All @@ -63,12 +70,17 @@ function emptyStateCopy(
)
}
case 'error': {
const retryingLine = retrying
? translate(
'components.native-chat.state.error.retryingSubtitle',
NATIVE_CHAT_EMPTY_STATE_COPY.retryingError.subtitle
)
: null
const retryingLine = !retrying
? null
: words.headline && words.headlineSaysUnread
? translate(
'components.native-chat.state.error.keepsTrying',
NATIVE_CHAT_KEEPS_TRYING_COPY
)
: translate(
'components.native-chat.state.error.retryingSubtitle',
NATIVE_CHAT_EMPTY_STATE_COPY.retryingError.subtitle
)
if (words.headline) {
return { title: words.headline, subtitle: retryingLine }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { translate } from '@/i18n/i18n'
import type { StructuredAgentSessionLaunchLifecycle } from '@/lib/structured-agent-session-launch'
import { agentSessionRefusalCauseParts } from '../../../../shared/agent-session-refusal-notice'
import type { AgentSessionWriteRefusal } from '../../../../shared/agent-session-write-failure'
import { joinSentences } from '../../../../shared/sentence-joining'
import { agentSessionWriteNoticeText } from './agent-session-write-notice-text'

export function NativeChatLaunchRetry({
Expand Down Expand Up @@ -41,8 +42,7 @@ export function NativeChatLaunchRetry({
return (
<div className="mx-auto flex w-full max-w-4xl items-center justify-between gap-3 px-4 py-1 text-xs text-destructive">
<span className="min-w-0 break-words">
{message}
{cause ? ` ${cause}` : null}
{cause ? joinSentences([message, cause]) : message}
</span>
<Button type="button" variant="ghost" size="xs" onClick={onRetry}>
<RotateCcw className="size-3" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { ShortcutKeyCombo } from '@/components/ShortcutKeyCombo'
import { translate } from '@/i18n/i18n'
import { structuredAgentSessionAttemptFailureParts } from '../../../../shared/structured-agent-session-send-disposition'
import { classifyDispatchRejection } from '../../../../shared/structured-agent-session-dispatch-rejection'
import { readAgentSessionFailureFact } from '../../../../shared/agent-session-failure'
import { readWholeAgentSessionFailureFact } from '../../../../shared/agent-session-failure'
import { agentSessionWriteNoticeText } from './agent-session-write-notice-text'
import { QUEUED_MESSAGE_PAUSED_SEND_FAILED } from '../../../../shared/agent-session-wire'
import { isMacPlatform } from './native-chat-shortcut'
Expand Down Expand Up @@ -47,7 +47,7 @@ export function queuedMessageCardCaption(card: QueuedMessageCard): string | null
{ kind: 'rejected', reason },
// The card's own Send is the retry, so the words leave out sending again.
{ retryControl: true },
readAgentSessionFailureFact(card.returnedRejection)
readWholeAgentSessionFailureFact(card.returnedRejection)
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,25 @@ describe('NativeChatQueuedMessageList', () => {
expect(row.textContent).not.toContain('Your message was not sent.')
})

it("shows the host's sentence for a fact this build cannot read all of", () => {
const reason = "Claude couldn't start. Start a new chat to continue."
renderList(
controller([
card({
messageId: 'newer',
state: 'returned',
hold: 'returned',
returnedReason: reason,
// As a newer host sends it: a known code with a reason this build doesn't know.
returnedRejection: JSON.parse(
'{ "kind": "startFailed", "refusal": { "code": "agent_session_conflict", "details": { "reason": "newerReason" } } }'
)
})
])
)
expect(screen.getByRole('listitem').textContent).toContain(reason)
})

it('the words leave out sending again: the card offers its own Send', () => {
renderList(
controller([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ function renderPane(): void {
)
}

function journalRefusal(reason: 'journalCorrupt' | 'journalUnavailable') {
function journalRefusal(
reason: 'journalCorrupt' | 'journalUnavailable' | 'journalWrittenByNewerOrca'
) {
return { code: 'agent_session_journal_unreadable', details: { reason } } as const
}

Expand Down Expand Up @@ -80,10 +82,38 @@ it("names a history that couldn't open right now once, and that the pane keeps t

expect(screen.getAllByText("Orca couldn't open this chat's history right now.")).toHaveLength(1)
expect(screen.queryByText('Could not load conversation')).toBeNull()
// The title already says the history didn't open, so the line under it says only the retrying.
expect(screen.getByText('Orca keeps trying to load it.')).toBeTruthy()
expect(screen.queryByText(/could not be read/)).toBeNull()
expect(screen.queryByText(/Try again/)).toBeNull()
})

it("says only that it keeps trying under a code's own words that the history didn't load", () => {
mocks.status = 'error'
mocks.readRefusal = {
code: 'agent_session_checkpoint_stale',
details: { reason: 'fenceStale' }
} as const
mocks.messages = []

renderPane()

expect(screen.getAllByText("This chat's history couldn't be loaded.")).toHaveLength(1)
expect(screen.getByText('Orca keeps trying to load it.')).toBeTruthy()
expect(screen.queryByText(/could not be read/)).toBeNull()
})

it('keeps the whole retrying line under a newer Orca\'s words, which name no one chat for "it"', () => {
mocks.status = 'error'
mocks.readRefusal = journalRefusal('journalWrittenByNewerOrca')
mocks.messages = []

renderPane()

expect(screen.getByText(/^Chats were saved by a newer Orca\./)).toBeTruthy()
expect(
screen.getByText('The transcript could not be read. Orca keeps trying to load it.')
).toBeTruthy()
expect(screen.queryByText(/Try again/)).toBeNull()
})

it('says only that it is reconnecting, not as an error, when a failure names nothing', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,9 @@ export function NativeChatStructuredSession(
<NativeChatEmptyState
kind="error"
retrying={!readFailure?.final}
{...(readFailure?.named ? { headline: readFailure.text } : {})}
{...(readFailure?.named
? { headline: readFailure.text, headlineSaysUnread: readFailure.saysUnread }
: {})}
/>
) : viewState.kind === 'empty' ? (
<NativeChatEmptyState kind="empty" agent={props.agent} />
Expand Down
Loading
Loading