Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
311b5ba
refactor(native-chat): share the words for a recorded, rejected message
brennanb2025 Oct 7, 2026
9bd0254
fix(native-chat): place a not-sent row at its journal position in the…
brennanb2025 Oct 7, 2026
e951642
fix(mobile): match a send's echo past rows the host shows as not sent
brennanb2025 Oct 7, 2026
1032f99
feat(mobile): draw a recorded, rejected message in place, as the desk…
brennanb2025 Oct 7, 2026
cbbab97
fix(mobile): settle a structured send by its own row, and name a reco…
brennanb2025 Oct 7, 2026
85d9909
fix(mobile): say another agent's not-sent message was not sent
brennanb2025 Oct 7, 2026
057fe48
fix(native-chat): place not-sent rows after the stop rows, without a …
brennanb2025 Oct 7, 2026
84b74de
docs(native-chat): say when the projection leaves rejected rows out
brennanb2025 Oct 7, 2026
d46439d
fix(mobile): settle a structured send from the journal's record of it
brennanb2025 Oct 7, 2026
3bad603
fix(mobile): keep a photo bubble until it binds to its own drawn row
brennanb2025 Oct 7, 2026
58bf244
test(mobile): check the controller hands the journal's submissions to…
brennanb2025 Oct 7, 2026
538c201
test(mobile): type-check the photo-race test's hook reads
brennanb2025 Oct 7, 2026
8869cfc
test(mobile): keep the round-3 tests inside the quality gates
brennanb2025 Oct 7, 2026
b0185e2
test(native-chat): name the projection test's row helper for what it …
brennanb2025 Oct 7, 2026
c4328cd
Merge origin/main into phone recorded rejection change
brennanb2025 Oct 7, 2026
a330110
Merge main into phone recorded rejection change
brennanb2025 Oct 8, 2026
8ed8886
Clarify unsent chat messages and withhold technical diagnostics
brennanb2025 Oct 8, 2026
4ef5afa
Keep technical diagnostics out of chat failure instructions
brennanb2025 Oct 8, 2026
5822e86
Keep final agent errors and older rejection copy readable
brennanb2025 Oct 8, 2026
11fa1e2
Merge main and preserve recorded phone send failures
brennanb2025 Oct 8, 2026
37920c0
Expect safe transport retry copy on phone
brennanb2025 Oct 8, 2026
91751ae
Expect safe transport retry copy on desktop
brennanb2025 Oct 8, 2026
3c06915
Align failure assertions and SQLite test routing
brennanb2025 Oct 8, 2026
99ecd04
Avoid inferring idle from a refused response Stop
brennanb2025 Oct 8, 2026
c15c79b
Align runtime Stop assertion with the scoped refusal copy
brennanb2025 Oct 8, 2026
d9ac767
Merge main while preserving recorded phone rejections and shared fail…
brennanb2025 Oct 8, 2026
1c3ed1f
Keep failure lines plain after the main sync
brennanb2025 Oct 9, 2026
e416d27
Expect hidden technical detail in the startup sign-in row test
brennanb2025 Oct 9, 2026
e940ff7
Keep a failed agent turn's reason in Details and its usage-limit lead
brennanb2025 Oct 9, 2026
ff35ce3
Keep the ACP agent id option clear of main's neighbouring lane changes
brennanb2025 Oct 9, 2026
e94cf2d
Merge main while keeping failure lines plain and named
brennanb2025 Oct 9, 2026
a7a110f
Keep setting names and model ids with digits readable in failure lines
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
Original file line number Diff line number Diff line change
Expand Up @@ -28,47 +28,60 @@ describe('startup sign-in rows from host to renderer', () => {
],
['OMP', 'Connectez-vous à OMP.']
])(
'translates an actual %s startup row with its diagnostic and resend advice',
'translates an actual %s startup row with plain detail and resend advice, hiding technical detail',
async (agentName, guidance) => {
await i18n.changeLanguage('fr')
const detail = 'Provider diagnostic: {{agent}} must remain literal.'
const startup = structuredAgentSessionStartFailure(
{
failure: {
kind: 'notSignedIn',
account: 'system',
detail: { text: detail, audience: 'person' }
}
},
{ agentName }
)
const mutation = structuredAgentSessionStartFailureRow('generation', startup)
if (mutation.kind !== 'item' || mutation.body.kind !== 'status') {
throw new Error('Expected the startup writer to produce a status row')
}
expect(mutation.body.text).toContain('Then send your message again.')
const [message] = projectStructuredItemsToNativeChat([
{
itemId: agentJournalItemKey(mutation.identity),
sequence: 1,
revision: 1,
observedAt: 1,
body: mutation.body,
turnScope: mutation.turnScope
}
])
render(
createElement(MessageRow, {
message,
agentName,
expandSignal: false,
onScrollMessageToTop: vi.fn()
})
)
renderStartupRow(agentName, 'Sign-in note: {{agent}} must remain literal.')
expect(
screen.getByText(`${guidance} ${detail} Puis envoyez à nouveau votre message.`)
screen.getByText(
`${guidance} Sign-in note: {{agent}} must remain literal. Puis envoyez à nouveau votre message.`
)
).toBeInTheDocument()
expect(screen.queryByText(mutation.body.text)).toBeNull()
cleanup()

// Technical detail stays off the row; the agent-named sign-in step and resend advice remain.
renderStartupRow(agentName, 'Provider diagnostic: {{agent}} must remain literal.')
expect(
screen.getByText(`${guidance} Puis envoyez à nouveau votre message.`)
).toBeInTheDocument()
expect(screen.queryByText(/provider|diagnostic/i)).toBeNull()
}
)
})

