feat(native-chat): one notice card above the composer for the chat's own errors (#25835)

Orca's own errors in a native chat were loose lines between the transcript
and the message box: a red "Chat could not be started." line with Retry, a
red paragraph for the chat's line and for a send or slash-command error
(where the second was hidden whenever the first showed), and a muted
attachment notice inside the composer that always used a broken-image icon.
A send that threw showed the raw IPC error as the whole message.

They are now rows of one card attached above the message box, errors first:
each row has an icon, Orca's words, and its action (Retry for a failed start,
dismiss for errors the user caused). Error text Orca did not write, such as a
host or system error, is shown apart in a monospace block with a copy button,
under the words saying what didn't happen. The card also shows where a
question or approval card takes the composer's place.

Also translates "Paste failed." and the attachments-with-a-command refusal.
This commit is contained in:
Brennan Benson
2026-10-06 23:30:55 -07:00
committed by GitHub
parent f31d7a4ed3
commit a3ecee0dce
33 changed files with 733 additions and 111 deletions
@@ -1,3 +1,4 @@
import { useNativeChatComposerNotice } from './use-native-chat-composer-notice'
import type { NativeChatComposerInput } from './native-chat-composer-input'
import { forwardRef, useCallback, useState } from 'react'
import { useNativeChatComposerInterrupt } from './use-native-chat-composer-interrupt'
@@ -70,7 +71,8 @@ const NativeChatComposerPane = forwardRef<NativeChatComposerHandle, NativeChatCo
launchSeed,
structuredTransport,
steerQueued,
inputOwnedByCard = false
inputOwnedByCard = false,
notices: chatNotices
},
ref
): React.JSX.Element {
@@ -96,7 +98,7 @@ const NativeChatComposerPane = forwardRef<NativeChatComposerHandle, NativeChatCo
})
const [history, setHistory] = useState<HistoryState>(EMPTY_HISTORY)
const [activeSuggestion, setActiveSuggestion] = useState(0)
const [notice, setNotice] = useState<string | null>(null)
const { notices, setNotice } = useNativeChatComposerNotice(chatNotices)
const { textareaRef } = useNativeChatComposerAppMenuSelection(imeEnterGesture.isComposing)
const { cancelPendingSends, trackPendingSend } = useNativeChatSendLifecycle(
terminalTabId,
@@ -370,7 +372,7 @@ const NativeChatComposerPane = forwardRef<NativeChatComposerHandle, NativeChatCo
autocomplete={autocomplete}
mentionFiles={mentionFiles}
activeSuggestion={activeSuggestion}
notice={notice}
notices={notices}
imageAttachments={imageAttachments}
sendButtonDisabled={sendButtonDisabled}
sendBlockedReason={imageBlock.reason}
@@ -2,7 +2,6 @@ import { NativeChatPromptEditor } from './NativeChatPromptEditor'
import type { NativeChatComposerInput } from './native-chat-composer-input'
import type { ClipboardEventHandler, KeyboardEventHandler, RefObject } from 'react'
import { useLayoutEffect, useRef } from 'react'
import { ImageOff } from 'lucide-react'
import type { useImeEnterGestureOwnership } from '@/lib/ime-composition-keyboard-event'
import { cn } from '@/lib/utils'
import { NATIVE_FILE_DROP_TARGET } from '../../../../shared/native-file-drop'
@@ -10,6 +9,8 @@ import type { ComposerAutocomplete, NativeChatPickerItem } from './native-chat-c
import { NativeChatMentionMenu, NativeChatPickerMenu } from './NativeChatAutocompleteMenus'
import type { NativeChatMentionFiles } from './use-native-chat-mention-files'
import { NativeChatComposerActions } from './NativeChatComposerActions'
import { NativeChatComposerNotices } from './NativeChatComposerNotices'
import type { NativeChatComposerNotice } from './native-chat-composer-notice'
import type { NativeChatContextUsageSummary } from './native-chat-context-usage-summary'
import {
nativeChatComposerPlaceholder,
@@ -45,7 +46,7 @@ export type NativeChatComposerFieldProps = {
autocomplete: ComposerAutocomplete
mentionFiles: NativeChatMentionFiles
activeSuggestion: number
notice: string | null
notices: readonly NativeChatComposerNotice[]
imageAttachments: readonly NativeChatComposerImageAttachment[]
sendButtonDisabled: boolean
/** Why the send button is disabled, when the user can do something about it. */
@@ -132,7 +133,7 @@ export function NativeChatComposerField({
autocomplete,
mentionFiles,
activeSuggestion,
notice,
notices,
imageAttachments,
sendButtonDisabled,
sendBlockedReason,
@@ -243,12 +244,21 @@ export function NativeChatComposerField({
onChoose={onChooseMentionFile}
/>
) : null}
{notice ? (
<div className="mb-1.5 flex items-center gap-1.5 text-xs text-muted-foreground">
<ImageOff className="size-3.5 shrink-0" />
<span>{notice}</span>
</div>
) : null}
<NativeChatComposerNotices
notices={notices.map((notice) =>
// A dismissed row takes its focused × with it; the message box gets focus back.
notice.onDismiss
? {
...notice,
onDismiss: () => {
notice.onDismiss?.()
textareaRef.current?.focus()
}
}
: notice
)}
className="mb-1.5"
/>
<div
data-native-file-drop-target={NATIVE_FILE_DROP_TARGET.composer}
data-composer-scope-key={dropScopeKey}
@@ -0,0 +1,132 @@
// @vitest-environment happy-dom
import '@testing-library/jest-dom/vitest'
import { cleanup, fireEvent, render, screen, within } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { NativeChatComposerNotices } from './NativeChatComposerNotices'
import { CLIPBOARD_TEXT_TOO_LARGE_ERROR } from '../../../../shared/clipboard-text'
import { nativeChatNoticeFromError, setNativeChatPasteFailure } from './native-chat-composer-notice'
afterEach(cleanup)
describe('NativeChatComposerNotices', () => {
it('draws no card when there is nothing to say, but keeps its live region for the next notice', () => {
const { container } = render(<NativeChatComposerNotices notices={[]} />)
expect(container.querySelectorAll('li')).toHaveLength(0)
expect(container.querySelector('[aria-live="polite"]')).toBeInTheDocument()
})
it('puts errors first and keeps every notice, so one never hides another', () => {
const { container } = render(
<NativeChatComposerNotices
notices={[
{ key: 'composer', kind: 'attachment', text: 'Worktree not ready.' },
{ key: 'session', kind: 'error', text: 'Orca couldn’t save your message.' },
{ key: 'slash', kind: 'error', text: 'sonnet-9 is not an available model.' }
]}
/>
)
const texts = screen.getAllByText(/./, { selector: 'p' }).map((node) => node.textContent)
expect(texts).toEqual([
'Orca couldn’t save your message.',
'sonnet-9 is not an available model.',
'Worktree not ready.'
])
expect(container.querySelectorAll('[data-notice-kind="error"]')).toHaveLength(2)
})
it('shows error text Orca did not write in its own block, with a copy button', () => {
const write = vi.fn().mockResolvedValue(undefined)
Object.assign(window, { api: { ui: { writeClipboardText: write } } })
render(
<NativeChatComposerNotices
notices={[
{
key: 'composer',
kind: 'error',
text: 'Your message was not sent.',
errorText: 'connect ECONNREFUSED /tmp/agent-host.sock'
}
]}
/>
)
const row = screen.getByRole('listitem')
expect(within(row).getByText('Your message was not sent.').tagName).toBe('P')
expect(within(row).getByText('connect ECONNREFUSED /tmp/agent-host.sock').tagName).toBe('PRE')
fireEvent.click(within(row).getByRole('button', { name: 'Copy error' }))
expect(write).toHaveBeenCalledWith('connect ECONNREFUSED /tmp/agent-host.sock')
})
it('offers the notice action and dismiss only where given', () => {
const retry = vi.fn()
const dismiss = vi.fn()
render(
<NativeChatComposerNotices
notices={[
{
key: 'launch',
kind: 'error',
text: 'Chat could not be started.',
action: { label: 'Retry', onClick: retry }
},
{ key: 'composer', kind: 'attachment', text: 'Paste failed.', onDismiss: dismiss }
]}
/>
)
expect(screen.getAllByRole('button', { name: 'Dismiss' })).toHaveLength(1)
fireEvent.click(screen.getByRole('button', { name: 'Retry' }))
fireEvent.click(screen.getByRole('button', { name: 'Dismiss' }))
expect(retry).toHaveBeenCalledTimes(1)
expect(dismiss).toHaveBeenCalledTimes(1)
})
})
describe('nativeChatNoticeFromError', () => {
it('keeps a wrapped main-process error apart from the headline, without its wrapper', () => {
expect(
nativeChatNoticeFromError(
new Error(
"Error invoking remote method 'agentSession:send': Error: connect ECONNREFUSED /tmp/a.sock"
),
'Your message was not sent.'
)
).toEqual({ text: 'Your message was not sent.', errorText: 'connect ECONNREFUSED /tmp/a.sock' })
})
it('keeps a local error apart unless the caller says it is already Orca’s words', () => {
const error = new Error('Pasted text is too large for this message.')
expect(nativeChatNoticeFromError(error, 'Paste failed.')).toEqual({
text: 'Paste failed.',
errorText: 'Pasted text is too large for this message.'
})
expect(
nativeChatNoticeFromError(error, 'Paste failed.', { localErrorIsOrcaWords: true })
).toEqual({ text: 'Pasted text is too large for this message.' })
})
it("says a paste over the size limit in Orca's words, even when it comes back from the main process", () => {
const setNotice = vi.fn()
setNativeChatPasteFailure(
setNotice,
new Error(
`Error invoking remote method 'ui:readClipboardText': Error: ${CLIPBOARD_TEXT_TOO_LARGE_ERROR}`
)
)
expect(setNotice).toHaveBeenCalledWith(CLIPBOARD_TEXT_TOO_LARGE_ERROR)
})
it('keeps any other main-process paste failure apart under "Paste failed."', () => {
const setNotice = vi.fn()
setNativeChatPasteFailure(
setNotice,
new Error("Error invoking remote method 'ui:readClipboardText': Error: sftp down")
)
expect(setNotice).toHaveBeenCalledWith('Paste failed.', 'sftp down')
})
it('says only the headline when the error carries no words', () => {
expect(nativeChatNoticeFromError(new Error(' '), 'Paste failed.')).toEqual({
text: 'Paste failed.'
})
expect(nativeChatNoticeFromError(undefined, 'Paste failed.')).toEqual({ text: 'Paste failed.' })
})
})
@@ -0,0 +1,110 @@
import { AlertCircle, Paperclip, X } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { translate } from '@/i18n/i18n'
import { cn } from '@/lib/utils'
import { NativeChatCopyButton } from './NativeChatCopyButton'
import type { NativeChatComposerNotice } from './native-chat-composer-notice'
/** One card above the composer, one row per notice, errors first. The live region stays mounted so
* a screen reader hears the first notice too. */
export function NativeChatComposerNotices({
notices,
className
}: {
notices: readonly NativeChatComposerNotice[]
className?: string
}): React.JSX.Element {
if (notices.length === 0) {
return <ul aria-live="polite" />
}
const ordered = [...notices].sort(
(a, b) => Number(b.kind === 'error') - Number(a.kind === 'error')
)
return (
<ul
aria-live="polite"
className={cn(
'divide-y divide-border overflow-hidden rounded-md border border-border bg-card text-xs text-card-foreground',
ordered[0]?.kind === 'error' && 'border-destructive/30',
className
)}
>
{ordered.map((notice) => (
<NoticeRow key={notice.key} notice={notice} />
))}
</ul>
)
}
/** The card where a question or approval card has taken the composer's place. */
export function NativeChatPromptSlotNotices({
notices
}: {
notices: readonly NativeChatComposerNotice[]
}): React.JSX.Element {
return (
<div
className={cn(
notices.length > 0 && 'mx-auto w-full max-w-(--chat-content-max-width) px-3 pt-2 sm:px-4'
)}
>
<NativeChatComposerNotices notices={notices} />
</div>
)
}
function NoticeRow({ notice }: { notice: NativeChatComposerNotice }): React.JSX.Element {
const isError = notice.kind === 'error'
const Icon = isError ? AlertCircle : Paperclip
const dismissLabel = translate('components.native-chat.notices.dismiss', 'Dismiss')
return (
<li data-notice-kind={notice.kind} className={cn(isError && 'bg-destructive/5')}>
<div className="flex items-start gap-2 px-2.5 py-1.5">
<Icon
aria-hidden
className={cn(
'mt-0.5 size-3.5 shrink-0',
isError ? 'text-destructive' : 'text-muted-foreground'
)}
/>
<p
className={cn(
'min-w-0 flex-1 select-text py-px leading-5 [overflow-wrap:anywhere]',
isError ? 'text-foreground' : 'text-muted-foreground'
)}
>
{notice.text}
</p>
{notice.action ? (
<Button type="button" variant="outline" size="xs" onClick={notice.action.onClick}>
{notice.action.label}
</Button>
) : null}
{notice.onDismiss ? (
<Button
type="button"
variant="ghost"
size="icon-xs"
aria-label={dismissLabel}
title={dismissLabel}
onClick={notice.onDismiss}
>
<X />
</Button>
) : null}
</div>
{notice.errorText ? (
<div className="relative mb-2 ml-8 mr-2.5 rounded-md border border-border bg-muted">
<pre className="scrollbar-sleek max-h-28 select-text overflow-auto whitespace-pre-wrap break-words py-1.5 pl-2 pr-8 font-mono text-[11px] text-foreground">
{notice.errorText}
</pre>
<NativeChatCopyButton
text={notice.errorText}
label={translate('components.native-chat.notices.copyError', 'Copy error')}
className="absolute right-0.5 top-0.5"
/>
</div>
) : null}
</li>
)
}
@@ -0,0 +1,106 @@
// @vitest-environment happy-dom
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, expect, it, vi } from 'vitest'
const { mocks, moduleFactories, resetStructuredSessionMocks } = await vi.hoisted(async () =>
(await import('./NativeChatStructuredSession.test-harness')).createStructuredSessionMocks()
)
vi.mock('@/lib/structured-agent-session-launch', () =>
moduleFactories.structuredAgentSessionLaunch()
)
vi.mock('@/runtime/structured-agent-session-client', () =>
moduleFactories.structuredAgentSessionClient()
)
vi.mock('./use-structured-agent-session', () => moduleFactories.useStructuredAgentSession())
vi.mock('./use-native-chat-font-size', () => moduleFactories.useNativeChatFontSize())
vi.mock('./use-native-chat-file-link-context', () => moduleFactories.useNativeChatFileLinkContext())
vi.mock('./use-native-chat-file-link-click', () => moduleFactories.useNativeChatFileLinkClick())
vi.mock('./NativeChatMessageList', () => moduleFactories.nativeChatMessageList())
vi.mock('./NativeChatComposer', () => moduleFactories.nativeChatComposer())
vi.mock('./NativeChatApprovalCard', () => moduleFactories.nativeChatApprovalCard())
vi.mock('./NativeChatQuestionCard', () => moduleFactories.nativeChatQuestionCard())
import { NativeChatStructuredSession } from './NativeChatStructuredSession'
import type { AgentJournalRenderItem } from '../../../../shared/agent-session-journal-types'
const SESSION_ID = 'notice-card-session'
afterEach(() => {
cleanup()
resetStructuredSessionMocks()
})
function renderPane(): void {
render(
<NativeChatStructuredSession
isVisible
isFocusedGroup
tabId="structured-notice-card-tab"
sessionId={SESSION_ID}
target={{ kind: 'local' }}
agent="codex"
/>
)
}
function composerOnError(): (text: string | null, errorText?: string) => void {
const onError = mocks.composerProps?.structuredTransport?.onError
if (!onError) {
throw new Error('composer transport has no onError')
}
return onError
}
it("shows the chat's own error and a composer error together, where one used to hide the other", () => {
// A history read that failed beside a transcript it keeps: the chat's own line.
mocks.status = 'error'
mocks.readRefusal = {
code: 'agent_session_journal_unreadable',
details: { reason: 'journalUnavailable' }
}
renderPane()
act(() => {
composerOnError()('sonnet-9 is not an available model for this chat session.')
})
expect(screen.getByText("Orca couldn't open this chat's history right now.")).toBeTruthy()
expect(screen.getByText('sonnet-9 is not an available model for this chat session.')).toBeTruthy()
expect(document.querySelectorAll('[data-notice-kind="error"]')).toHaveLength(2)
})
it('keeps a send failure’s raw error apart and lets the user dismiss it', () => {
renderPane()
act(() => {
composerOnError()('Your message was not sent.', 'connect ECONNREFUSED /tmp/agent-host.sock')
})
expect(screen.getByText('connect ECONNREFUSED /tmp/agent-host.sock').tagName).toBe('PRE')
fireEvent.click(screen.getByRole('button', { name: 'Dismiss' }))
expect(screen.queryByText('Your message was not sent.')).toBeNull()
})
const APPROVAL: AgentJournalRenderItem = JSON.parse(
JSON.stringify({
itemId: 'approval-item',
revision: 1,
sequence: 1,
observedAt: 1,
body: {
kind: 'approval',
title: 'Run the plan?',
detail: null,
subject: { kind: 'plan', text: 'x' },
options: [{ id: 'allow', label: 'Approve' }],
resolution: { state: 'pending', selectedOptionId: null, resolvedBy: null, resolvedAt: null }
}
})
)
it("keeps the notices above an approval card that takes the composer's place", () => {
mocks.launchLifecycle = 'failed'
mocks.promptItems = [APPROVAL]
renderPane()
expect(screen.queryByTestId('structured-composer')).toBeNull()
expect(screen.getByText('Chat could not be started.')).toBeTruthy()
expect(screen.getByRole('button', { name: 'Retry' })).toBeTruthy()
})
@@ -0,0 +1,37 @@
import { forwardRef, useImperativeHandle, useRef } from 'react'
import type { NativeChatComposerNotice } from './native-chat-composer-notice'
import { NativeChatComposerNotices } from './NativeChatComposerNotices'
/** The structured session tests' composer: records its props, takes real DOM focus, and draws the
* chat's notices above its input as the real composer does. */
export function createStructuredSessionComposerMock(mocks: {
composerProps: unknown
handlePasteEvent: unknown
pasteFromClipboard: unknown
}) {
return {
NativeChatComposer: forwardRef(
(props: { notices?: readonly NativeChatComposerNotice[] }, ref) => {
mocks.composerProps = props
const fieldRef = useRef<HTMLTextAreaElement>(null)
useImperativeHandle(ref, () => ({
// Real DOM focus: the reveal-focus loop retries until focus lands in the pane.
focus: () => {
fieldRef.current?.focus()
return true
},
insertTypedText: () => true,
handlePasteEvent: mocks.handlePasteEvent,
pasteFromClipboard: mocks.pasteFromClipboard,
contains: (node: Node | null) => fieldRef.current?.contains(node) === true
}))
return (
<>
<NativeChatComposerNotices notices={props.notices ?? []} />
<textarea ref={fieldRef} aria-label="Message" data-testid="structured-composer" />
</>
)
}
)
}
}
@@ -1,5 +1,5 @@
import { act } from '@testing-library/react'
import { forwardRef, useImperativeHandle, useRef } from 'react'
import { useImperativeHandle } from 'react'
import { vi } from 'vitest'
import type { AgentJournalRenderItem } from '../../../../shared/agent-session-journal-types'
import type { QueuedMessageCard } from './structured-agent-session-queued-cards'
@@ -12,6 +12,8 @@ import type { NativeChatQuestionCardProps } from './NativeChatQuestionCard'
import type { NativeChatLaunchSeed } from './native-chat-composer-types'
import type { NativeChatMessageListHandle } from './use-native-chat-reveal-latest'
import type { NativeChatFileLinkContext } from './native-chat-file-link'
import type { NativeChatComposerNotice } from './native-chat-composer-notice'
import { createStructuredSessionComposerMock } from './NativeChatStructuredSession.test-composer'
import type { NativeChatOlderPageResult } from './native-chat-pagination'
import type { StructuredAgentSessionThreadGoal } from './use-structured-agent-session-thread-goal'
import type { StructuredAgentSessionLaunchLifecycle } from '@/lib/structured-agent-session-launch'
@@ -141,12 +143,16 @@ export function createStructuredSessionMocks() {
messageListProps: initialMessageListProps,
composerProps: nullable<{
launchSeed?: NativeChatLaunchSeed
structuredTransport?: Record<string, unknown> & { queueResume?: QueueResumeMock }
structuredTransport?: Record<string, unknown> & {
queueResume?: QueueResumeMock
onError?: (text: string | null, errorText?: string) => void
}
isWorking?: boolean
isStopping?: boolean
afterStop?: 'queue' | 'send'
steerQueued?: () => boolean
onStop?: () => void
notices?: readonly NativeChatComposerNotice[]
}>(),
approvalCardProps: initialApprovalCardProps,
questionCardProps: null as NativeChatQuestionCardProps | null,
@@ -341,24 +347,7 @@ export function createStructuredSessionMocks() {
return <DeliveryNoticesMock notices={props?.deliveryNotices} />
}
}),
nativeChatComposer: () => ({
NativeChatComposer: forwardRef((props: typeof mocks.composerProps, ref) => {
mocks.composerProps = props
const fieldRef = useRef<HTMLTextAreaElement>(null)
useImperativeHandle(ref, () => ({
// Real DOM focus: the reveal-focus loop retries until focus lands in the pane.
focus: () => {
fieldRef.current?.focus()
return true
},
insertTypedText: () => true,
handlePasteEvent: mocks.handlePasteEvent,
pasteFromClipboard: mocks.pasteFromClipboard,
contains: (node: Node | null) => fieldRef.current?.contains(node) === true
}))
return <textarea ref={fieldRef} data-testid="structured-composer" />
})
}),
nativeChatComposer: () => createStructuredSessionComposerMock(mocks),
nativeChatEmptyState: () => ({ NativeChatEmptyState: () => null }),
nativeChatApprovalCard: () => ({
NativeChatApprovalCard: (props: NativeChatApprovalCardProps) => {
@@ -26,7 +26,9 @@ import { useStructuredNativeChatPaneCommands } from './use-structured-native-cha
import type { NativeChatStructuredViewProps } from './native-chat-view-types'
import { NativeChatStructuredSessionStatus } from './NativeChatStructuredSessionStatus'
import { useNativeChatLaunchDraftSignal } from './use-native-chat-launch-draft-adoption'
import { NativeChatLaunchRetry } from './NativeChatLaunchRetry'
import { structuredSessionNotices } from './native-chat-structured-session-notices'
import { NativeChatPromptSlotNotices } from './NativeChatComposerNotices'
import { useNativeChatComposerError } from './use-native-chat-composer-notice'
import { useNativeChatProvisionalLaunch } from './use-native-chat-provisional-launch'
import { useStructuredAgentSessionHostExecution } from './StructuredAgentSessionStatusBridge'
import { useNativeChatRewindHost } from './use-native-chat-rewind-host'
@@ -83,7 +85,7 @@ export function NativeChatStructuredSession(
// phases, that empty list must not become the draft's turn baseline.
transcriptLoading: controller.status === 'idle' || controller.status === 'loading'
})
const [composerError, setComposerError] = useState<string | null>(null)
const { composerError, reportComposerError } = useNativeChatComposerError()
const [optionPickerRequest, setOptionPickerRequest] = useState<OptionPickerRequest | null>(null)
const rootRef = useRef<HTMLDivElement>(null)
const paneCommands = useStructuredNativeChatPaneCommands({
@@ -191,11 +193,16 @@ export function NativeChatStructuredSession(
worktreeId: ownerWorktreeId ?? undefined,
optionPickerRequest,
setOptionPickerRequest,
onError: setComposerError,
onError: reportComposerError,
onSubmitted: revealLatest,
queuedMessages: submits.queuedMessages
})
// Said once: on the pane when the failure took it, else in the composer's notices.
const sessionError =
viewState.kind === 'error' || !readFailure ? controller.error : readFailure.text
const launch = { ...provisionalLaunch, retry: submits.retryLaunch }
const notices = structuredSessionNotices({ launch, agentLabel, sessionError, composerError })
return (
<div
ref={rootRef}
@@ -259,12 +266,6 @@ export function NativeChatStructuredSession(
</div>
{readFailedFinally ? null : (
<>
<NativeChatLaunchRetry
lifecycle={provisionalLaunch.lifecycle}
failure={provisionalLaunch.failure}
agentLabel={agentLabel}
onRetry={submits.retryLaunch}
/>
{/* Host-held drafts, never transcript rows. Above the status area, so running shells and agents sit next to the composer. */}
<NativeChatQueuedMessageList
controller={submits.queuedMessages}
@@ -275,10 +276,6 @@ export function NativeChatStructuredSession(
<NativeChatStructuredSessionStatus
sessionId={props.sessionId}
paneKey={paneKey}
// Said once: on the pane when the failure took it, else here. A loaded chat stores only a
// refusal the host sent, so one beside messages is the host's or from before any load.
error={viewState.kind === 'error' || !readFailure ? controller.error : readFailure.text}
composerError={composerError}
isVisible={props.isVisible}
backgroundTasks={controller.backgroundTasks}
stopBackgroundTask={controller.stopBackgroundTask}
@@ -298,6 +295,7 @@ export function NativeChatStructuredSession(
/>
) : null}
{/* Prompt cards take the composer's slot, below the background-task dock. */}
{composerShown ? null : <NativeChatPromptSlotNotices notices={notices} />}
{prompt && approval ? (
<NativeChatApprovalCard
key={`${prompt.itemId}:${prompt.revision}`}
@@ -353,6 +351,7 @@ export function NativeChatStructuredSession(
steerQueued={stopControls.stopping ? undefined : submits.queuedMessages.steerNewest}
structuredTransport={structuredTransport}
launchSeed={{ ...launchDraftSignal, ownsTabWideLaunchDraft: true }}
notices={notices}
/>
) : null}
</>
@@ -15,8 +15,6 @@ export function NativeChatStructuredSessionStatus(props: {
sessionId: string
/** The session's own status row, whose verdict the strip's children read. */
paneKey: string
error: string | null
composerError: string | null
isVisible: boolean
backgroundTasks: StructuredSessionBackgroundTasksView
stopBackgroundTask: (taskId?: string) => Promise<unknown>
@@ -58,11 +56,6 @@ export function NativeChatStructuredSessionStatus(props: {
return (
<>
{props.error || props.composerError ? (
<p className="mx-auto w-full max-w-(--chat-content-max-width) px-4 py-1 text-xs text-destructive">
{props.error ?? props.composerError}
</p>
) : null}
{props.backgroundTasks.show ? (
<NativeChatBackgroundTasksStatus
isVisible={props.isVisible}
@@ -55,7 +55,7 @@ function TestField({
canSend
autocomplete={{ mode: 'none' }}
activeSuggestion={0}
notice={null}
notices={[]}
imageAttachments={imageAttachments}
sendButtonDisabled={false}
isWorking={false}
@@ -44,7 +44,7 @@ function fieldProps(overrides: Partial<TestFieldProps> = {}): TestFieldProps {
canSend: true,
autocomplete: { mode: 'none' },
activeSuggestion: 0,
notice: null,
notices: [],
imageAttachments: [],
sendButtonDisabled: false,
isWorking: false,
@@ -80,7 +80,7 @@ function TestField({
canSend
autocomplete={{ mode: 'none' }}
activeSuggestion={0}
notice={null}
notices={[]}
imageAttachments={imageAttachments}
sendButtonDisabled={disabled || (!isWorking && draft === '' && imageAttachments.length === 0)}
isWorking={isWorking}
@@ -0,0 +1,66 @@
// What the notice card above a native chat's composer shows: Orca's own words, plus, apart from
// them, any error text Orca did not write, so it can be read and copied as written.
import { translate } from '@/i18n/i18n'
import { readIpcErrorDetail } from '@/lib/ipc-error'
import { isClipboardTextTooLargeError } from '../../../../shared/clipboard-text'
export type NativeChatComposerNoticeContent = {
text: string
/** Text from outside Orca (a host or system error), shown apart and copyable. */
errorText?: string
}
export type NativeChatComposerNotice = NativeChatComposerNoticeContent & {
key: string
/** `attachment` is the composer's own paste and attachment notice. */
kind: 'error' | 'attachment'
action?: { label: string; onClick: () => void }
onDismiss?: () => void
}
/** `headline` in Orca's words, with the error as written beside it. `localErrorIsOrcaWords`: an
* error raised in this window (not wrapped by main-process IPC) is already Orca's sentence. */
export function nativeChatNoticeFromError(
error: unknown,
headline: string,
options: { localErrorIsOrcaWords?: boolean } = {}
): NativeChatComposerNoticeContent {
const detail = error instanceof Error ? readIpcErrorDetail(error) : String(error ?? '').trim()
if (!detail) {
return { text: headline }
}
const wrapped = error instanceof Error && detail !== error.message.trim()
return options.localErrorIsOrcaWords && error instanceof Error && !wrapped
? { text: detail }
: { text: headline, errorText: detail }
}
/** Says `headline` through a notice setter, with the error's own text apart when it has any. */
export function setNativeChatNoticeFromError(
setNotice: (text: string | null, errorText?: string) => void,
error: unknown,
headline: string,
options: { localErrorIsOrcaWords?: boolean } = {}
): void {
const notice = nativeChatNoticeFromError(error, headline, options)
if (notice.errorText) {
setNotice(notice.text, notice.errorText)
} else {
setNotice(notice.text)
}
}
/** A failed paste: an error this window raised, or Orca's own size limit coming back from the main
* process, is already Orca's sentence; any other is kept apart under `headline`. */
export function setNativeChatPasteFailure(
setNotice: (text: string | null, errorText?: string) => void,
error: unknown,
headline = translate('components.native-chat.composer.pasteFailed', 'Paste failed.')
): void {
if (isClipboardTextTooLargeError(error)) {
setNotice(readIpcErrorDetail(error) ?? headline)
return
}
setNativeChatNoticeFromError(setNotice, error, headline, { localErrorIsOrcaWords: true })
}
@@ -1,4 +1,5 @@
import type { NativeChatQueueResume } from './native-chat-composer-primary-action'
import type { NativeChatComposerNotice } from './native-chat-composer-notice'
import type { AgentSessionConversationCommand } from '../../../../shared/agent-session-conversation-command'
import type { StructuredAgentContextUsage } from '../../../../shared/structured-agent-session-context-usage'
import type { AgentSessionSlashCommand } from '../../../../shared/agent-session-wire'
@@ -46,7 +47,8 @@ export type NativeChatStructuredComposerTransport = {
worktreeId?: string
/** Present only where the host can set this session's goal. */
threadGoal?: { setObjective: (objective: string) => Promise<boolean> }
onError: (message: string | null) => void
/** `errorText` is error text Orca did not write, shown apart and copyable. */
onError: (message: string | null, errorText?: string) => void
/** A local send: brings the latest into view at the press, not when the host answers. */
onSubmitted?: () => void
runtime: 'local' | 'remote'
@@ -116,6 +118,8 @@ export type NativeChatComposerProps = {
/** Cmd/Ctrl+Enter from an empty composer: send the newest queued draft now.
* False = nothing queued, and the chord falls through to a plain send. */
steerQueued?: () => boolean
/** The chat's own notices, shown in the composer's notice card above its input. */
notices?: readonly NativeChatComposerNotice[]
}
/** Launch context prefilled into the TUI input as an unsent draft, plus the two
@@ -174,7 +174,7 @@ function ComposerProbe({
canSend={!disabled}
autocomplete={{ mode: 'none' }}
activeSuggestion={0}
notice={notice}
notices={notice ? [{ key: 'composer', kind: 'attachment', text: notice }] : []}
imageAttachments={attachments.imageAttachments}
sendButtonDisabled={false}
isWorking={false}
@@ -4,7 +4,8 @@ import { afterEach, expect, it } from 'vitest'
import { sayAgentSessionFailureEnglish } from '../../../../shared/agent-session-failure-copy'
import { agentSessionFailureSentence } from '../../../../shared/agent-session-failure-words'
import { agentSessionRefusalFailure } from '../../../../shared/agent-session-write-failure'
import { NativeChatLaunchRetry } from './NativeChatLaunchRetry'
import { NativeChatComposerNotices } from './NativeChatComposerNotices'
import { structuredSessionNotices } from './native-chat-structured-session-notices'
afterEach(cleanup)
@@ -14,11 +15,13 @@ it('names the Command setting when it is not a program Orca can run', () => {
details: { reason: 'agentCommandNotRunnable' }
})
render(
<NativeChatLaunchRetry
lifecycle="failed"
failure={failure}
agentLabel="Claude"
onRetry={() => {}}
<NativeChatComposerNotices
notices={structuredSessionNotices({
launch: { lifecycle: 'failed', failure, retry: () => {} },
agentLabel: 'Claude',
sessionError: null,
composerError: null
})}
/>
)
expect(
@@ -1,25 +1,28 @@
import { RotateCcw } from 'lucide-react'
import { Button } from '@/components/ui/button'
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'
import type {
NativeChatComposerNotice,
NativeChatComposerNoticeContent
} from './native-chat-composer-notice'
export function NativeChatLaunchRetry({
/** A chat whose start failed or went unconfirmed, as a notice with Retry. */
function nativeChatLaunchNotice({
lifecycle,
failure = null,
agentLabel,
onRetry
}: {
lifecycle: StructuredAgentSessionLaunchLifecycle | null
/** Names the agent in a start failure's words. */
agentLabel?: string
/** The host's refusal behind the failed start; its message is never shown. */
failure?: AgentSessionWriteRefusal | null
/** Names the agent in a start failure's words. */
agentLabel?: string
onRetry: () => void
}): React.JSX.Element | null {
}): NativeChatComposerNotice | null {
if (lifecycle !== 'failed' && lifecycle !== 'visibility-unknown') {
return null
}
@@ -39,17 +42,45 @@ export function NativeChatLaunchRetry({
agentSessionRefusalCauseParts(failure, agentLabel ? { agentName: agentLabel } : {})
)
: ''
// An argument problem already says the start failed; the generic lead would repeat it.
const saysStartFailure =
failure?.code === 'agent_session_operation_invalid' && failure.details?.argumentProblem
return (
<div className="mx-auto flex w-full max-w-(--chat-content-max-width) items-center justify-between gap-3 px-4 py-1 text-xs text-destructive">
<span className="min-w-0 break-words">
{cause ? (saysStartFailure ? cause : joinSentences([message, cause])) : message}
</span>
<Button type="button" variant="ghost" size="xs" onClick={onRetry}>
<RotateCcw className="size-3" />
{translate('auto.components.native.chat.NativeChatLaunchRetry.retry', 'Retry')}
</Button>
</div>
)
return {
key: 'launch',
kind: 'error',
text: cause ? (saysStartFailure ? cause : joinSentences([message, cause])) : message,
action: {
label: translate('auto.components.native.chat.NativeChatLaunchRetry.retry', 'Retry'),
onClick: onRetry
}
}
}
/** A structured chat's own notices, for the card above its composer. */
export function structuredSessionNotices({
launch,
agentLabel,
sessionError,
composerError
}: {
launch: {
lifecycle: StructuredAgentSessionLaunchLifecycle | null
failure: AgentSessionWriteRefusal | null
retry: () => void
}
agentLabel: string
sessionError: string | null
composerError: (NativeChatComposerNoticeContent & { onDismiss: () => void }) | null
}): NativeChatComposerNotice[] {
const launchNotice = nativeChatLaunchNotice({
lifecycle: launch.lifecycle,
failure: launch.failure,
agentLabel,
onRetry: launch.retry
})
return [
...(launchNotice ? [launchNotice] : []),
...(sessionError ? [{ key: 'session', kind: 'error' as const, text: sessionError }] : []),
...(composerError ? [{ key: 'composer-error', kind: 'error' as const, ...composerError }] : [])
]
}
@@ -199,8 +199,6 @@ function Surfaces(props: { roster: AgentSessionBackgroundTaskState | null }): Re
<NativeChatStructuredSessionStatus
sessionId={tab.entityId}
paneKey={PANE_KEY}
error={null}
composerError={null}
isVisible
backgroundTasks={structuredSessionBackgroundTasksView(props.roster, null)}
stopBackgroundTask={async () => undefined}
@@ -0,0 +1,35 @@
// @vitest-environment happy-dom
import { act, renderHook } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import { useNativeChatComposerNotice } from './use-native-chat-composer-notice'
import type { NativeChatComposerNotice } from './native-chat-composer-notice'
const CHAT_ERROR: NativeChatComposerNotice = {
key: 'composer-error',
kind: 'error',
text: 'sonnet-9 is not an available model for this chat session.'
}
describe('useNativeChatComposerNotice', () => {
it("adds the composer's own notice after the chat's, each under its own key", () => {
const chatNotices = [CHAT_ERROR]
const { result } = renderHook(() => useNativeChatComposerNotice(chatNotices))
act(() => result.current.setNotice('Paste failed.', 'sftp down'))
expect(result.current.notices.map((notice) => notice.key)).toEqual([
'composer-error',
'composer-attachment'
])
expect(result.current.notices[1]).toMatchObject({
text: 'Paste failed.',
errorText: 'sftp down'
})
})
it("clears its own notice on dismiss and leaves the chat's", () => {
const chatNotices = [CHAT_ERROR]
const { result } = renderHook(() => useNativeChatComposerNotice(chatNotices))
act(() => result.current.setNotice('Worktree not ready.'))
act(() => result.current.notices[1]?.onDismiss?.())
expect(result.current.notices).toEqual([CHAT_ERROR])
})
})
@@ -0,0 +1,54 @@
import { useCallback, useMemo, useState } from 'react'
import type {
NativeChatComposerNotice,
NativeChatComposerNoticeContent
} from './native-chat-composer-notice'
/** The composer's own paste and attachment notice, after the chat's notices. */
export function useNativeChatComposerNotice(chatNotices?: readonly NativeChatComposerNotice[]): {
notices: readonly NativeChatComposerNotice[]
setNotice: (text: string | null, errorText?: string) => void
} {
const [notice, setNoticeContent] = useState<NativeChatComposerNoticeContent | null>(null)
const setNotice = useCallback(
(text: string | null, errorText?: string) =>
setNoticeContent(text === null ? null : { text, ...(errorText ? { errorText } : {}) }),
[]
)
const notices = useMemo(
() => [
...(chatNotices ?? []),
...(notice
? [
{
key: 'composer-attachment',
kind: 'attachment' as const,
...notice,
onDismiss: () => setNotice(null)
}
]
: [])
],
[chatNotices, notice, setNotice]
)
return { notices, setNotice }
}
/** A chat's last send or command error: shown in the card until dismissed or the next send clears
* it. */
export function useNativeChatComposerError(): {
composerError: (NativeChatComposerNoticeContent & { onDismiss: () => void }) | null
reportComposerError: (text: string | null, errorText?: string) => void
} {
const [content, setContent] = useState<NativeChatComposerNoticeContent | null>(null)
const reportComposerError = useCallback(
(text: string | null, errorText?: string) =>
setContent(text === null ? null : { text, ...(errorText ? { errorText } : {}) }),
[]
)
const composerError = useMemo(
() => (content ? { ...content, onDismiss: () => setContent(null) } : null),
[content]
)
return { composerError, reportComposerError }
}
@@ -2,7 +2,7 @@ import { useCallback, useLayoutEffect, useMemo, useRef } from 'react'
import { nativeChatAttachmentOwnerUnchanged } from './native-chat-resolved-path-ownership'
import { assertClipboardTextWithinLimit } from '../../../../shared/clipboard-text'
import { translate } from '@/i18n/i18n'
import { extractIpcErrorMessage } from '@/lib/ipc-error'
import { setNativeChatPasteFailure } from './native-chat-composer-notice'
import type { AgentType } from '../../../../shared/agent-status-types'
import { NATIVE_CHAT_CONTEXT_PASTE_MAX_BYTES } from './native-chat-composer-target'
import { nativeChatPasteUnavailableNotice } from '@/lib/native-chat-paste-request'
@@ -34,7 +34,7 @@ export type UseNativeChatComposerPasteArgs = {
dropPendingImageAttachment: (id: string) => void
insertTypedText: (text: string) => boolean
setCaret: (caret: number) => void
setNotice: (notice: string | null) => void
setNotice: (notice: string | null, errorText?: string) => void
}
/** Owners whose attachment path is a file this client can write right now. */
@@ -111,11 +111,10 @@ export function useNativeChatComposerPaste({
// A failed save must be visible: over SSH it fails whenever the
// connection drops, and a silent no-op reads as a broken paste.
if (canPaste()) {
setNotice(
extractIpcErrorMessage(
error,
translate('components.native-chat.composer.imagePasteFailed', 'Image paste failed.')
)
setNativeChatPasteFailure(
setNotice,
error,
translate('components.native-chat.composer.imagePasteFailed', 'Image paste failed.')
)
}
return { status: 'failed' }
@@ -179,7 +178,7 @@ export function useNativeChatComposerPaste({
showPasteUnavailable()
}
} catch (error) {
setNotice(extractIpcErrorMessage(error, 'Paste failed.'))
setNativeChatPasteFailure(setNotice, error)
}
}
if (!imageFile) {
@@ -262,7 +261,7 @@ export function useNativeChatComposerPaste({
})
.catch((error) => {
if (canPaste()) {
setNotice(extractIpcErrorMessage(error, 'Paste failed.'))
setNativeChatPasteFailure(setNotice, error)
}
return null
})
@@ -84,7 +84,7 @@ export function answerNativeChatCommandInComposer(args: {
setDraft: (value: string) => void
setCaret: Dispatch<SetStateAction<number>>
clearSkillOrigin: () => void
setNotice: Dispatch<SetStateAction<string | null>>
setNotice: (notice: string | null) => void
}): boolean {
const command = args.draft.trim()
const answer =
@@ -38,7 +38,7 @@ export function useNativeChatPickerCommandDispatch(args: {
setActiveSuggestion: Dispatch<SetStateAction<number>>
clearSkillOrigin: () => void
clearImageAttachments: () => void
setNotice: Dispatch<SetStateAction<string | null>>
setNotice: (notice: string | null) => void
}): (command: Extract<NativeChatPickerItem, { kind: 'command' }>) => void {
const {
agent,
@@ -48,7 +48,7 @@ export function useNativeChatPtyComposerSend(args: {
setCaret: Dispatch<SetStateAction<number>>
clearSkillOrigin: () => void
clearImageAttachments: () => void
setNotice: Dispatch<SetStateAction<string | null>>
setNotice: (notice: string | null) => void
}): () => void {
return useCallback(() => {
const text = args.draft
@@ -128,3 +128,21 @@ describe('reports the sends that bring the latest into view', () => {
expect(onSubmitted).not.toHaveBeenCalled()
})
})
describe("says a failed send in Orca's words, with the error it hit apart", () => {
it('keeps a main-process failure apart from the words saying the message was not sent', async () => {
const { send, structuredTransport } = harness('claude')
vi.mocked(structuredTransport.send).mockImplementation(() => {
throw new Error(
"Error invoking remote method 'agentSession:send': Error: connect ECONNREFUSED /tmp/a.sock"
)
})
send('hello', [])
await vi.waitFor(() =>
expect(structuredTransport.onError).toHaveBeenCalledWith(
'Your message was not sent.',
'connect ECONNREFUSED /tmp/a.sock'
)
)
})
})
@@ -12,6 +12,9 @@ import { pushHistory, type HistoryState } from './native-chat-composer-state'
import type { NativeChatStructuredComposerTransport } from './native-chat-composer-types'
import type { NativeChatComposerImageAttachment } from './NativeChatComposerField'
import { nativeChatAttachImagesAgainReason } from './native-chat-image-reattach'
import { nativeChatNoticeFromError } from './native-chat-composer-notice'
import { agentSessionWriteNoticeText } from './agent-session-write-notice-text'
import { translate } from '@/i18n/i18n'
import {
readNativeChatComposerDraft,
updateNativeChatComposerDraft
@@ -77,7 +80,12 @@ export function useNativeChatStructuredComposerSend({
return
}
if (attachments.length > 0 && hostCommand) {
structuredTransport.onError('Remove attachments before using a chat-session command.')
structuredTransport.onError(
translate(
'components.native-chat.composer.commandAttachmentsUnsupported',
'Remove attachments before using a chat-session command.'
)
)
return
}
// A conversation command reveals at the press, not after its round trip; options move nothing.
@@ -119,9 +127,13 @@ export function useNativeChatStructuredComposerSend({
setCaret(composing ? 0 : left.text.length)
clearSkillOrigin()
})
.catch((error) =>
structuredTransport.onError(error instanceof Error ? error.message : String(error))
)
.catch((error) => {
const notice = nativeChatNoticeFromError(
error,
agentSessionWriteNoticeText([hostCommand ? 'notDoneCommand' : 'notDoneSend'])
)
structuredTransport.onError(notice.text, notice.errorText)
})
},
[
agent,
@@ -17,7 +17,7 @@ export function useNativeChatStructuredComposerTransport(args: {
worktreeId: string | undefined
optionPickerRequest: NativeChatOptionPickerRequest | null
setOptionPickerRequest: Dispatch<SetStateAction<NativeChatOptionPickerRequest | null>>
onError: (message: string | null) => void
onError: NativeChatStructuredComposerTransport['onError']
onSubmitted: () => void
queuedMessages: Pick<StructuredAgentSessionQueuedMessagesController, 'queueHold' | 'queueResume'>
}): NativeChatStructuredComposerTransport {
+6 -2
View File
@@ -17947,7 +17947,9 @@
"plan": "Plan",
"historyRepaired": "Part of this chat's history couldn't be loaded.",
"historyItemTooLarge": "This part of the chat was too large to show.",
"stoppedBeforeStart": "Stopped before the agent started"
"stoppedBeforeStart": "Stopped before the agent started",
"dismiss": "Dismiss",
"copyError": "Copy error"
},
"composer": {
"imageUnsupported": "Image paste is not supported for this agent.",
@@ -18028,7 +18030,9 @@
"workspaceAttachmentMismatch": "Files can only be attached to their source workspace.",
"attachmentOwnerChanged": "This workspace changed hosts while attaching — drop the files again.",
"attachmentUnreadable": "Couldn't read the dropped files.",
"tooManyAttachments": "Attach {{value0}} or fewer files at a time."
"tooManyAttachments": "Attach {{value0}} or fewer files at a time.",
"pasteFailed": "Paste failed.",
"commandAttachmentsUnsupported": "Remove attachments before using a chat-session command."
},
"tool": {
"exitCode": "exit {{value0}}",
+6 -2
View File
@@ -17765,7 +17765,9 @@
"workspaceAttachmentMismatch": "Los archivos solo se pueden adjuntar a su espacio de trabajo de origen.",
"attachmentOwnerChanged": "Este espacio de trabajo cambió de host durante la conexión; vuelve a soltar los archivos.",
"attachmentUnreadable": "No se pudieron leer los archivos soltados.",
"tooManyAttachments": "Adjunta {{value0}} archivos o menos a la vez."
"tooManyAttachments": "Adjunta {{value0}} archivos o menos a la vez.",
"pasteFailed": "Error al pegar.",
"commandAttachmentsUnsupported": "Quita los archivos adjuntos antes de usar un comando de la sesión de chat."
},
"tool": {
"running": "Ejecutando…",
@@ -18084,7 +18086,9 @@
"plan": "Plan",
"historyRepaired": "No se pudo cargar parte del historial de este chat.",
"historyItemTooLarge": "Esta parte del chat era demasiado grande para mostrarla.",
"stoppedBeforeStart": "Detenido antes de que el agente empezara"
"stoppedBeforeStart": "Detenido antes de que el agente empezara",
"dismiss": "Descartar",
"copyError": "Copiar error"
},
"providerFrame": {
"byteLength": "{{value0}} bytes"
+6 -2
View File
@@ -17866,7 +17866,9 @@
"workspaceAttachmentMismatch": "Les fichiers ne peuvent être joints qu'à leur espace de travail source.",
"attachmentOwnerChanged": "Cet espace de travail a changé d'hôte lors de la connexion : supprimez à nouveau les fichiers.",
"attachmentUnreadable": "Impossible de lire les fichiers supprimés.",
"tooManyAttachments": "Joignez {{value0}} fichiers maximum à la fois."
"tooManyAttachments": "Joignez {{value0}} fichiers maximum à la fois.",
"pasteFailed": "Échec du collage.",
"commandAttachmentsUnsupported": "Retirez les pièces jointes avant d'utiliser une commande de session de chat."
},
"tool": {
"running": "En cours…",
@@ -18072,7 +18074,9 @@
"plan": "Plan",
"historyRepaired": "Une partie de l'historique de ce chat n'a pas pu être chargée.",
"historyItemTooLarge": "Cette partie du chat était trop volumineuse pour être affichée.",
"stoppedBeforeStart": "Arrêté avant le démarrage de l'agent"
"stoppedBeforeStart": "Arrêté avant le démarrage de l'agent",
"dismiss": "Ignorer",
"copyError": "Copier l'erreur"
},
"providerFrame": {
"byteLength": "{{value0}} octets"
+6 -2
View File
@@ -17800,7 +17800,9 @@
"workspaceAttachmentMismatch": "ファイルはソースワークスペースにのみ添付できます。",
"attachmentOwnerChanged": "このワークスペースは接続中にホストを変更しました。ファイルを再度ドロップしてください。",
"attachmentUnreadable": "ドロップされたファイルを読み取れませんでした。",
"tooManyAttachments": "一度に添付できるファイルは {{value0}} 個以下です。"
"tooManyAttachments": "一度に添付できるファイルは {{value0}} 個以下です。",
"pasteFailed": "貼り付けに失敗しました。",
"commandAttachmentsUnsupported": "チャットセッションのコマンドを使う前に添付ファイルを削除してください。"
},
"tool": {
"running": "実行中…",
@@ -18006,7 +18008,9 @@
"plan": "プラン",
"historyRepaired": "このチャットの履歴の一部を読み込めませんでした。",
"historyItemTooLarge": "チャットのこの部分は大きすぎるため表示できません。",
"stoppedBeforeStart": "Agent の開始前に停止しました"
"stoppedBeforeStart": "Agent の開始前に停止しました",
"dismiss": "閉じる",
"copyError": "エラーをコピー"
},
"providerFrame": {
"byteLength": "{{value0}}バイト"
+6 -2
View File
@@ -17800,7 +17800,9 @@
"workspaceAttachmentMismatch": "파일은 소스 워크스페이스에만 첨부할 수 있습니다.",
"attachmentOwnerChanged": "이 워크스페이스는 첨부하는 동안 호스트를 변경했습니다. 파일을 다시 삭제하세요.",
"attachmentUnreadable": "삭제된 파일을 읽을 수 없습니다.",
"tooManyAttachments": "한 번에 {{value0}}개 이하의 파일을 첨부하세요."
"tooManyAttachments": "한 번에 {{value0}}개 이하의 파일을 첨부하세요.",
"pasteFailed": "붙여넣기 실패.",
"commandAttachmentsUnsupported": "채팅 세션 명령을 사용하기 전에 첨부 파일을 제거하세요."
},
"tool": {
"running": "실행 중…",
@@ -18006,7 +18008,9 @@
"plan": "계획",
"historyRepaired": "이 채팅 기록의 일부를 불러올 수 없습니다.",
"historyItemTooLarge": "채팅의 이 부분은 너무 커서 표시할 수 없습니다.",
"stoppedBeforeStart": "에이전트가 시작되기 전에 중지됨"
"stoppedBeforeStart": "에이전트가 시작되기 전에 중지됨",
"dismiss": "닫기",
"copyError": "오류 복사"
},
"providerFrame": {
"byteLength": "{{value0}}바이트"
+6 -2
View File
@@ -17765,7 +17765,9 @@
"workspaceAttachmentMismatch": "文件只能附加到其源工作区。",
"attachmentOwnerChanged": "此工作区在附加时更改了主机 - 再次删除文件。",
"attachmentUnreadable": "无法读取删除的文件。",
"tooManyAttachments": "每次最多附加 {{value0}} 个文件。"
"tooManyAttachments": "每次最多附加 {{value0}} 个文件。",
"pasteFailed": "粘贴失败。",
"commandAttachmentsUnsupported": "使用聊天会话命令前,请先移除附件。"
},
"tool": {
"running": "正在运行…",
@@ -17971,7 +17973,9 @@
"plan": "计划",
"historyRepaired": "此聊天的部分历史记录无法加载。",
"historyItemTooLarge": "聊天的这一部分过大,无法显示。",
"stoppedBeforeStart": "在智能体开始前已停止"
"stoppedBeforeStart": "在智能体开始前已停止",
"dismiss": "关闭",
"copyError": "复制错误"
},
"providerFrame": {
"byteLength": "{{value0}} 字节"