diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc index e5d8b5b2c57..642918a992c 100644 --- a/config/reliability-gates.jsonc +++ b/config/reliability-gates.jsonc @@ -948,7 +948,7 @@ "invariant": "One structured-send operation id causes at most one provider dispatch. A recorded or transport-ambiguous send reuses that id across retry, caller reconnect, client remount, and journal recovery; only a terminal rejection may rotate to a first delivery.", "oracle": "Inject adapter acknowledgement loss, RPC response loss, caller replacement, logical-client close after response, auth recovery with a written request, missing journal submissions, legacy pending rows, stale fences, operation expiry, mobile remount, and durable-journal capacity. Assert one provider dispatch or one operation id for every ambiguous retry, fresh identity only after rejection, and no eviction of ambiguous mobile ids.", "commands": [ - "ORCA_BACKGROUND_LAUNCH=1 pnpm test src/shared/agent-session-operation-ledger.test.ts src/shared/structured-agent-session-send-disposition.test.ts src/main/runtime/agent-session-operation-admission.test.ts src/main/native-chat/agent-session-wire/structured-agent-session-refusal-retry.test.ts src/main/native-chat/agent-session-wire/structured-agent-session-send.test.ts src/main/runtime/orchestration/structured-mailbox-pointer-delivery.test.ts src/main/runtime/orchestration/structured-mailbox-pointer-host.test.ts src/main/runtime/orchestration/structured-pointer-operation-id.test.ts src/renderer/src/components/native-chat/use-structured-agent-session-outbox.test.tsx src/renderer/src/components/native-chat/use-structured-agent-session.test.tsx src/renderer/src/components/native-chat/NativeChatStructuredSessionDelivery.test.tsx src/renderer/src/lib/launch-structured-agent-session.test.ts", + "ORCA_BACKGROUND_LAUNCH=1 pnpm test src/shared/agent-session-operation-ledger.test.ts src/shared/structured-agent-session-send-disposition.test.ts src/main/runtime/agent-session-operation-admission.test.ts src/main/native-chat/agent-session-wire/structured-agent-session-refusal-retry.test.ts src/main/native-chat/agent-session-wire/structured-agent-session-send.test.ts src/main/runtime/orchestration/structured-mailbox-pointer-delivery.test.ts src/main/runtime/orchestration/structured-mailbox-pointer-host.test.ts src/main/runtime/orchestration/structured-pointer-operation-id.test.ts src/renderer/src/components/native-chat/use-structured-agent-session-outbox.test.tsx src/renderer/src/components/native-chat/use-structured-agent-session.test.tsx src/renderer/src/components/native-chat/NativeChatStructuredSessionDelivery.test.tsx src/renderer/src/components/native-chat/NativeChatStructuredSessionDelivery.probe.test.tsx src/renderer/src/lib/launch-structured-agent-session.test.ts", "ORCA_BACKGROUND_LAUNCH=1 pnpm --dir mobile test ../mobile/src/session/mobile-native-chat-image-attachment.test.ts ../mobile/src/session/use-mobile-native-chat-image-attachments.test.ts ../mobile/src/session/mobile-structured-send-operation-journal.test.ts ../mobile/src/session/mobile-structured-send-delivery.test.ts ../mobile/src/session/use-mobile-structured-agent-session-send.test.tsx ../mobile/src/session/use-mobile-structured-agent-session.test.tsx ../mobile/src/transport/mobile-relay-rpc-session.test.ts ../mobile/src/transport/rpc-client-delivery-ambiguity.test.ts ../mobile/src/transport/stable-logical-rpc-client.test.ts" ], "testFiles": [ @@ -963,6 +963,7 @@ "src/renderer/src/components/native-chat/use-structured-agent-session-outbox.test.tsx", "src/renderer/src/components/native-chat/use-structured-agent-session.test.tsx", "src/renderer/src/components/native-chat/NativeChatStructuredSessionDelivery.test.tsx", + "src/renderer/src/components/native-chat/NativeChatStructuredSessionDelivery.probe.test.tsx", "src/renderer/src/lib/launch-structured-agent-session.test.ts", "mobile/src/session/mobile-native-chat-image-attachment.test.ts", "mobile/src/session/use-mobile-native-chat-image-attachments.test.ts", @@ -1033,13 +1034,13 @@ ], "evidenceRuns": [ { - "date": "2026-09-12", + "date": "2026-10-04", "runner": "local", "platform": "macos", - "command": "ORCA_BACKGROUND_LAUNCH=1 pnpm test src/shared/agent-session-operation-ledger.test.ts src/shared/structured-agent-session-send-disposition.test.ts src/main/runtime/agent-session-operation-admission.test.ts src/main/native-chat/agent-session-wire/structured-agent-session-refusal-retry.test.ts src/main/native-chat/agent-session-wire/structured-agent-session-send.test.ts src/main/runtime/orchestration/structured-mailbox-pointer-delivery.test.ts src/main/runtime/orchestration/structured-mailbox-pointer-host.test.ts src/main/runtime/orchestration/structured-pointer-operation-id.test.ts src/renderer/src/components/native-chat/use-structured-agent-session-outbox.test.tsx src/renderer/src/components/native-chat/use-structured-agent-session.test.tsx src/renderer/src/components/native-chat/NativeChatStructuredSessionDelivery.test.tsx src/renderer/src/lib/launch-structured-agent-session.test.ts", + "command": "ORCA_BACKGROUND_LAUNCH=1 pnpm test src/shared/agent-session-operation-ledger.test.ts src/shared/structured-agent-session-send-disposition.test.ts src/main/runtime/agent-session-operation-admission.test.ts src/main/native-chat/agent-session-wire/structured-agent-session-refusal-retry.test.ts src/main/native-chat/agent-session-wire/structured-agent-session-send.test.ts src/main/runtime/orchestration/structured-mailbox-pointer-delivery.test.ts src/main/runtime/orchestration/structured-mailbox-pointer-host.test.ts src/main/runtime/orchestration/structured-pointer-operation-id.test.ts src/renderer/src/components/native-chat/use-structured-agent-session-outbox.test.tsx src/renderer/src/components/native-chat/use-structured-agent-session.test.tsx src/renderer/src/components/native-chat/NativeChatStructuredSessionDelivery.test.tsx src/renderer/src/components/native-chat/NativeChatStructuredSessionDelivery.probe.test.tsx src/renderer/src/lib/launch-structured-agent-session.test.ts", "result": "passed", - "durationSeconds": 22.1, - "summary": "Thirteen focused host, shared, renderer, and orchestration files passed 148 tests." + "durationSeconds": 42.3, + "summary": "Thirteen focused host, shared, renderer, and orchestration files passed 197 tests after the delivery probe tests moved to their own file." }, { "date": "2026-09-12", diff --git a/src/renderer/src/components/native-chat/NativeChatMessageRow.test.tsx b/src/renderer/src/components/native-chat/NativeChatMessageRow.test.tsx index bcecab05bc6..f9e247d1354 100644 --- a/src/renderer/src/components/native-chat/NativeChatMessageRow.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMessageRow.test.tsx @@ -3,7 +3,7 @@ import '@testing-library/jest-dom/vitest' import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' import { afterEach, describe, expect, it, vi } from 'vitest' import type { NativeChatMessage } from '../../../../shared/native-chat-types' -import { MessageRow } from './NativeChatMessageRow' +import { MessageRow, type NativeChatDeliveryNotice } from './NativeChatMessageRow' afterEach(cleanup) @@ -163,8 +163,8 @@ describe('MessageRow send mode', () => { }) }) -describe('a user message that did not go through', () => { - function renderUser(deliveryNotice?: { text: string; onRetry?: () => void }) { +describe('what a user message says about its delivery', () => { + function renderUser(deliveryNotice?: NativeChatDeliveryNotice) { return render( { renderUser() expect(screen.queryByRole('button', { name: 'Retry' })).toBeNull() }) + + // Muted, in the time's place, and shown without hover: a message nothing confirmed yet never + // looks like one that went through. Copy keeps its hover reveal, and the row its height. + it('says quietly that it is still sending in place of its time, with no Retry', () => { + renderUser({ sending: true }) + + const sending = screen.getByText('Sending…') + const copy = screen.getByRole('button', { name: 'Copy message' }) + expect(sending).toHaveClass('text-xs', 'text-muted-foreground') + expect(Array.from(sending.parentElement!.children)).toEqual([copy, sending]) + expect(sending.parentElement).not.toHaveClass('can-hover:opacity-0') + expect(sending.parentElement!.parentElement).toHaveClass('group') + expect(copy).toHaveClass('can-hover:opacity-0', 'group-hover:opacity-100') + expect(screen.queryByRole('time')).toBeNull() + expect(screen.queryByRole('button', { name: 'Retry' })).toBeNull() + }) + + it('keeps the same row when the message is confirmed, with the time back in its place', () => { + const { rerender } = renderUser({ sending: true }) + const meta = screen.getByText('Sending…').parentElement + rerender( + + ) + expect(screen.queryByText('Sending…')).toBeNull() + expect(screen.getByRole('time').parentElement).toBe(meta) + expect(meta).toHaveClass('can-hover:opacity-0') + }) }) diff --git a/src/renderer/src/components/native-chat/NativeChatMessageRow.tsx b/src/renderer/src/components/native-chat/NativeChatMessageRow.tsx index 234934664f4..10c8a68de07 100644 --- a/src/renderer/src/components/native-chat/NativeChatMessageRow.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMessageRow.tsx @@ -28,12 +28,45 @@ import type { } from './native-chat-subagent-sections' import type { RuntimeFileOperationArgs } from '@/runtime/runtime-file-client' -/** What a user message says under it when it did not go through, with its own Retry when the - * surface can send it again. */ -export type NativeChatDeliveryNotice = { - text: string - onRetry?: () => void - onDismiss?: () => void +/** What a user message says about its delivery: that nothing has confirmed it yet, quietly in + * place of its time, or that it did not go through, with its own Retry when the surface can send + * it again. */ +export type NativeChatDeliveryNotice = + | { sending: true; text?: never; onRetry?: never; onDismiss?: never } + | { sending?: never; text: string; onRetry?: () => void; onDismiss?: () => void } + +const USER_META_REVEAL = + 'transition-opacity can-hover:pointer-events-none can-hover:opacity-0 group-hover:pointer-events-auto group-hover:opacity-100 [.group:has(:focus-visible)_&]:pointer-events-auto [.group:has(:focus-visible)_&]:opacity-100' + +/** Under a user message: copy + timestamp, revealed together like the agent controls row. Until + * confirmed, a quiet "Sending…" stays visible in the time's place and copy keeps its own reveal, + * so the row keeps its height when it clears. Image-only prompts have no text to copy. */ +function UserMessageMeta({ + markdown, + timestamp, + sending +}: { + markdown: string + timestamp: number | null + sending: boolean +}): React.JSX.Element | null { + if (!markdown && timestamp === null && !sending) { + return null + } + return ( +
+ {markdown ? ( + + ) : null} + {sending ? ( + + {translate('components.native-chat.messageSending', 'Sending…')} + + ) : ( + + )} +
+ ) } /** One message: its prose first, then a collapsible run folding all of the @@ -178,15 +211,12 @@ export const MessageRow = memo(function MessageRow({ {translate('components.native-chat.goal.sentAsGoal', 'Sent as goal')} ) : null} - {/* Copy + timestamp reveal together, mirroring the agent controls row. - Image-only prompts have no text to copy, so the button is omitted. */} - {markdown || message.timestamp !== null ? ( -
- {markdown ? : null} - -
- ) : null} - {deliveryNotice ? ( + + {deliveryNotice?.text !== undefined ? (
{deliveryNotice.text} {deliveryNotice.onDismiss ? ( diff --git a/src/renderer/src/components/native-chat/NativeChatStructuredSession.test-harness.tsx b/src/renderer/src/components/native-chat/NativeChatStructuredSession.test-harness.tsx index ee876868c35..89fda3f7f31 100644 --- a/src/renderer/src/components/native-chat/NativeChatStructuredSession.test-harness.tsx +++ b/src/renderer/src/components/native-chat/NativeChatStructuredSession.test-harness.tsx @@ -1,3 +1,4 @@ +import { act } from '@testing-library/react' import { forwardRef, useImperativeHandle, useRef } from 'react' import { vi } from 'vitest' import type { AgentJournalRenderItem } from '../../../../shared/agent-session-journal-types' @@ -27,7 +28,8 @@ function absent(): T | undefined { return undefined } -/** Stands in for the transcript: renders only each message's delivery notice and its Retry. */ +/** Stands in for the transcript: renders only each message's delivery notice and its Retry, or the + * row's quiet "Sending…" while nothing has confirmed it. */ export function DeliveryNoticesMock({ notices }: { @@ -37,7 +39,7 @@ export function DeliveryNoticesMock({
{[...(notices ?? [])].map(([id, notice]) => (
- {notice.text} + {notice.sending ? 'Sending…' : notice.text} {notice.onRetry ? (