function renderStartupRow(agentName, detail) {
const startup = structuredAgentSessionStartFailure(
{
failure: {
kind: 'notSignedIn',
account: 'system',
detail: { text: detail, audience: 'person' }
}
},
{ agentName }
)
const mutation = structuredAgentSessionStartFailureRow('generation', startup)
if (mutation.kind !== 'item' || mutation.body.kind !== 'status') {
throw new Error('Expected the startup writer to produce a status row')
}
expect(mutation.body.text).toContain('Then send your message again.')
const [message] = projectStructuredItemsToNativeChat([
{
itemId: agentJournalItemKey(mutation.identity),
sequence: 1,
revision: 1,
observedAt: 1,
body: mutation.body,
turnScope: mutation.turnScope
}
])
render(
createElement(MessageRow, {
message,
agentName,
expandSignal: false,
onScrollMessageToTop: vi.fn()
})
)
expect(screen.queryByText(mutation.body.text)).toBeNull()
}
1 change: 1 addition & 0 deletions config/scripts/vitest-sqlite-runtime-files.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ export const SQLITE_RUNTIME_INCLUDE = [
'src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-stopping.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-status-feed.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-status-reentry.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-stop-failure-copy.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-stop-note-opened-turn.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-stop-note-withdrawn-send.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-stopping-journal.test.ts',
Expand Down
53 changes: 53 additions & 0 deletions mobile/src/session/MobileNativeChatMessage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ describe('MobileNativeChatMessage', () => {
} | null
onToggleTurn?: () => void
reasoningIsLive?: boolean
unsentNotice?: string
} = {}
): ReactTestRenderer {
act(() => {
Expand Down Expand Up @@ -168,6 +169,28 @@ describe('MobileNativeChatMessage', () => {
expect(rows.some((row) => row.props.style.includes(styles.rowUser))).toBe(false)
})

// As on the desktop: an orchestration task the host recorded and rejected reads as not sent too.
it("says under another agent's message the host never delivered that it was not sent", () => {
const tree = render(
{
...userMessage([{ type: 'text', text: 'Fix the parser.' }]),
unsent: true,
from: {
kind: 'agent',
senders: [
{
party: { address: 'term_a', terminalHandle: 'term_a', orcaSessionId: null },
name: 'Coder'
}
],
orchestration: null
}
},
{ unsentNotice: "Claude couldn't start." }
)
expect(textIn(tree.root)).toContain("Claude couldn't start.")
})

it('preserves an ordinary assistant answer without interpreting its text as a host notice', () => {
const tree = render(toolMessage([{ type: 'text', text: 'provider fallback' }]))
expect(tree.root.find((node) => String(node.type) === 'MobileMarkdown').props.content).toBe(
Expand Down Expand Up @@ -201,6 +224,36 @@ describe('MobileNativeChatMessage', () => {
expect(texts.some((text) => text.includes('/tmp/host.png'))).toBe(true)
})

it('says under a message the host recorded but never delivered why it was not sent', () => {
const tree = render(
{ ...userMessage([{ type: 'text', text: 'hello' }]), unsent: true },
{ unsentNotice: "Claude couldn't receive this message. Send it again." }
)
expect(textIn(tree.root)).toEqual([
'hello',
"Claude couldn't receive this message. Send it again."
])
})

it('still says it was not sent when no words for it are loaded', () => {
const tree = render({ ...userMessage([{ type: 'text', text: 'hello' }]), unsent: true })
expect(textIn(tree.root)).toEqual(['hello', 'Your message was not sent.'])
})

it('says it was not sent as a muted label, not an error', () => {
const tree = render({ ...userMessage([{ type: 'text', text: 'hello' }]), unsent: true })
const [label] = tree.root.findAll(
(node) =>
node.props.style !== undefined && node.children.join('') === 'Your message was not sent.'
)
expect(label?.props.style).toMatchObject({ color: colors.textMuted })
})

it('says nothing more under a delivered message', () => {
const tree = render(userMessage([{ type: 'text', text: 'hello' }]))
expect(textIn(tree.root)).toEqual(['hello'])
})

it('makes user message text selectable', () => {
const tree = render(userMessage([{ type: 'text', text: 'Prompt I typed' }]))
const text = tree.root
Expand Down
9 changes: 9 additions & 0 deletions mobile/src/session/MobileNativeChatMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import type { NativeChatTurnStatus } from './use-mobile-native-chat-turn-status'
import { isRenderableImageUri } from './mobile-native-chat-image-preview'
import { styles, TEXT_SIZE } from './mobile-native-chat-message-styles'
import { agentMessageAttribution } from './mobile-agent-message-attribution'
import { AGENT_SESSION_WRITE_NOTICE_COPY } from '../../../src/shared/agent-session-write-notice-copy'
import { withoutPendingNativeChatVisualDirectiveTail } from '../../../src/shared/native-chat-visual-directive'
import {
MobileNativeChatVisualContext,
Expand Down Expand Up @@ -146,6 +147,7 @@ function MobileNativeChatMessageImpl({
reasoningIsLive = false,
reasoningExpanded,
onToggleReasoning,
unsentNotice,
subagentGroupsOpen,
onToggleSubagentGroup
}: {
Expand Down Expand Up @@ -175,6 +177,8 @@ function MobileNativeChatMessageImpl({
/** The transcript-held disclosure of a reasoning row; one stable handler takes its key. */
reasoningExpanded?: boolean
onToggleReasoning?: (key: string) => void
/** Why the host did not deliver this message, when it is shown as not sent. */
unsentNotice?: string
/** The roster groups the reader opened, held by the transcript; only a roster row gets it. */
subagentGroupsOpen?: ReadonlySet<string>
onToggleSubagentGroup?: (groupId: string) => void
Expand Down Expand Up @@ -308,6 +312,11 @@ function MobileNativeChatMessageImpl({
/>
) : null}
</Content>
{message.role === 'user' && message.unsent === true ? (
<NativeText style={styles.unsentLabel}>
{unsentNotice ?? AGENT_SESSION_WRITE_NOTICE_COPY.notDoneSend}
</NativeText>
) : null}
</View>
{actionsOpen ? (
<MobileNativeChatMessageActionsSheet
Expand Down
2 changes: 1 addition & 1 deletion mobile/src/session/MobileNativeChatQueuedMessages.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ describe('MobileNativeChatQueuedMessages', () => {

it("lets a returned card's reason wrap whole, in the destructive color, beside an alert", async () => {
const reason =
'The provider did not accept this message: Claude does not support the image type .bmp in a steering message.'
"Claude didn't accept this message: Claude does not support the image type .bmp in a steering message."
const mounted = await mount({
cards: [
card({ messageId: 'r', state: 'returned', needsAttention: true, caption: reason }),
Expand Down
25 changes: 9 additions & 16 deletions mobile/src/session/MobileNativeChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type { NativeChatTurnJournal } from '../../../src/shared/native-chat-turn
import { colors } from '../theme/mobile-theme'
import { styles } from './mobile-native-chat-view-styles'
import { mobileNativeChatListFooter } from './mobile-native-chat-list-footer'
import { mobileNativeChatListHeader } from './mobile-native-chat-list-header'
import {
buildMobileNativeChatTransientData,
mobileNativeChatEmptyState,
Expand All @@ -44,6 +45,7 @@ import type { MobileChatPermission } from './mobile-native-chat-permission'
import type { MobileChatQuestion } from './mobile-native-chat-question'
import type { MobileNativeChatSessionOptionPickersProps } from './MobileNativeChatSessionOptionPickers'
import { MobileNativeChatMessage } from './MobileNativeChatMessage'
import { mobileNativeChatUnsentNotices } from './mobile-native-chat-unsent-notices'
import type { MobileNativeChatStatus } from './use-mobile-native-chat-session'

/** Why the composer input is locked: the transport is disconnected, or the
Expand Down Expand Up @@ -297,6 +299,10 @@ export function MobileNativeChatView({
scopeKey: sendSurfaceId
})

const unsentNotices = useMemo(
() => mobileNativeChatUnsentNotices(turnJournal, agent ?? null),
[turnJournal, agent]
)
const renderItem = useCallback(
({ item, index }: { item: NativeChatMessage; index: number }) => (
<MobileNativeChatMessage
Expand All @@ -306,10 +312,11 @@ export function MobileNativeChatView({
onOpenFile={onOpenFile}
structuredActivityUi={structuredActivityUi}
onToggleTurn={turns.onToggleTurn}
unsentNotice={item.unsent === true ? unsentNotices.get(item.id) : undefined}
{...turns.resolveRow(index, item)}
/>
),
[toolsExpanded, fontScale, onOpenFile, structuredActivityUi, turns]
[toolsExpanded, fontScale, onOpenFile, structuredActivityUi, turns, unsentNotices]
)

const liveStatus = turns.liveLine ? (
Expand Down Expand Up @@ -368,21 +375,7 @@ export function MobileNativeChatView({
scrollEventThrottle={32}
onContentSizeChange={pinToTailAfterContentResize}
onLayout={pinToTail}
ListHeaderComponent={
hasMore ? (
<Pressable
style={styles.loadEarlier}
onPress={loadEarlier}
disabled={loadingEarlier}
>
{loadingEarlier ? (
<ActivityIndicator size="small" color={colors.textMuted} />
) : (
<Text style={styles.loadEarlierText}>Load earlier messages</Text>
)}
</Pressable>
) : null
}
ListHeaderComponent={mobileNativeChatListHeader(hasMore, loadingEarlier, loadEarlier)}
ListFooterComponent={mobileNativeChatListFooter(
liveStatus,
turns.waitingRows,
Expand Down
Loading
Loading