Files
orca/src/shared/native-chat-notice.ts
T
Kelvin Amoaba f3551dba57 fix(mobile): show skill commands you ran and draw warnings, errors and status lines as notices (#28899)
* fix(mobile): show skill commands you ran in the chat

Claude records a skill run (/simplify, /review-pr 123) as a command
envelope, which the noise filter hides. Desktop surfaces it as the typed
`/name args` before folding; the phone only normalised image markers, so
the turn vanished and the reply answered nothing.

Both apps now prepare a terminal agent's transcript through one shared
prepareNativeChatMessages: skill envelopes, reporting-command output and
image marker turns.

* fix(native-chat): keep a streamed sentence from coming back after a tool or thought row

Both chats hide the live preview once the transcript shows it, but each
only compared the preview with the last row. When a tool call or thought
landed below the sentence, the last row had no words and the earlier
sentence came back at the bottom as a live message.

latestNativeChatReplyText reads back past those rows to the newest
assistant words in the turn; desktop's preview check and the phone's
streaming gate both use it.

* fix(mobile): draw errors, warnings and status notices as notices, not agent replies

The phone drew every system row through the agent's markdown, so an API
retry warning, a sign-in failure, "Context compacted" or "Stopped manually"
read as the agent's own words. Only the clear boundary and two host
notices were special-cased.

Desktop's notice rules move to shared native-chat-notice: which block of a
system row is its notice, and the look it gets (divider, command output,
host status, plan, aside, or a toned line). Desktop's notice row switches
on it with no change in what it draws; the phone's new notice row draws
the same looks, with words in faint type when a row has no notice.

* fix(native-chat): show a new reply that follows a tool call on desktop

The earlier fix read back past a tool call or thought to the newest
assistant words, then kept desktop's length rule. Commentary written
before the tool is often longer than the reply after it, so a new,
shorter reply stayed hidden until the transcript caught up.

The last-row rule is back as it was. Reading back now only hides the
preview when the earlier words already contain it, which is the sentence
still on screen while the tool runs. The phone's gate reads only rows
that landed after the current reply began, so it was not affected.

* fix(mobile): open the actions sheet from a long press on a plan's link

A plan card's Markdown was drawn without the row's long-press handler, so
on Android holding a link or file path inside a plan no longer opened Copy
message / Select text, as it did before plans were drawn as notices. The
notice row now passes the handler to the plan's Markdown.
2026-10-15 11:04:34 +00:00

74 lines
2.8 KiB
TypeScript

// How a chat draws a row the agent did not write in its own words: compaction and clear markers,
// command output, host status, plans, and warnings or errors. Desktop translates these words (they
// are its fallbacks); the phone shows them as they are.
import {
isAgentSessionHostStatusPresentation,
type AgentSessionHostStatusPresentation
} from './agent-session-host-status-rows'
import { AGENT_SESSION_ORCA_STOP_PRESENTATION } from './agent-session-orca-stop'
import { NATIVE_CHAT_STOPPED_BEFORE_START_PRESENTATION } from './native-chat-stopped-before-start'
import type { NativeChatBlock, NativeChatTextBlock } from './native-chat-types'
export const NATIVE_CHAT_NOTICE_COPY = {
compaction: 'Context compacted',
contextCleared: 'Context cleared',
plan: 'Plan'
} as const
export type NativeChatNoticeTone = 'warning' | 'error' | 'notice'
export type NativeChatNoticeLook =
/** A rule across the chat with its label. */
| { kind: 'divider'; label: 'compaction' | 'contextCleared' }
/** Laid out in columns, so drawn in monospace. */
| { kind: 'command-output' }
/** The host's line in the reader's words, drawn faint. */
| { kind: 'host-status'; presentation: AgentSessionHostStatusPresentation }
| { kind: 'plan' }
/** Faint, as a system row with no notice of its own is. */
| { kind: 'aside' }
/** A boxed line with its icon when it has a tone; plain text when it has none. */
| { kind: 'line'; tone: NativeChatNoticeTone | null }
/** The block a system row draws as its notice. A system row without one reads as a faint aside. */
export function nativeChatNoticeBlock(
blocks: readonly NativeChatBlock[]
): NativeChatTextBlock | undefined {
return blocks.find(
(block): block is NativeChatTextBlock =>
block.type === 'text' && (block.presentation !== undefined || block.tone !== undefined)
)
}
export function nativeChatNoticeLook(block: NativeChatTextBlock): NativeChatNoticeLook {
const { presentation } = block
if (presentation === 'compaction') {
return { kind: 'divider', label: 'compaction' }
}
if (presentation === 'context-cleared') {
return { kind: 'divider', label: 'contextCleared' }
}
if (presentation === 'command-output') {
return { kind: 'command-output' }
}
if (isAgentSessionHostStatusPresentation(presentation)) {
return { kind: 'host-status', presentation }
}
if (presentation === 'plan-document') {
return { kind: 'plan' }
}
if (presentation === NATIVE_CHAT_STOPPED_BEFORE_START_PRESENTATION) {
return { kind: 'aside' }
}
// Orca stopped the reply, not the agent: stored red for clients that predate it, read as a notice.
if (presentation === AGENT_SESSION_ORCA_STOP_PRESENTATION) {
return { kind: 'line', tone: 'notice' }
}
const { tone } = block
return {
kind: 'line',
tone: tone === 'warning' || tone === 'error' || tone === 'notice' ? tone : null
}
}