diff --git a/src/renderer/src/components/native-chat/NativeChatTextAttachmentConsumers.test.tsx b/src/renderer/src/components/native-chat/NativeChatTextAttachmentConsumers.test.tsx
deleted file mode 100644
index 65c1e8cffc2..00000000000
--- a/src/renderer/src/components/native-chat/NativeChatTextAttachmentConsumers.test.tsx
+++ /dev/null
@@ -1,502 +0,0 @@
-// @vitest-environment happy-dom
-import { useRef, useMemo } from 'react'
-
-import { NativeChatMarkdownImageOwnerContext } from './native-chat-markdown-image-owner'
-
-import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
-import { afterEach, expect, it, vi } from 'vitest'
-
-import { NativeChatPromptEditor } from './NativeChatPromptEditor'
-import {
- NativeChatTextAttachmentScope,
- NativeChatTextAttachments
-} from './NativeChatTextAttachments'
-import { useNativeChatPastedTextFile } from './use-native-chat-pasted-text-file'
-import {
- insertNativeChatPastedText,
- type NativeChatComposerInput
-} from './native-chat-composer-input'
-import { promptEditor, promptValue } from './native-chat-prompt-editor.test-support'
-import {
- clearNativeChatComposerDraftsForTests,
- readNativeChatComposerDraft,
- updateNativeChatComposerDraft
-} from './native-chat-composer-draft-store'
-import { resolveNativeChatPastedText } from './native-chat-composer-pasted-text-references'
-import { nativeChatPastedTextOperations } from './native-chat-pasted-text-reference'
-
-const mocks = vi.hoisted(() => ({
- save: vi.fn(),
- rpc: vi.fn(),
- write: vi.fn(),
- resolved: false,
- start: vi.fn(),
- append: vi.fn(),
- finish: vi.fn(),
- cancel: vi.fn()
-}))
-vi.mock('@/runtime/runtime-rpc-client', () => ({ callRuntimeRpc: mocks.rpc }))
-vi.mock('./native-chat-visual-markdown-extension', () => ({
- useNativeChatVisualMarkdownExtension: () => undefined
-}))
-vi.mock('./use-native-chat-file-link-existence', () => ({
- useNativeChatFileLinkExists: () => undefined
-}))
-vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }))
-vi.mock('./NativeChatAutocompleteMenus', () => ({
- NativeChatMentionMenu: () => null,
- NativeChatPickerMenu: () => null
-}))
-vi.mock('./use-native-chat-skills', () => ({
- useNativeChatSkills: () => ({ status: 'ready', skills: [], error: null, retry: () => {} })
-}))
-const scope = 'agent-session:consumer35636'
-const path =
- '/host/agent-session-attachments/12345678-1234-1234-1234-123456789abc/orca-paste-123-abc.txt'
-let paste: (text: string) => boolean
-function Composer({ initialValue = '' }: { initialValue?: string }) {
- const ref = useRef(null)
- paste = useNativeChatPastedTextFile({
- scopeKey: scope,
- scopeEditor: () => ref.current,
- resolveAttachmentOwner: () => ({ kind: 'local' }),
- insertTypedText: (text) => insertNativeChatPastedText(ref.current, text),
- setNotice: () => {}
- })
- const resolved = mocks.resolved
- const owner = useMemo(
- () => ({
- runtimeContext: resolved
- ? { target: { kind: 'local' as const }, worktreeId: 'w', worktreePath: '/repo' }
- : null,
- isVisible: true,
- fileLinkContext: null
- }),
- [resolved]
- )
- return (
-
-
- {}}
- onSelect={() => {}}
- />
-
-
- )
-}
-async function openSource() {
- fireEvent.click(await screen.findByRole('button', { name: 'Pasted text' }))
- await waitFor(() =>
- expect(screen.getByRole('button', { name: 'Copy contents' }).hasAttribute('disabled')).toBe(
- false
- )
- )
-}
-afterEach(() => {
- cleanup()
- clearNativeChatComposerDraftsForTests()
- nativeChatPastedTextOperations.clear()
- vi.clearAllMocks()
-})
-
-it('opens draft and sent sources, exports exact bytes and scrolls a giant-line paste to the end', async () => {
- const text = `\ufeff${Array.from({ length: 5000 }, (_, index) => `🙂paste${index}\r\n`).join('')}END35636`
- const ending = Promise.withResolvers()
- let heldRange = false
- let viewportHeight = 600
- let viewportWidth = 390
- let horizontalOccupation = 0
- let rowHeight = 20
- const nativeRect = HTMLElement.prototype.getBoundingClientRect
- vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function (
- this: HTMLElement
- ) {
- return this.classList.contains('invisible')
- ? new DOMRect(0, 0, 8, rowHeight)
- : nativeRect.call(this)
- })
- const positions = new WeakMap()
- const horizontal = new WeakMap()
- const scrollWrites: HTMLElement[] = []
- let deliverScroll = true
- const later = Promise.withResolvers()
- const laterStarted = Promise.withResolvers()
- let holdGiant = false
- const arrayBuffer = Blob.prototype.arrayBuffer
- vi.spyOn(Blob.prototype, 'arrayBuffer').mockImplementation(async function (this: Blob) {
- if (holdGiant) {
- laterStarted.resolve()
- await later.promise
- }
- return arrayBuffer.call(this)
- })
- vi.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockImplementation(function (
- this: HTMLElement
- ) {
- return this.classList.contains('scrollbar-editor')
- ? Number.parseFloat(this.style.width) || viewportWidth
- : viewportWidth
- })
- vi.spyOn(HTMLElement.prototype, 'scrollWidth', 'get').mockImplementation(function (
- this: HTMLElement
- ) {
- const track = this.lastElementChild
- return Math.max(
- this.clientWidth,
- track instanceof HTMLElement ? Number.parseFloat(track.style.minWidth) || 0 : 0,
- ...(track instanceof HTMLElement && track.style.overflow === 'clip'
- ? []
- : Array.from(this.querySelectorAll('[data-text-segment]')).map(
- (row) => Array.from(row.textContent || '').length * 8
- ))
- )
- })
- vi.spyOn(HTMLElement.prototype, 'scrollLeft', 'get').mockImplementation(function (
- this: HTMLElement
- ) {
- const left = Math.min(horizontal.get(this) || 0, this.scrollWidth - this.clientWidth)
- horizontal.set(this, left)
- return left
- })
- vi.spyOn(HTMLElement.prototype, 'scrollLeft', 'set').mockImplementation(function (
- this: HTMLElement,
- value
- ) {
- horizontal.set(this, Math.max(0, Math.min(value, this.scrollWidth - this.clientWidth)))
- })
- const measurements = new Set<() => void>()
- vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockImplementation(function (
- this: HTMLElement
- ) {
- return this.classList.contains('scrollbar-editor')
- ? this.offsetHeight - (this.scrollWidth > this.clientWidth ? horizontalOccupation : 0)
- : viewportHeight
- })
- vi.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockImplementation(function (
- this: HTMLElement
- ) {
- return Number.parseFloat(this.style.height) || viewportHeight
- })
- vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockImplementation(function (
- this: HTMLElement
- ) {
- const track = this.lastElementChild
- return Math.max(
- this.clientHeight,
- track instanceof HTMLElement ? Number.parseFloat(track.style.height) || 0 : 0,
- ...(track instanceof HTMLElement && track.style.overflow === 'clip'
- ? []
- : Array.from(this.querySelectorAll('[data-text-segment]')).map(
- (row) => Number.parseFloat(row.style.top) + Number.parseFloat(row.style.height)
- ))
- )
- })
- vi.spyOn(HTMLElement.prototype, 'scrollTop', 'get').mockImplementation(function (
- this: HTMLElement
- ) {
- const top = Math.min(positions.get(this) || 0, this.scrollHeight - this.clientHeight)
- positions.set(this, top)
- return top
- })
- vi.spyOn(HTMLElement.prototype, 'scrollTop', 'set').mockImplementation(function (
- this: HTMLElement,
- value
- ) {
- scrollWrites.push(this)
- const top = Math.max(0, Math.min(value, this.scrollHeight - this.clientHeight))
- if (top !== this.scrollTop) {
- positions.set(this, top)
- if (deliverScroll) {
- queueMicrotask(() => this.dispatchEvent(new Event('scroll')))
- }
- }
- })
- vi.stubGlobal(
- 'ResizeObserver',
- class {
- readonly notify: () => void
- constructor(callback: ResizeObserverCallback) {
- this.notify = () => callback([], this)
- measurements.add(this.notify)
- }
- observe() {}
- unobserve() {}
- disconnect() {
- measurements.delete(this.notify)
- }
- }
- )
- try {
- mocks.start.mockResolvedValue({ canceled: false, transferId: 'output' })
- mocks.finish.mockResolvedValue({ canceled: false })
- mocks.resolved = false
- const upload = Promise.withResolvers()
- mocks.save.mockReturnValue(upload.promise)
- mocks.rpc.mockImplementation(async (_target, method, request) => {
- if (method === 'status.get') {
- return { capabilities: ['native-chat.text-attachment-read.v1'] }
- }
- const bytes = Buffer.from(text)
- if (request.offset > 0) {
- heldRange = true
- await ending.promise
- }
- const chunk = bytes.subarray(request.offset, request.offset + request.length)
- return {
- snapshot: 'a'.repeat(64),
- size: bytes.length,
- bytesRead: chunk.length,
- contentBase64: chunk.toString('base64'),
- eof: request.offset + chunk.length === bytes.length
- }
- })
- Object.defineProperty(window, 'api', {
- configurable: true,
- value: {
- ui: { savePastedTextAsFile: mocks.save, writeClipboardText: mocks.write },
- fs: {
- startDownloadedFile: mocks.start,
- appendDownloadedFileChunk: mocks.append,
- finishDownloadedFile: mocks.finish,
- cancelDownloadedFile: mocks.cancel
- }
- }
- })
- let view = render()
- await act(async () => {
- expect(paste(text)).toBe(true)
- })
- expect(screen.getByRole('button', { name: 'Pasted text' })).toBeTruthy()
- await openSource()
- const dialog = screen.getByRole('dialog', { name: 'Pasted text' })
- expect(dialog.getAttribute('data-slot')).toBe('dialog-content')
- expect(document.getElementById(dialog.getAttribute('aria-describedby')!)?.textContent).toBe(
- `${Buffer.byteLength(text).toLocaleString('en-US')} bytes`
- )
- await waitFor(() => expect(dialog.contains(document.activeElement)).toBe(true))
- fireEvent.click(screen.getByRole('button', { name: 'Copy contents' }))
- await waitFor(() => expect(mocks.write).toHaveBeenCalledWith(text))
- expect(mocks.rpc).not.toHaveBeenCalled()
- fireEvent.keyDown(document.activeElement!, { key: 'Escape' })
- await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull())
- mocks.resolved = true
- await act(async () => upload.resolve(path))
- view.rerender()
- expect(nativeChatPastedTextOperations.size).toBe(0)
- fireEvent.click(screen.getByRole('button', { name: 'Pasted text' }))
- await waitFor(() =>
- expect(mocks.rpc).toHaveBeenCalledWith(
- expect.anything(),
- 'agentSessionAttachment.readTextChunk',
- expect.objectContaining({ path }),
- expect.anything()
- )
- )
- const measured = document.querySelector('.scrollbar-editor')
- if (!(measured instanceof HTMLElement)) {
- throw new Error('Expected real committed text scroller')
- }
- await waitFor(() =>
- expect(measured.querySelectorAll('[data-text-segment]').length).toBeGreaterThan(0)
- )
- fireEvent.keyDown(measured, { key: 'End' })
- await waitFor(() => expect(heldRange).toBe(true))
- await act(async () => ending.resolve())
- await waitFor(() => {
- expect(measured.textContent).toContain('END35636')
- expect(measured.scrollTop + measured.clientHeight).toBe(measured.scrollHeight)
- })
- const oldEnd = measured.scrollTop
- viewportHeight = 3000
- expect.soft(measured.scrollTop).toBe(oldEnd)
- act(() => measurements.forEach((notify) => notify()))
- await waitFor(() =>
- expect(measured.scrollTop + measured.clientHeight).toBe(measured.scrollHeight)
- )
- fireEvent.keyDown(measured, { key: 'Home' })
- await waitFor(() => {
- expect(measured.scrollTop).toBe(0)
- expect(measured.querySelector('[data-text-segment]')?.textContent).toContain('🙂paste0')
- })
- fireEvent.keyDown(measured, { key: 'PageDown' })
- await waitFor(() => {
- expect(measured.querySelector('[data-text-segment]')?.textContent).toBeTruthy()
- expect(measured.querySelector('[data-text-segment]')?.textContent).not.toContain('🙂paste0')
- })
- const visible = measured.querySelector('[data-text-segment]')?.textContent
- viewportHeight = 400
- rowHeight = 24
- act(() => measurements.forEach((notify) => notify()))
- await waitFor(() =>
- expect(measured.querySelector('[data-text-segment]')?.textContent).toBe(visible)
- )
- fireEvent.keyDown(measured, { key: 'Home' })
- await waitFor(() => expect(measured.scrollTop).toBe(0))
- fireEvent.click(screen.getByRole('button', { name: 'Close' }))
- await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull())
- const editor = promptEditor(screen.getByRole('textbox'))
- const token = promptValue(screen.getByRole('textbox')).trim()
- act(() => {
- editor.commands.selectAll()
- editor.commands.deleteSelection()
- })
- expect(screen.queryByRole('button', { name: 'Pasted text' })).toBeNull()
- await act(async () => {
- editor.commands.undo()
- })
- expect(screen.getByRole('button', { name: 'Pasted text' })).toBeTruthy()
- const saved = structuredClone(readNativeChatComposerDraft(scope))
- expect(saved.text).toContain(token)
- const sent = resolveNativeChatPastedText(scope, token)
- expect(sent).toBe(`@${path}`)
- view.unmount()
- clearNativeChatComposerDraftsForTests()
- updateNativeChatComposerDraft(
- scope,
- { text: saved.text, document: saved.document, pastedTextFiles: saved.pastedTextFiles },
- 'immediate'
- )
- expect(readNativeChatComposerDraft(scope).text).toBe(saved.text)
- view = render(
- {content}}
- />
- )
- await openSource()
- fireEvent.click(screen.getByRole('button', { name: 'Copy contents' }))
- await waitFor(() => expect(mocks.write).toHaveBeenCalledTimes(2))
- expect(mocks.write.mock.calls[1]).toEqual([text])
- await waitFor(() =>
- expect(screen.getByRole('button', { name: 'Save file' }).hasAttribute('disabled')).toBe(false)
- )
- fireEvent.click(screen.getByRole('button', { name: 'Save file' }))
- await waitFor(() => expect(mocks.finish).toHaveBeenCalledOnce())
- expect(
- Buffer.concat(
- mocks.append.mock.calls.map(([args]) => Buffer.from(args.contentBase64, 'base64'))
- ).equals(Buffer.from(text))
- ).toBe(true)
- view.unmount()
- const giant = `${'x'.repeat(25 * 1024 * 1024)}TAIL`
- mocks.resolved = false
- const heldUpload = Promise.withResolvers()
- mocks.save.mockReturnValueOnce(heldUpload.promise)
- viewportHeight = 600
- view = render()
- await act(async () => {
- expect(paste(giant)).toBe(true)
- })
- fireEvent.click(screen.getByRole('button', { name: 'Pasted text' }))
- await waitFor(() =>
- expect(screen.getByRole('button', { name: 'Copy contents' }).hasAttribute('disabled')).toBe(
- true
- )
- )
- const scroller = document.querySelector('.scrollbar-editor')
- if (!(scroller instanceof HTMLElement)) {
- throw new Error('Expected real text scroller')
- }
- await waitFor(() =>
- expect(scroller.querySelectorAll('[data-text-segment]').length).toBeGreaterThan(0)
- )
- const track = scroller.lastElementChild
- if (!(track instanceof HTMLElement)) {
- throw new Error('Expected virtual track')
- }
- expect(Number.parseFloat(track.style.height)).toBeLessThan(8_000_000)
- scroller.scrollLeft = 500
- holdGiant = true
- await act(async () => {
- scroller.scrollTop = 4000
- })
- await laterStarted.promise
- expect.soft(scroller.scrollLeft).toBe(500)
- deliverScroll = false
- scroller.scrollTop += 80
- const liveTop = scroller.scrollTop
- const writes = scrollWrites.filter((node) => node === scroller).length
- holdGiant = false
- await act(async () => later.resolve())
- await waitFor(() =>
- expect(scroller.querySelectorAll('[data-text-segment]').length).toBeGreaterThan(0)
- )
- expect.soft(scrollWrites.filter((node) => node === scroller).length).toBe(writes)
- expect.soft(scroller.scrollTop).toBe(liveTop)
- expect.soft(scroller.scrollLeft).toBe(500)
- deliverScroll = true
- fireEvent.scroll(scroller)
-
- fireEvent.scroll(scroller, {
- target: { scrollTop: Number.parseFloat(track.style.height) - 600 }
- })
- await waitFor(() => expect(scroller.textContent).toContain('TAIL'))
- expect(scroller.querySelectorAll('[data-text-segment]').length).toBeLessThanOrEqual(
- Math.ceil(viewportHeight / rowHeight) + 4
- )
- fireEvent.scroll(scroller, { target: { scrollTop: 0 } })
- await waitFor(() =>
- expect(scroller.querySelector('[data-text-segment]')?.textContent).toBe('x'.repeat(256))
- )
- viewportHeight = 3000
- rowHeight = 10
- act(() => measurements.forEach((measure) => measure()))
- await waitFor(() => {
- const rows = scroller.querySelectorAll('[data-text-segment]')
- expect(rows.length).toBeGreaterThanOrEqual(Math.ceil(viewportHeight / rowHeight))
- expect(rows.length).toBeLessThanOrEqual(Math.ceil(viewportHeight / rowHeight) + 4)
- expect(scroller.scrollTop).toBe(0)
- })
- fireEvent.click(screen.getByRole('button', { name: 'Close' }))
- await act(async () => heldUpload.resolve(path))
- view.unmount()
- viewportHeight = 600
- viewportWidth = 400
- rowHeight = 20
- horizontalOccupation = 6
- const dense = `HEAD${'X'.repeat(56)}\n${'\n'.repeat(50 * 1024 * 1024 - 61)}`
- mocks.save.mockReturnValueOnce(new Promise(() => {}))
- view = render()
- await act(async () => {
- expect(paste(dense)).toBe(true)
- })
- fireEvent.click(screen.getByRole('button', { name: 'Pasted text' }))
- const classic = document.querySelector('.scrollbar-editor')
- if (!(classic instanceof HTMLElement)) {
- throw new Error('Expected native classic-scrollbar host')
- }
- await waitFor(() =>
- expect(classic.querySelector('[data-text-segment]')?.textContent).toContain('HEAD')
- )
- act(() => measurements.forEach((notify) => notify()))
- // Visit real source blocks through the shipping wheel/demand/read path, without a prefilled index.
- for (let block = 1; block < 100; block++) {
- await act(async () => {
- fireEvent.wheel(classic, { deltaY: (block === 1 ? 15292 : 15352) * rowHeight })
- })
- }
- fireEvent.keyDown(classic, { key: 'End' })
- await waitFor(() => expect(classic.scrollHeight).toBe(8_000_593))
- await waitFor(() => expect(classic.scrollTop).toBe(7_999_999))
- const classicEnd = classic.scrollTop
- viewportWidth = 650
- expect.soft(classic.scrollTop).toBe(classicEnd)
- act(() => measurements.forEach((notify) => notify()))
- await waitFor(() => expect(classic.scrollTop + classic.clientHeight).toBe(classic.scrollHeight))
- view.unmount()
- } finally {
- ending.resolve()
- later.resolve()
- cleanup()
- vi.restoreAllMocks()
- vi.unstubAllGlobals()
- }
-})
diff --git a/src/renderer/src/components/native-chat/NativeChatTextAttachments.visibility.test.tsx b/src/renderer/src/components/native-chat/NativeChatTextAttachments.visibility.test.tsx
deleted file mode 100644
index 44df2f93609..00000000000
--- a/src/renderer/src/components/native-chat/NativeChatTextAttachments.visibility.test.tsx
+++ /dev/null
@@ -1,293 +0,0 @@
-// @vitest-environment happy-dom
-import type { ReactNode } from 'react'
-import type { TerminalActivationController } from '../use-terminal-activation-actions'
-import type { NativeChatStructuredViewProps } from './native-chat-view-types'
-import { useContext, useState } from 'react'
-import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
-import { afterEach, beforeEach, expect, it, vi } from 'vitest'
-import { useAppStore } from '@/store'
-import { registerTabGroupBody } from '../tab-group/tab-group-body-geometry'
-import { useTerminalKeyboardShortcuts } from '../use-terminal-keyboard-shortcuts'
-import { useTerminalWorkspaceFoundation } from '../use-terminal-workspace-foundation'
-
-import StructuredAgentSessionPaneOverlayLayer from './StructuredAgentSessionPaneOverlayLayer'
-import {
- NativeChatTextAttachmentScope,
- NativeChatTextAttachmentOpener
-} from './NativeChatTextAttachments'
-import { nativeChatFanoutFixture } from './native-chat-workspace-test-fixtures'
-import { getDefaultSettings } from '../../../../shared/constants'
-
-const mocks = vi.hoisted(() => ({
- rpc: vi.fn(),
- write: vi.fn(),
- start: vi.fn(),
- append: vi.fn(),
- finish: vi.fn(),
- cancel: vi.fn(),
- fallback: false,
- web: false
-}))
-vi.mock('@/runtime/runtime-rpc-client', () => ({ callRuntimeRpc: mocks.rpc }))
-vi.mock('@/lib/web-client-location', () => ({ isWebClientLocation: () => mocks.web }))
-vi.mock('sonner', () => ({ toast: { success: vi.fn() } }))
-// Keep the production overlay, NativeChatView and its visibility provider; agent execution is outside this UI contract.
-vi.mock('./NativeChatStructuredSession', () => ({
- NativeChatStructuredSession: (props: NativeChatStructuredViewProps) =>
-}))
-vi.mock('./NativeChatPaneFileDropSurface', () => ({
- NativeChatPaneFileDropSurface: ({ children }: { children: ReactNode }) => <>{children}>
-}))
-const path = '/host/agent-session-attachments/12345678-1234-1234-1234-123456789abc/log.txt'
-const bytes = Buffer.from('selected source')
-const chunk = () => ({
- snapshot: 'a'.repeat(64),
- size: bytes.length,
- contentBase64: bytes.toString('base64'),
- bytesRead: bytes.length,
- eof: true
-})
-const ws = 'folder:workspace-0'
-const initial = useAppStore.getInitialState()
-let unregister: () => void
-let anchor: HTMLElement
-
-function Editor({ tabId }: { tabId: string }) {
- const [draft, setDraft] = useState('')
- const open = useContext(NativeChatTextAttachmentOpener)
- return (
- <>
- setDraft(event.target.value)}
- />
-
- >
- )
-}
-function ChatBody({ tabId }: NativeChatStructuredViewProps) {
- return (
-
-
-
- )
-}
-function Host() {
- const visible = useAppStore(
- (state) => state.activeWorktreeId === ws && state.activeView === 'terminal'
- )
- const controller: TerminalActivationController = {
- ...useAppStore.getState(),
- ...useTerminalWorkspaceFoundation(),
- activeWorktreeId: ws,
- keybindings: {},
- mobileEmulatorEnabled: false,
- terminalShortcutPolicy: 'orca-first',
- tabBarOrder: undefined,
- activityTerminalPortals: [],
- foregroundTerminalTabIds: [],
- tabs: [],
- titlebarTabsTarget: null,
- worktreeFiles: [],
- worktreeBrowserTabs: [],
- worktreeClientHostedBrowserRows: [],
- getEffectiveLayoutForWorktree: vi.fn(),
- effectiveActiveLayout: undefined,
- activeWorktreeBrowserTabIdsKey: '',
- terminalProviderSnapshotCapabilityRevision: 0,
- saveDialogFileId: null,
- setSaveDialogFileId: vi.fn(),
- saveDialogFile: null,
- pendingEditorCloseQueueRef: { current: [] },
- closedReactionsRef: { current: new Map() },
- inFlightSaveFileIdRef: { current: null },
- isClosingRef: { current: false },
- closeDialogDebounceTimersRef: { current: new Set() },
- releaseCloseDialogGuardAfterDebounce: vi.fn(),
- windowCloseDialogOpen: false,
- setWindowCloseDialogOpen: vi.fn(),
- windowCloseDialogKind: 'running',
- windowCloseAfterDirtyRef: { current: null },
- confirmNativeWindowClose: vi.fn(),
- proceedToNativeWindowClose: vi.fn(),
- waitForFileClosed: vi.fn(),
- settleQueuedClose: vi.fn(),
- getNextQueuedEditorClose: vi.fn(),
- advanceEditorCloseQueue: vi.fn(),
- queueEditorCloseRequests: vi.fn(),
- handleCloseFile: vi.fn(),
- handleSaveDialogSave: vi.fn(),
- handleSaveDialogDiscard: vi.fn(),
- handleSaveDialogCancel: vi.fn(),
- measurableBackgroundWorktreeTimersRef: { current: new Map() },
- backgroundMountRevision: 0,
- setBackgroundMountRevision: vi.fn(),
- terminalParkingRevision: 0,
- setTerminalParkingRevision: vi.fn(),
- browserGuestRetentionRevision: 0,
- setBrowserGuestRetentionRevision: vi.fn(),
- parkedTerminalWorktreeIds: new Set(),
- setParkedTerminalWorktreeIds: vi.fn(),
- manuallyParkedTerminalWorktreeIds: new Set(),
- effectiveParkedTerminalWorktreeIds: new Set(),
- forceParkedTerminalWorktreeIds: new Set(),
- setForceParkedTerminalWorktreeIds: vi.fn(),
- evictionExemptTerminalTabIds: new Set(),
- setEvictionExemptTerminalTabIds: vi.fn(),
- parkedCaptureDoneRef: { current: new Set() },
- backgroundMountTabIdsByWorktreeRef: { current: new Map() },
- activationDeferredMountTabIdsByWorktreeRef: { current: new Map() },
- lastActivationWorktreeIdRef: { current: null },
- prewarmLeasesRef: { current: new Map() },
- startupTerminalTabHoldRef: { current: null },
- activationDeferralPlanRevisionRef: { current: 0 },
- anyMountedWorktreeHasLayout: false,
- activationDeferralPlanRevision: 0,
- startupTerminalTabHold: null,
- handleCloseTab: vi.fn(),
- handleCloseBrowserTab: vi.fn(),
- handlePtyExit: vi.fn(),
- closeTabBarTabs: vi.fn(),
- handleCloseOthers: vi.fn(),
- handleCloseTabsToRight: vi.fn(),
- handleCloseTabsToLeft: vi.fn(),
- handleCloseAllFiles: vi.fn(),
- handleNewBrowserTab: vi.fn(),
- handleNewBrowserTabSplitRight: vi.fn(),
- handleNewFile: vi.fn(),
- handleNewSimulatorTab: vi.fn(),
- handleNewTab: vi.fn(),
- handleOpenEntry: vi.fn(),
- handleDuplicateBrowserTab: vi.fn(),
- handleActivateTab: vi.fn(),
- handleTogglePaneExpand: vi.fn(),
- handleActivateBrowserTab: vi.fn()
- }
- useTerminalKeyboardShortcuts(controller)
- return
-}
-beforeEach(() => {
- vi.stubGlobal('reportError', (error: unknown) => {
- throw error
- })
- vi.stubGlobal('__ORCA_WEB_CLIENT__', true)
- vi.stubGlobal(
- 'ResizeObserver',
- class {
- observe() {}
- unobserve() {}
- disconnect() {}
- }
- )
- vi.spyOn(navigator, 'userAgent', 'get').mockReturnValue('Linux')
- anchor = document.createElement('div')
- anchor.getBoundingClientRect = () => new DOMRect(0, 32, 800, 568)
- document.body.append(anchor)
- unregister = registerTabGroupBody('g', anchor)
- const fixture = nativeChatFanoutFixture(2, 1, 0)
- fixture.unifiedTabsByWorktree[ws] = fixture.unifiedTabsByWorktree[ws].map((tab) => ({
- ...tab,
- groupId: 'g'
- }))
- useAppStore.setState(
- {
- ...initial,
- ...fixture,
- settings: getDefaultSettings('/test35636'),
- activeWorktreeId: ws,
- activeWorkspaceExecutionHostId: 'local',
- activeView: 'terminal',
- activeTabType: 'agent-session',
- groupsByWorktree: {
- [ws]: [{ id: 'g', worktreeId: ws, activeTabId: 'chat-0', tabOrder: ['chat-0', 'chat-1'] }]
- },
- activeGroupIdByWorktree: { [ws]: 'g' }
- },
- true
- )
- mocks.web = false
- mocks.write.mockReset().mockResolvedValue(undefined)
- mocks.start.mockReset().mockResolvedValue({ canceled: false, transferId: 'output35636' })
- mocks.append.mockReset().mockResolvedValue(undefined)
- mocks.finish.mockReset().mockResolvedValue({ canceled: false })
- mocks.cancel.mockReset().mockResolvedValue(undefined)
- mocks.rpc
- .mockReset()
- .mockImplementation(async (_target, method) =>
- method === 'status.get'
- ? { runtimeId: 'host', capabilities: ['native-chat.text-attachment-read.v1'] }
- : chunk()
- )
- Object.defineProperty(window, 'api', {
- configurable: true,
- value: {
- ui: { writeClipboardText: mocks.write },
- fs: {
- startDownloadedFile: mocks.start,
- appendDownloadedFileChunk: mocks.append,
- finishDownloadedFile: mocks.finish,
- cancelDownloadedFile: mocks.cancel
- }
- }
- })
-})
-afterEach(() => {
- cleanup()
- unregister()
- anchor.remove()
- useAppStore.setState(initial, true)
- vi.restoreAllMocks()
- vi.unstubAllGlobals()
-})
-
-it('retires delayed Copy on a genuine keyboard tab hide, preserves the editor and reopens', async () => {
- const view = render()
- const input = screen.getByRole('textbox', { name: 'draft-chat-0' })
- fireEvent.change(input, { target: { value: 'editor state survives hide' } })
- input.focus()
- const open = () => fireEvent.click(screen.getByRole('button', { name: 'Open source' }))
- const ready = () =>
- waitFor(() =>
- expect(screen.getByRole('button', { name: 'Copy contents' }).hasAttribute('disabled')).toBe(
- false
- )
- )
- open()
- await ready()
- const delayed = Promise.withResolvers()
- const waiting = Promise.withResolvers()
- mocks.rpc.mockImplementationOnce(() => {
- waiting.resolve()
- return delayed.promise
- })
- fireEvent.click(screen.getByRole('button', { name: 'Copy contents' }))
- await waiting.promise
- const dispatched = mocks.rpc.mock.calls.length
- fireEvent.keyDown(screen.getByRole('dialog'), {
- key: ']',
- code: 'BracketRight',
- ctrlKey: true,
- altKey: true
- })
- expect(useAppStore.getState().getActiveTab(ws)?.id).toBe('chat-1')
- await act(async () => {})
- expect(
- view.container
- .querySelector('[data-structured-agent-session-overlay-tab-id="chat-0"]')
- ?.hasAttribute('inert')
- ).toBe(true)
- expect(screen.queryByRole('dialog')).toBeNull()
- await act(async () => delayed.resolve(chunk()))
- expect(mocks.write).not.toHaveBeenCalled()
- expect(mocks.rpc).toHaveBeenCalledTimes(dispatched)
- fireEvent.keyDown(document, { key: '[', code: 'BracketLeft', ctrlKey: true, altKey: true })
- const returned = screen.getByRole('textbox', { name: 'draft-chat-0' })
- expect(returned).toBe(input)
- expect(returned.value).toBe('editor state survives hide')
- expect(screen.queryByRole('dialog')).toBeNull()
- open()
- await ready()
- fireEvent.click(screen.getByRole('button', { name: 'Copy contents' }))
- await waitFor(() => expect(mocks.write).toHaveBeenCalledExactlyOnceWith('selected source'))
-})
diff --git a/src/renderer/src/components/native-chat/NativeChatToolLine.test.tsx b/src/renderer/src/components/native-chat/NativeChatToolLine.test.tsx
index 4e0cefb74f3..7cfc39704cc 100644
--- a/src/renderer/src/components/native-chat/NativeChatToolLine.test.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatToolLine.test.tsx
@@ -1,10 +1,8 @@
// @vitest-environment happy-dom
import '@testing-library/jest-dom/vitest'
-import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
+import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { NativeChatToolLine } from './NativeChatToolLine'
-import { NativeChatToolRun } from './NativeChatToolRun'
-import type { NativeChatToolCallBlock } from '../../../../shared/native-chat/native-chat-types'
vi.mock('@/lib/syntax-highlighting/oniguruma', async () => ({
loadOniguruma: (await import('@/lib/syntax-highlighting/oniguruma-test-harness'))
@@ -14,166 +12,6 @@ vi.mock('@/lib/syntax-highlighting/oniguruma', async () => ({
afterEach(cleanup)
describe('tool sentence rows', () => {
- it.each([
- ['Bash', { command: 'pnpm test' }, 'Ran', 'pnpm test', false],
- ['Read', { file_path: '/repo/src/main.ts' }, 'Read', 'main.ts', true],
- ['Edit', { file_path: 'C:\\repo\\main.ts' }, 'Edited', 'main.ts', true],
- ['Write', { file_path: '/repo/new.ts' }, 'Edited', 'new.ts', true],
- ['MultiEdit', { file_path: '/repo/main.ts' }, 'Edited', 'main.ts', true],
- ['Grep', { pattern: 'TODO', path: '/repo' }, 'Searched', 'TODO', true],
- ['Glob', { pattern: '**/*.ts' }, 'Searched', '**/*.ts', true],
- ['search', { query: 'settings', command: 'rg settings' }, 'Searched', 'settings', true],
- ['web_search', { query: 'react docs' }, 'Searched the web', 'react docs', true],
- ['WebFetch', { url: 'https://example.com/docs' }, 'Fetched', 'example.com/docs', true],
- ['CreateWidget', { description: 'a widget' }, 'CreateWidget', 'a widget', true],
- ['list', { directory: '/repo' }, 'Listed', '/repo', true],
- ['exec', { command: 'git diff' }, 'Ran', 'git diff', false],
- ['local_shell', { command: 'pwd' }, 'Ran', 'pwd', false]
- ])('describes %s with a verb and target', (name, input, verb, target, verbShown) => {
- const { container } = render(
-
- )
- // A command is its own row: the run's header already says it ran.
- expect(screen.queryByText(verb, { selector: 'span:not(.sr-only)' }) !== null).toBe(verbShown)
- expect(screen.getByText(target, { selector: 'span:not(.sr-only)' })).toHaveClass(
- 'text-chat-foreground'
- )
- expect(screen.getByRole('button')).toHaveAccessibleName(new RegExp(name))
- expect(screen.getByRole('button')).toHaveAccessibleName(new RegExp(verb))
- expect(container.querySelector('.font-semibold')).toBeNull()
- })
-
- it('retains full paths in titles and accessible targets', () => {
- render(
-
- )
- expect(screen.getByTitle('/repo/src/main.ts')).toHaveTextContent('main.ts')
- expect(screen.getByTitle('/repo/src/main.ts')).toHaveAttribute('aria-hidden', 'true')
- expect(screen.getByRole('button')).toHaveAccessibleName(/Read.*\/repo\/src\/main.ts/)
- })
-
- it('keeps integration identity in plain text even for a command-shaped tool name', () => {
- render(
-
- )
- expect(screen.getByText('My server')).toBeInTheDocument()
- expect(screen.getByText('shell')).toBeInTheDocument()
- expect(screen.queryByText('Ran')).toBeNull()
- expect(screen.getByText('inspect')).not.toHaveClass('font-mono')
- expect(screen.getByText('inspect')).not.toHaveAttribute('data-native-chat-code-content')
- })
-
- it('counts changed lines rather than unchanged lines in an edit', () => {
- const block: NativeChatToolCallBlock = {
- type: 'tool-call',
- name: 'Edit',
- input: { file_path: 'main.ts', old_string: 'same\nold\n', new_string: 'same\nnew\nextra\n' },
- state: 'completed'
- }
- render()
- expect(screen.getByRole('button', { name: /^Edited main\.ts/ })).toBeInTheDocument()
- expect(screen.getByText('+2')).toBeInTheDocument()
- expect(screen.getByText('-1')).toBeInTheDocument()
- })
-
- it('omits counts when an edit supplies no diff', () => {
- render(
-
- )
- expect(screen.queryByText(/^[+-]\d+$/)).toBeNull()
- })
-
- it.each([
- ['Bash', { command: 'pnpm test' }, 'Running'],
- ['exec', { command: 'git diff' }, 'Running'],
- ['local_shell', { command: 'pwd' }, 'Running'],
- ['Read', { file_path: '/repo/a.ts' }, 'Reading'],
- ['Write', { file_path: '/repo/a.ts', content: 'new' }, 'Editing'],
- ['Grep', { pattern: 'TODO' }, 'Searching'],
- ['list', { directory: '/repo' }, 'Listing'],
- ['web_search', { query: 'docs' }, 'Searching the web'],
- ['WebFetch', { url: 'https://example.com/docs' }, 'Fetching']
- ])('uses present tense for a running %s', (name, input, verb) => {
- render()
- expect(screen.getByText(verb)).toBeInTheDocument()
- })
-
- it.each(['failed', undefined] as const)('never claims an edit landed with state %s', (state) => {
- render(
-
- )
- expect(screen.getByRole('button')).toHaveAccessibleName(
- state === 'failed' ? 'Edit Tried to edit /repo/a.ts' : 'Edit /repo/a.ts'
- )
- expect(screen.queryByText('Edited')).toBeNull()
- expect(screen.queryByText(/^[+-]\d+$/)).toBeNull()
- })
-
- it('does not treat an error result as a completed edit', () => {
- render(
-
- )
- expect(screen.getByRole('button')).toHaveAccessibleName('Edit Tried to edit /repo/a.ts')
- expect(screen.queryByText('Edited')).toBeNull()
- })
-
- it('uses a successful legacy result as completion evidence', () => {
- render(
-
- )
- expect(screen.getByText('Ran')).toBeInTheDocument()
- })
-
- it.each([false, true])(
- 'keeps a long Unicode fetch target unchanged (JSON input: %s)',
- (jsonInput) => {
- const url = `https://example.com/日本語/${'segment/'.repeat(25)}end`
- const input = jsonInput ? JSON.stringify({ url }) : { url }
- render(
-
- )
- expect(screen.getByTitle(url)).toHaveTextContent(url.slice('https://'.length))
- expect(screen.getByTitle(url)).not.toHaveTextContent('%E2%80%A6')
- }
- )
-
it('keeps metadata and errors while toggling output behind the sentence', () => {
render(
{
fireEvent.click(button)
expect(screen.queryByText('command failed')).toBeNull()
})
-
- it.each([
- ['Bash', { type: 'tool-result', output: 'failed', isError: true }, undefined],
- ['exec', { type: 'tool-result', output: 'failed', isError: true }, 1],
- ['local_shell', undefined, 2],
- ['Bash', { type: 'tool-result', output: 'ok' }, undefined]
- ] as const)(
- 'says Ran for %s with execution evidence, including failures',
- (name, result, exitCode) => {
- render(
-
- )
- expect(screen.getByText('Ran')).toBeInTheDocument()
- expect(screen.queryByText('Running')).toBeNull()
- }
- )
-
- it.each([
- ['Read', { file_path: '/repo/a.ts' }, 'Tried to read'],
- ['Grep', { pattern: 'TODO' }, 'Tried to search'],
- ['list', { directory: '/repo' }, 'Tried to list'],
- ['WebFetch', { url: 'https://example.com' }, 'Tried to fetch'],
- ['web_search', { query: 'docs' }, 'Tried to search the web']
- ])('describes a failed %s without claiming success', (name, input, verb) => {
- render(
-
- )
- expect(screen.getByText(verb)).toBeInTheDocument()
- })
-
- it.each([
- ['Bash', { command: 'pnpm test' }],
- ['exec', JSON.stringify({ cmd: 'pnpm test' })],
- ['Bash', 'pnpm test']
- ])('omits duplicate input for a complete %s command row', (name, input) => {
- const { container } = render(
-
- )
- fireEvent.click(screen.getByRole('button'))
- expect(container.querySelectorAll('pre')).toHaveLength(1)
- expect(container.querySelector('pre')).toHaveTextContent('all passed')
- })
-
- it.each([
- [{ command: 'pnpm test', description: 'Run the suite' }],
- [{ command: ['printf', '%s', 'a b'] }],
- [{ command: '/bin/zsh -lc "pnpm test"' }]
- ])('opens a command to its output alone, never its raw arguments', (input) => {
- const { container } = render(
-
- )
- fireEvent.click(screen.getByRole('button'))
- expect([...container.querySelectorAll('pre')].map((pre) => pre.textContent)).toEqual([
- 'all passed'
- ])
- })
-
- it('does not offer empty detail for a short command without output', () => {
- const { container } = render(
-
- )
- expect(screen.getByRole('button')).not.toHaveAttribute('aria-expanded')
- expect(container.querySelector('pre')).toBeNull()
- })
-
- it.each([false, true])(
- 'shows the full abbreviated command as plain text (JSON: %s)',
- (jsonInput) => {
- const command = `pnpm test ${'src/renderer/tests/long-path/'.repeat(170)}end.test.ts`
- const input = jsonInput ? JSON.stringify({ command }) : { command }
- const { container } = render(
-
- )
- expect(screen.getByTitle(command)).toHaveTextContent(/…$/)
- expect(container.querySelector('pre')).toBeNull()
- fireEvent.click(screen.getByRole('button'))
- const detail = container.querySelector('pre')
- expect(detail?.textContent).toBe(command)
- expect(detail).toHaveClass('font-mono', 'bg-chat-code-surface')
- expect(container.querySelectorAll('pre')).toHaveLength(2)
- }
- )
-
- it.each([
- ['PowerShell', 'Get-ChildItem', true],
- ['execute', 'Get-ChildItem', false]
- ])('colors the full %s command only when its shell is known', async (name, verb, colored) => {
- const command = `${verb} -Path 'C:\\repo\\src\\renderer' -Recurse -File | Where-Object { $_.Length -gt 10 }`
- const { container } = render(
-
- )
- fireEvent.click(screen.getByRole('button'))
- const detail = container.querySelector('pre')
-
- if (colored) {
- // The first PowerShell block in this file loads the grammar and its regex engine.
- await waitFor(
- () => {
- expect(detail?.querySelector('span[style*="--syntax-dark"]')).not.toBeNull()
- },
- { timeout: 10_000 }
- )
- } else {
- expect(detail?.querySelector('span')).toBeNull()
- }
- expect(detail?.textContent).toBe(command)
- })
-
- it('keeps a long plain command with surrounding whitespace complete', () => {
- const command = ` pnpm test ${'src/renderer/tests/long-path/'.repeat(170)}end.test.ts `
- const { container } = render(
-
- )
- fireEvent.click(screen.getByRole('button'))
- expect(container.querySelector('pre')?.textContent).toBe(command)
- })
-
- it('opens an edit that has not landed to the change it proposes', () => {
- const { container } = render(
-
- )
- expect(container.textContent).not.toContain('now')
- fireEvent.click(screen.getByRole('button'))
- expect(container.textContent).toContain('now')
- })
-
- it.each(['exec', 'shell', 'Bash'])('offers nothing to open on a running %s command', (name) => {
- render(
-
- )
- expect(screen.getByRole('button')).not.toHaveAttribute('aria-expanded')
- })
-
- it('offers nothing to open on a call that has produced no output', () => {
- render(
-
- )
- expect(screen.getByRole('button')).not.toHaveAttribute('aria-expanded')
- })
-
- it('shows a read as its output alone, not the arguments its row already names', () => {
- const { container } = render(
-
- )
- fireEvent.click(screen.getByRole('button'))
- expect([...container.querySelectorAll('pre')].map((pre) => pre.textContent)).toEqual([
- 'const a = 1'
- ])
- })
-
- it.each(['Task', 'Agent'])(
- 'uses the unchanged description for live and settled %s calls',
- (name) => {
- const description = `explore settings search entries ${'without rewriting '.repeat(8)}end`
- const input = {
- description,
- prompt: 'longer private instructions',
- query: 'not the description'
- }
- const { rerender } = render(
-
- )
- expect(screen.getByText('Subagent')).toBeInTheDocument()
- expect(screen.getByTitle(description).textContent).toBe(description)
- rerender(
-
- )
- expect(screen.getByText('Subagent')).toBeInTheDocument()
- expect(screen.getByTitle(description).textContent).toBe(description)
- expect(screen.getByRole('button')).toHaveAccessibleName(new RegExp(name))
- }
- )
-
- it('preserves integration identity for an integration named Agent', () => {
- render(
-
- )
- expect(screen.getByText('My server')).toBeInTheDocument()
- expect(screen.queryByText('Subagent')).toBeNull()
- })
-
- it('still renders result-only rows', () => {
- render()
- fireEvent.click(screen.getByRole('button', { name: /Result/ }))
- expect(screen.getByText('first second', { selector: 'pre' })).toHaveClass(
- 'text-chat-foreground'
- )
- })
})
diff --git a/src/renderer/src/components/native-chat/NativeChatToolRun.appearance.test.tsx b/src/renderer/src/components/native-chat/NativeChatToolRun.appearance.test.tsx
index 1a416114ed7..93844d513bc 100644
--- a/src/renderer/src/components/native-chat/NativeChatToolRun.appearance.test.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatToolRun.appearance.test.tsx
@@ -10,8 +10,6 @@ import type { NativeChatBlock } from '../../../../shared/native-chat/native-chat
import { NativeChatToolRun } from './NativeChatToolRun'
-import { nativeChatAppearanceStyle } from './native-chat-appearance-style'
-
import { openToolRunMembers } from './native-chat-tool-run-members-test-support'
import {
@@ -20,43 +18,8 @@ import {
useNativeChatDisclosures
} from './native-chat-disclosure-store'
-import { revealNativeChatToolRunMember } from './NativeChatToolRunMembers'
-
import { i18n } from '@/i18n/i18n'
-{
- describe('tool-run summary in a matching chat', () => {
- afterEach(cleanup)
-
- it('keeps a long command available in the expanded detail', () => {
- const command = `printf ${'x'.repeat(5000)}`
- const style = nativeChatAppearanceStyle({
- terminalFontFamily: 'Menlo',
- nativeChatAppearance: { matchTerminalInterface: true }
- })
- const { container } = render(
-
-
-
- )
-
- const summary = container.querySelector('span.line-clamp-2')
- expect(summary).toHaveClass('line-clamp-2', 'break-words')
- expect(summary?.textContent).toContain('printf')
- expect(summary?.textContent?.length).toBeLessThan(command.length)
- expect(container.querySelector('pre')).toBeNull()
-
- fireEvent.click(screen.getByRole('button'))
- openToolRunMembers()
-
- expect(container.querySelector('pre')).toHaveTextContent(command)
- })
- })
-}
-
{
const QUESTION = 'What would you like me to do next in this repo?'
const ASK_INPUT = { questions: [{ question: QUESTION }] }
@@ -107,29 +70,6 @@ import { i18n } from '@/i18n/i18n'
describe('NativeChatToolRun awaiting-input row', () => {
afterEach(cleanup)
- it('does not revive stale tool state after a turn stops', () => {
- render(
-
- )
- expect(screen.queryByText('Awaiting user input:')).toBeNull()
- expect(screen.getByText('Asked:')).toBeInTheDocument()
- })
-
- it('keeps a pending question visible alongside another active tool', () => {
- render(
-
- )
- expect(screen.getByText('Awaiting user input:')).toBeInTheDocument()
- expect(screen.getByText('Reading 1 file')).toBeInTheDocument()
- expect(screen.getByText('Read a.ts')).toBeInTheDocument()
- })
it('preserves errors from failed question calls', () => {
render(
@@ -147,79 +87,6 @@ import { i18n } from '@/i18n/i18n'
expect(screen.queryByText('Asked:')).toBeNull()
expect(screen.getAllByText('Question rejected').length).toBeGreaterThan(0)
})
- it('replaces a running ask call with the awaiting row', () => {
- const { container } = render(
-
- )
-
- expect(screen.getByText('Awaiting user input:')).toHaveClass(
- 'animate-pulse',
- 'motion-reduce:animate-none'
- )
- expect(screen.getByText(QUESTION)).toBeInTheDocument()
- expect(container.querySelector('.lucide-message-square-more')).toBeInTheDocument()
- expect(screen.queryByText(/Running AskUserQuestion/)).toBeNull()
- expect(screen.queryByText(/AskUserQuestion/)).toBeNull()
- })
-
- it('reports a settled ask without the pulse or a tool-count header', () => {
- const { container } = render(
-
- )
-
- expect(screen.getByText('Asked:')).not.toHaveClass('animate-pulse')
- expect(screen.getByText(QUESTION)).toBeInTheDocument()
- expect(container.querySelector('[data-native-chat-tool-run-state]')).toBeNull()
- })
-
- it('keeps a question that fits on its line collapsed but expandable', () => {
- render(
-
- )
- expect(screen.getByText(QUESTION)).toHaveClass('truncate')
- expect(screen.getByRole('button', { name: /Asked:/ })).toHaveAttribute(
- 'aria-expanded',
- 'false'
- )
- })
-
- it('opens a clipped question below the toggle and folds it back', () => {
- const restore = clipEveryLine()
- try {
- render(
-
- )
- const toggle = screen.getByRole('button', { name: /Asked:/ })
- expect(toggle).toHaveAttribute('aria-expanded', 'false')
-
- fireEvent.click(toggle)
- expect(toggle).toHaveAttribute('aria-expanded', 'true')
- const full = screen.getByText(QUESTION)
- expect(full).not.toHaveClass('truncate')
- // Outside the button, so it selects like prose and a click in it keeps it open.
- expect(full.closest('button')).toBeNull()
- fireEvent.click(full)
- expect(toggle).toHaveAttribute('aria-expanded', 'true')
-
- fireEvent.click(toggle)
- expect(toggle).toHaveAttribute('aria-expanded', 'false')
- expect(screen.getByText(QUESTION)).toHaveClass('truncate')
- } finally {
- restore()
- }
- })
it('keeps an opened question open when the windowed row remounts', () => {
const restore = clipEveryLine()
@@ -244,51 +111,6 @@ import { i18n } from '@/i18n/i18n'
restore()
}
})
-
- it('offers no expansion when the row names only a question count', () => {
- const restore = clipEveryLine()
- try {
- const input = { questions: [{ question: 'First?' }, { question: 'Second?' }] }
- render(
-
- )
- expect(screen.getByText('2 questions')).toBeInTheDocument()
- expect(screen.queryByRole('button', { name: /Asked:/ })).toBeNull()
- } finally {
- restore()
- }
- })
-
- it('counts only the work that ran in the header beside the ask', () => {
- const blocks: NativeChatBlock[] = [
- { type: 'tool-call', name: 'Read', input: { file_path: 'a.ts' }, state: 'completed' },
- { type: 'tool-call', name: 'AskUserQuestion', input: ASK_INPUT, state: 'running' }
- ]
-
- render()
-
- expect(screen.getByText('Awaiting user input:')).toBeInTheDocument()
- // Waiting for an answer settles the work summary; the question itself adds no call count.
- expect(screen.getByText('Read 1 file')).toBeInTheDocument()
- expect(screen.queryByText('Reading 1 file')).toBeNull()
- })
-
- it('draws the row from the tool name when the payload names no question', () => {
- render(
-
- )
-
- expect(screen.getByText('Awaiting user input')).toBeInTheDocument()
- expect(screen.queryByText(/request_user_input/)).toBeNull()
- })
})
}
@@ -463,57 +285,6 @@ import { i18n } from '@/i18n/i18n'
fireEvent.click(returnedHeader)
expect(membersBox(container).scrollTop).toBe(0)
})
-
- it('opens a settled run at its first call', () => {
- layOut({ current: 600 })
- const { container } = render(
-
- )
- expect(membersBox(container).scrollTop).toBe(0)
- })
-
- it('brings a revealed member to the top of the box and reports the box', () => {
- layOut({ current: 600 })
- const { container } = render(
-
- )
- const box = membersBox(container)
- const member = box.querySelectorAll('button')[2]!
- vi.spyOn(box, 'getBoundingClientRect').mockReturnValue(new DOMRect(0, 40, 300, VIEWPORT))
- vi.spyOn(member, 'getBoundingClientRect').mockReturnValue(new DOMRect(0, 340, 300, 26))
-
- expect(revealNativeChatToolRunMember(member)).toBe(box)
- expect(box.scrollTop).toBe(300)
- })
- })
-
- describe('NativeChatToolRun asides', () => {
- afterEach(() => {
- cleanup()
- vi.restoreAllMocks()
- })
- it('draws the rows it is handed among its calls, in order', () => {
- const blocks = calls(2)
- const { container } = render(
- between
]]]),
- after: [last
]
- }}
- expandSignal
- activeTurnIsWorking={false}
- />
- )
- const rows = [...membersBox(container).querySelectorAll('button, p')].map(
- (row) => row.textContent ?? ''
- )
- expect(rows).toHaveLength(4)
- expect(rows[0]).toContain('file-0.ts')
- expect(rows[1]).toBe('between')
- expect(rows[2]).toContain('file-1.ts')
- expect(rows[3]).toBe('last')
- })
})
}
@@ -531,44 +302,6 @@ import { i18n } from '@/i18n/i18n'
cleanup()
await i18n.changeLanguage('en')
})
- it.each(['Task', 'Agent'])('separates failure counts in command and %s runs', (name) => {
- const blocks: NativeChatBlock[] = [
- { type: 'tool-call', name: 'Bash', input: { command: 'false' }, state: 'failed' },
- { type: 'tool-result', output: 'exit 1', isError: true },
- {
- type: 'tool-call',
- name,
- input: { description: 'explored settings search entries' },
- state: 'completed'
- }
- ]
- const { container } = render()
- const header = runHeader(container)
- expect(header).toHaveTextContent('Ran 1 command and ran 1 agent · 1 failed')
- expect(header).toHaveAccessibleName(/Failed tool calls: 1/)
- expect(header).not.toHaveAccessibleName(/·/)
- fireEvent.click(header)
- expect(screen.getByText('Subagent')).toBeInTheDocument()
- expect(screen.getByTitle('explored settings search entries')).toBeInTheDocument()
- })
-
- it('uses the agent glyph for a lone Agent transcript call', () => {
- const { container } = render(
-
- )
- expect(runHeader(container)).toHaveTextContent('Ran 1 agent')
- expect(container.querySelectorAll('button .lucide-bot')).toHaveLength(2)
- })
it('relabels an already-mounted run and its rows when the UI language changes', async () => {
const blocks: NativeChatBlock[] = [
diff --git a/src/renderer/src/components/native-chat/NativeChatToolRun.identity.test.tsx b/src/renderer/src/components/native-chat/NativeChatToolRun.identity.test.tsx
index dbf85af6af1..0fbb34ef8db 100644
--- a/src/renderer/src/components/native-chat/NativeChatToolRun.identity.test.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatToolRun.identity.test.tsx
@@ -2,11 +2,7 @@
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { NativeChatToolRun } from './NativeChatToolRun'
-import type {
- NativeChatBlock,
- NativeChatToolCallBlock,
- NativeChatToolResultBlock
-} from '../../../../shared/native-chat/native-chat-types'
+import type { NativeChatToolCallBlock } from '../../../../shared/native-chat/native-chat-types'
import {
NativeChatDisclosureContext,
useNativeChatDisclosures
@@ -17,11 +13,6 @@ vi.mock('./NativeChatDiffCard', () => ({ NativeChatDiffCard: () => null }))
vi.mock('./NativeChatDiffView', () => ({ NativeChatDiffView: () => null }))
const disclosureWrite = vi.fn()
-const capturedDisclosures = {
- read: (_key: string) => undefined,
- write: (key: string, open: boolean) => disclosureWrite(key, open),
- runScroll: new Map()
-}
afterEach(() => {
cleanup()
@@ -37,12 +28,6 @@ const shell: NativeChatToolCallBlock = {
durationMs: 400
}
-const shellResult: NativeChatToolResultBlock = {
- type: 'tool-result',
- output: 'command not found',
- isError: true
-}
-
function ToolRunDisclosureHarness({ expandSignal }: { expandSignal: boolean }) {
const disclosures = useNativeChatDisclosures()
return (
@@ -73,175 +58,6 @@ describe('inline tool annotations', () => {
screen.getByRole('button', { name: /missing-command/ }).getAttribute('aria-expanded')
).toBe('false')
})
-
- it('resynchronizes a standalone run when the toolbar signal flips', () => {
- const { rerender } = render(
-
- )
- expect(screen.getByRole('button').getAttribute('aria-expanded')).toBe('false')
-
- rerender()
-
- expect(
- screen.getAllByRole('button', { name: /missing-command/ })[0].getAttribute('aria-expanded')
- ).toBe('true')
- })
-
- it('uses provider call identities for byte-identical line disclosure keys', () => {
- const blocks: NativeChatBlock[] = [
- { ...shell, callId: 'call-a' },
- shellResult,
- { ...shell, callId: 'call-b' },
- shellResult
- ]
- render(
-
-
-
- )
-
- fireEvent.click(screen.getAllByRole('button')[2]!)
-
- expect(disclosureWrite).toHaveBeenCalledExactlyOnceWith('line:message-1:call:call-b', true)
- })
-
- it('keeps occurrence identity as the fallback for calls without provider IDs', () => {
- render(
-
-
-
- )
-
- fireEvent.click(screen.getAllByRole('button')[2]!)
-
- expect(disclosureWrite).toHaveBeenCalledExactlyOnceWith(
- 'line:message-1:tool-call:shell:{"command":"missing-command"}:1',
- true
- )
- })
-
- it('keeps occurrence identity for whitespace-only provider IDs', () => {
- render(
-
-
-
- )
-
- fireEvent.click(screen.getAllByRole('button')[2]!)
-
- expect(disclosureWrite).toHaveBeenCalledExactlyOnceWith(
- 'line:message-1:tool-call:shell:{"command":"missing-command"}:1',
- true
- )
- })
-
- it('keeps command completion annotations on the collapsed tool line', () => {
- render(
-
- )
- expect(screen.getByText('exit 127').closest('button')).toBe(
- screen.getByText('400ms').closest('button')
- )
- expect(screen.getByText('exit 127').closest('button')?.getAttribute('aria-expanded')).toBe(
- 'false'
- )
- expect(screen.queryByText('0s')).toBeNull()
- })
- it('renders a legacy command without invented metadata', () => {
- render(
-
- )
- expect(screen.queryByText(/exit \d/)).toBeNull()
- expect(screen.queryByText(/\d+ms/)).toBeNull()
- })
- it('shows distinct MCP names while retaining the raw identifier', () => {
- const name = 'mcp__linear__list_issues'
- render()
- expect(screen.getByText('Linear')).toBeTruthy()
- expect(screen.getByText('list issues')).toBeTruthy()
- expect(screen.getByTitle(name)).toBeTruthy()
- })
- it('reveals safe result links only inside row disclosure and routes clicks through chat', () => {
- const onLinkClick = vi.fn((event) => event.preventDefault())
- const block: NativeChatToolCallBlock = {
- type: 'tool-call',
- name: 'web_search',
- input: { query: 'docs' },
- state: 'completed',
- webSearchResults: [{ title: 'Reference docs', url: 'https://example.com/docs' }]
- }
- render()
- expect(screen.queryByRole('link')).toBeNull()
- fireEvent.click(screen.getByText('Searched the web').closest('button')!)
- const link = screen.getByRole('link', { name: /Reference docs/ })
- expect(link.getAttribute('href')).toBe('https://example.com/docs')
- expect(link.closest('button')).toBeNull()
- fireEvent.click(link)
- expect(onLinkClick).toHaveBeenCalledWith(expect.anything(), 'https://example.com/docs')
- fireEvent(link, new MouseEvent('auxclick', { button: 1, bubbles: true }))
- expect(onLinkClick).toHaveBeenCalledTimes(2)
- })
-})
-
-it.each(['tools/read', 'browser.open', 'package.lock', 'linear/list_issues'])(
- 'keeps an ordinary tool name %s intact',
- (name) => {
- render()
- expect(screen.getByText(name)).toBeTruthy()
- expect(document.querySelector('.lucide-plug')).toBeNull()
- }
-)
-
-it.each(['running', 'completed'] as const)(
- 'renders provider MCP identity on %s rows and headers',
- (state) => {
- const name = 'my_server/ns.tool'
- render(
-
- )
- expect(screen.getByText('My server')).toBeTruthy()
- expect(screen.getByText('ns.tool')).toBeTruthy()
- expect(screen.getByTitle(name)).toBeTruthy()
- // Run and member glyphs must agree on MCP identity.
- expect(document.querySelectorAll('.lucide-plug')).toHaveLength(2)
- }
-)
-
-it.each([
- [{ exitCode: 0 }, 'exit 0', '400ms'],
- [{ durationMs: 400 }, '400ms', 'exit 0']
-])('renders independently optional command metadata %j', (metadata, present, absent) => {
- render(
-
- )
- expect(screen.getByText(present)).toBeTruthy()
- expect(screen.queryByText(absent)).toBeNull()
})
it('filters untrusted persisted result URLs at the renderer boundary', () => {
diff --git a/src/renderer/src/components/native-chat/NativeChatToolRun.test.tsx b/src/renderer/src/components/native-chat/NativeChatToolRun.test.tsx
index 14f13ce9d1b..d43ddbab860 100644
--- a/src/renderer/src/components/native-chat/NativeChatToolRun.test.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatToolRun.test.tsx
@@ -13,15 +13,6 @@ import { openToolRunMembers } from './native-chat-tool-run-members-test-support'
afterEach(cleanup)
/** Read lucide classes so identity-changing glyph swaps remain observable. */
-function leadingGlyphs(container: HTMLElement): (string | null)[] {
- return [...container.querySelectorAll('button')].map(
- (button) =>
- button
- .querySelector('svg')
- ?.getAttribute('class')
- ?.match(/lucide-[a-z0-9-]+/)?.[0] ?? null
- )
-}
/** The first button is the header; summary members render as separate pills. */
function runHeader(container: HTMLElement): HTMLElement {
@@ -33,49 +24,6 @@ function runHeader(container: HTMLElement): HTMLElement {
}
describe('NativeChatToolRun', () => {
- it('uses the shared clean label for a desktop tool row', () => {
- const blocks: NativeChatBlock[] = [
- {
- type: 'tool-call',
- name: 'Read',
- input: '{"file_path":"src/index.ts","offset":10}'
- }
- ]
-
- render()
-
- expect(screen.getByTitle('src/index.ts')).toHaveTextContent('index.ts')
- expect(screen.queryByTitle('{"file_path":"src/index.ts","offset":10}')).toBeNull()
- })
-
- it('renders structured apply_patch changes as a reviewable diff instead of JSON', () => {
- const blocks: NativeChatBlock[] = [
- {
- type: 'tool-call',
- name: 'apply_patch',
- // This lane carries its patch on the call; provider completion proves the edit landed.
- state: 'completed',
- input: {
- changes: [
- {
- path: '/repo/src/app.ts',
- kind: { type: 'update', move_path: null },
- diff: '@@ -1 +1 @@\n-before\n+after'
- }
- ]
- }
- }
- ]
-
- const { container } = render()
- openToolRunMembers()
-
- expect(screen.getByText('after')).toBeInTheDocument()
- expect(screen.getByText('before')).toBeInTheDocument()
- expect(screen.getByText('Edited')).toBeInTheDocument()
- expect(container.querySelector('pre')).toBeNull()
- })
-
it('renders evidence-shaped projected patches as colored diffs without changes JSON', () => {
const item: AgentJournalRenderItem = {
itemId: 'apply-patch',
@@ -134,103 +82,6 @@ describe('NativeChatToolRun', () => {
expect(body).toHaveClass('text-destructive')
})
- it('leaves a `git diff` command as a command row rather than an edit card', () => {
- const blocks: NativeChatBlock[] = [
- { type: 'tool-call', name: 'exec', input: { command: 'git diff' }, state: 'completed' },
- {
- type: 'tool-result',
- output: 'diff --git a/a.ts b/a.ts\n--- a/a.ts\n+++ b/a.ts\n@@ -1 +1 @@\n-was\n+now'
- }
- ]
-
- const { container } = render()
-
- const commandRow = screen.getByRole('button', { name: /^exec Ran git diff/ })
- expect(within(commandRow).queryByText('Edited')).toBeNull()
- expect(container).toHaveTextContent('git diff')
- })
-
- it('shows no gutter number for a snippet edit, which cannot locate itself', () => {
- const blocks: NativeChatBlock[] = [
- {
- type: 'tool-call',
- name: 'Edit',
- input: { file_path: '/repo/a.ts', old_string: 'was', new_string: 'now' },
- state: 'completed'
- },
- { type: 'tool-result', output: 'ok' }
- ]
-
- render()
- openToolRunMembers()
-
- // Exact, because a snippet-relative number would sit ahead of the marker.
- expect(screen.getByText('now').closest('div')?.textContent).toBe('+now')
- expect(screen.getByText('was').closest('div')?.textContent).toBe('-was')
- })
-
- it('separates two regions of a file so the gutter jump is accounted for', () => {
- const blocks: NativeChatBlock[] = [
- {
- type: 'tool-call',
- name: 'Edit',
- input: { file_path: '/repo/a.ts' },
- state: 'completed'
- },
- {
- type: 'tool-result',
- output: 'ok',
- editPatch: {
- filePath: '/repo/a.ts',
- hunks: [
- { oldStart: 42, oldLines: 1, newStart: 42, newLines: 1, lines: ['-was', '+now'] },
- { oldStart: 310, oldLines: 1, newStart: 310, newLines: 1, lines: ['-old', '+new'] }
- ]
- }
- }
- ]
-
- render()
- openToolRunMembers()
-
- const separators = screen.getAllByRole('separator')
- expect(separators).toHaveLength(1)
- expect(separators[0]).toHaveAccessibleName('Lines not shown')
- })
-
- it('offers no empty body for a delete, which names the file and nothing else', () => {
- const blocks: NativeChatBlock[] = [
- {
- type: 'tool-call',
- name: 'apply_patch',
- input: { input: '*** Begin Patch\n*** Delete File: gone.ts\n*** End Patch' },
- state: 'completed'
- }
- ]
-
- render()
-
- expect(screen.getByTitle('gone.ts')).toBeInTheDocument()
- expect(screen.getByText('Deleted').closest('button')).not.toHaveAttribute('aria-expanded')
- })
-
- it('says a diff was clipped even while the card is collapsed', () => {
- const blocks: NativeChatBlock[] = [
- {
- type: 'tool-call',
- name: 'Diff',
- input: { path: 'src/a.ts' },
- state: 'completed'
- },
- { type: 'tool-result', output: '@@ -1,3 +1,3 @@\n ctx\n-was\n+now\n… (48210 bytes)' }
- ]
-
- render()
-
- expect(screen.getByText('Diff truncated')).toBeInTheDocument()
- expect(screen.queryByText('was')).toBeNull()
- })
-
it('copies the diff as signed rows, with the region breaks left out', () => {
const writeClipboardText = vi.fn()
Object.assign(window, { api: { ui: { writeClipboardText } } })
@@ -260,151 +111,6 @@ describe('NativeChatToolRun', () => {
expect(writeClipboardText).toHaveBeenCalledWith(' ctx\n-was\n+now\n+tail')
})
- describe('reading a batch as a group', () => {
- const batch: NativeChatBlock[] = [
- {
- type: 'tool-call',
- name: 'mcp__linear__list_issues',
- input: { query: 'todo' },
- state: 'completed',
- mcpIdentity: { server: 'linear', tool: 'list_issues' }
- },
- { type: 'tool-call', name: 'Bash', input: { command: 'ls -la' }, state: 'completed' },
- { type: 'tool-call', name: 'read', input: { file_path: 'README.md' }, state: 'completed' }
- ]
-
- // Summarize categories; separators also occur inside tool names and make joined call lists ambiguous.
- it('summarizes a run by category, in the order the run first used each one', () => {
- const { container } = render()
-
- expect(runHeader(container)).toHaveTextContent(
- 'Used 1 integration, ran 1 command, and read 1 file'
- )
- })
-
- it('names no individual call in the header, however wide the run', () => {
- const { container } = render()
-
- const header = runHeader(container)
- expect(header).not.toHaveTextContent('mcp__linear__list_issues')
- expect(header).not.toHaveTextContent('ls -la')
- expect(header).not.toHaveTextContent('README.md')
- })
-
- it('counts every call rather than capping the list and marking a remainder', () => {
- const wide: NativeChatBlock[] = [
- ...batch,
- { type: 'tool-call', name: 'Grep', input: { pattern: 'todo' }, state: 'completed' },
- { type: 'tool-call', name: 'Write', input: { file_path: 'a.ts' }, state: 'completed' }
- ]
-
- const { container } = render()
-
- expect(runHeader(container)).not.toHaveTextContent('more')
- expect(runHeader(container)).toHaveTextContent(
- 'Used 1 integration, ran 1 command, read 1 file, searched 1 time, and edited 1 file'
- )
- })
-
- it('joins exactly two categories with "and", and no comma', () => {
- const pair: NativeChatBlock[] = [
- { type: 'tool-call', name: 'Bash', input: { command: 'ls' }, state: 'completed' },
- { type: 'tool-call', name: 'Bash', input: { command: 'pwd' }, state: 'completed' },
- { type: 'tool-call', name: 'read', input: { file_path: 'a.ts' }, state: 'completed' }
- ]
-
- const { container } = render()
-
- expect(runHeader(container)).toHaveTextContent('Ran 2 commands and read 1 file')
- })
-
- it('prints the split MCP name on the opened row', () => {
- render()
-
- expect(screen.getByText('Linear')).toBeInTheDocument()
- })
-
- it('gives a run whose tool it cannot name the generic category', () => {
- const { container } = render(
-
- )
-
- expect(runHeader(container)).toHaveTextContent('Used 1 tool')
- })
-
- it('keeps a lone command as the header, unwrapped from its login shell', () => {
- const { container } = render(
-
- )
-
- expect(runHeader(container)).toHaveTextContent('git push --force-with-lease')
- expect(runHeader(container)).not.toHaveTextContent('/bin/zsh')
- })
- })
-
- it('keeps a grouped active run to one stable row showing only the latest tool', () => {
- const blocks: NativeChatBlock[] = [
- { type: 'tool-call', name: 'shell', input: { command: 'date' }, state: 'completed' },
- { type: 'tool-call', name: 'shell', input: { command: 'pwd' }, state: 'completed' },
- { type: 'tool-call', name: 'shell', input: { command: 'cat package.json' }, state: 'running' }
- ]
-
- const { container } = render()
-
- const header = runHeader(container)
- expect(header).toHaveTextContent('Running 3 commands')
- expect(header).toHaveTextContent('cat package.json')
- expect(header).not.toHaveTextContent('date')
- expect(header).not.toHaveTextContent('pwd')
- expect(screen.getByText('Running 3 commands')).toHaveClass(
- 'animate-pulse',
- 'motion-reduce:animate-none'
- )
- expect(header.querySelector('.lucide-check')).toBeNull()
- expect(container.querySelector('.animate-spin')).toBeNull()
- })
-
- it('treats legacy tool calls without lifecycle state as active while the turn works', () => {
- render(
-
- )
-
- expect(screen.getByText('Running 1 command')).toBeInTheDocument()
- expect(screen.getByText('sleep 5')).toBeInTheDocument()
- })
-
- it('keeps a completed tool payload collapsed until the run is expanded', () => {
- const blocks: NativeChatBlock[] = [
- {
- type: 'tool-call',
- name: 'shell',
- input: { command: 'printf hello' },
- state: 'completed'
- },
- { type: 'tool-result', output: 'hello' }
- ]
-
- render()
- expect(screen.queryByText('hello')).toBeNull()
- })
-
// Call transitions must preserve one turn-owned header, its count, hover and status mark.
it('keeps one header element from a call starting until its turn ends', () => {
const running: NativeChatBlock[] = [
@@ -451,109 +157,6 @@ describe('NativeChatToolRun', () => {
expect(header.querySelector('.animate-pulse')).toBeNull()
})
- it('settles a run the agent has moved past even while its last call still reports', () => {
- const { container } = render(
-
- )
-
- const header = runHeader(container)
- expect(header).toHaveAttribute('data-native-chat-tool-run-state', 'settled')
- expect(header).not.toHaveTextContent('Running')
- expect(header.querySelector('.animate-pulse')).toBeNull()
- expect(header.querySelector('.lucide-check')).toBeNull()
- })
-
- it('never animates a settled tool row with its completion check', () => {
- const { container } = render(
-
- )
-
- const settledRow = runHeader(container)
- expect(settledRow).toHaveTextContent('pnpm test')
- expect(settledRow.querySelector('.lucide-check')).toBeInTheDocument()
- // Windowing remounts settled rows on scroll; a mark that faded in would replay.
- expect(settledRow.querySelector('.lucide-check')).not.toHaveClass('animate-in')
- expect(settledRow.querySelector('.animate-pulse')).toBeNull()
- expect(container.querySelector('.animate-pulse')).toBeNull()
- })
-
- it('refuses the completion mark to a collapsed run whose call failed', () => {
- const blocks: NativeChatBlock[] = [
- { type: 'tool-call', name: 'shell', input: { command: 'false' }, state: 'failed' },
- { type: 'tool-result', output: 'exit 1', isError: true }
- ]
-
- const { container } = render()
-
- expect(container.querySelector('.lucide-check')).toBeNull()
- expect(runHeader(container)).toHaveTextContent(' · 1 failed')
- expect(runHeader(container)).toHaveAccessibleName(/Failed tool calls: 1/)
- expect(container.querySelector('.lucide-circle-alert')).toBeNull()
- expect(container.querySelector('[class*="destructive"]')).toBeNull()
- expect(screen.queryByText('exit 1')).toBeNull()
- })
-
- it('counts every failed call in a run, not just the last one', () => {
- const blocks: NativeChatBlock[] = [
- { type: 'tool-call', name: 'shell', input: { command: 'a' }, state: 'failed' },
- { type: 'tool-result', output: 'exit 1', isError: true },
- { type: 'tool-call', name: 'shell', input: { command: 'b' }, state: 'failed' },
- { type: 'tool-result', output: 'exit 2', isError: true },
- { type: 'tool-call', name: 'shell', input: { command: 'c' }, state: 'completed' },
- { type: 'tool-result', output: 'ok' }
- ]
-
- const { container } = render()
-
- expect(runHeader(container)).toHaveTextContent('2 failed')
- expect(container.querySelector('.lucide-check')).toBeNull()
- })
-
- it('says nothing and keeps the mark when every call in the run succeeded', () => {
- const blocks: NativeChatBlock[] = [
- { type: 'tool-call', name: 'shell', input: { command: 'a' }, state: 'completed' },
- { type: 'tool-result', output: 'ok' }
- ]
-
- const { container } = render()
-
- expect(runHeader(container)).not.toHaveTextContent('failed')
- expect(container.querySelector('.lucide-check')).toBeInTheDocument()
- })
-
- // Keep output behind each call’s disclosure to avoid doubling expanded runs with Result rows.
- it('keeps a call output behind the call row, not beside it', () => {
- const blocks: NativeChatBlock[] = [
- { type: 'tool-call', name: 'shell', input: { command: 'git log -1' }, state: 'failed' },
- { type: 'tool-result', output: 'exit 128', isError: true }
- ]
-
- render()
-
- expect(screen.queryByText('Result')).toBeNull()
- expect(screen.queryByText('exit 128')).toBeNull()
-
- // Click the member, since this single-call header names the same command.
- const rows = screen.getAllByText('git log -1')
- fireEvent.click(rows.at(-1)!.closest('button')!)
-
- expect(screen.getByText('exit 128')).toBeInTheDocument()
- })
-
it('keeps a post-turn running call neutral until the item itself settles', () => {
const blocks: NativeChatBlock[] = [
{ type: 'tool-call', name: 'shell', input: { command: 'sleep 1' }, state: 'running' }
@@ -578,284 +181,7 @@ describe('NativeChatToolRun', () => {
expect(container.querySelector('.lucide-check')).toBeInTheDocument()
})
- it('shows the category glyph beside the word a classified row is named by', () => {
- const blocks: NativeChatBlock[] = [
- {
- type: 'tool-call',
- name: 'read',
- input: { command: "sed -n '1,200p' notes.txt", path: 'notes.txt' },
- state: 'completed'
- }
- ]
-
- const { container } = render()
-
- const glyph = container.querySelector('.lucide-eye')
- expect(glyph).toBeInTheDocument()
- expect(glyph).toHaveAttribute('aria-hidden')
- expect(screen.getByText('Read')).toBeInTheDocument()
- })
-
- it('holds one glyph for a category across running, completed, and failed', () => {
- const searchCall = (state: 'running' | 'completed' | 'failed'): NativeChatBlock[] => [
- { type: 'tool-call', name: 'search', input: { query: 'beta' }, state }
- ]
- const { container, rerender } = render(
-
- )
-
- expect(leadingGlyphs(container)).toEqual(['lucide-search', 'lucide-search'])
-
- for (const settled of ['completed', 'failed'] as const) {
- rerender(
-
- )
-
- expect(leadingGlyphs(container)).toEqual(['lucide-search', 'lucide-search'])
- }
- })
-
- it('falls back to the generic tool glyph, not the terminal, for an unmodelled row', () => {
- const blocks: NativeChatBlock[] = [
- {
- type: 'tool-call',
- name: 'CreateWidget',
- input: { prompt: 'which?' },
- state: 'completed'
- }
- ]
-
- const { container } = render()
-
- expect(container.querySelector('.lucide-square-terminal')).toBeNull()
- expect(container.querySelector('.lucide-wrench')).toBeInTheDocument()
- })
-
- it('agrees between the header and the row it names for an unmodelled tool', () => {
- const blocks: NativeChatBlock[] = [
- {
- type: 'tool-call',
- name: 'CreateWidget',
- input: { prompt: 'which?' },
- state: 'completed'
- }
- ]
-
- const { container } = render()
-
- expect(leadingGlyphs(container)).toEqual(['lucide-wrench', 'lucide-wrench'])
- })
-
- // Unpaired results have translated labels rather than a tool name to classify.
- it('leaves an unpaired result row without a category glyph', () => {
- const blocks: NativeChatBlock[] = [{ type: 'tool-result', output: 'first line' }]
-
- const { container } = render()
-
- expect(runHeader(container)).toHaveAccessibleName('Result')
- const resultRow = screen.getByRole('button', { name: 'Result first line' })
- // Keying a category off 'Result' would resolve a different glyph per locale.
- expect(
- [...(resultRow?.querySelectorAll('svg') ?? [])].map(
- (svg) => svg.getAttribute('class')?.match(/lucide-[a-z0-9-]+/)?.[0]
- )
- ).toEqual(['lucide-chevron-right'])
- })
-
- it('heads a projected diff run with the file-change glyph, not the generic one', () => {
- const projected = projectStructuredItemToNativeChat({
- itemId: 'file-change',
- revision: 1,
- sequence: 1,
- observedAt: 1,
- body: {
- kind: 'diff',
- path: 'src/a.ts',
- patch: {
- head: '@@ -1 +1 @@\n-was\n+now',
- truncated: false,
- byteLength: 24,
- digest: 'a'.repeat(64)
- }
- }
- })
-
- const { container } = render(
-
- )
-
- expect(container.querySelector('.lucide-pencil')).toBeInTheDocument()
- expect(container.querySelector('.lucide-wrench')).toBeNull()
- })
-
- describe('the settled header glyph over a whole run', () => {
- // The header glyph describes the summarized run, not its last-finishing call.
- const call = (name: string, input: unknown): NativeChatBlock => ({
- type: 'tool-call',
- name,
- input,
- state: 'completed'
- })
-
- it('heads a run that is all reads with the read glyph', () => {
- const blocks: NativeChatBlock[] = [
- call('read', { command: "sed -n '1,50p' a.ts", path: 'a.ts' }),
- call('read', { command: "sed -n '1,50p' b.ts", path: 'b.ts' })
- ]
-
- const { container } = render(
-
- )
-
- expect(leadingGlyphs(container)).toEqual(['lucide-eye', 'lucide-eye', 'lucide-eye'])
- })
-
- it('heads a run that is all shell with the terminal glyph, whatever each is named', () => {
- const blocks: NativeChatBlock[] = [
- call('shell', { command: 'npm test' }),
- call('Bash', { command: 'git status' })
- ]
-
- const { container } = render(
-
- )
-
- expect(leadingGlyphs(container)).toEqual([
- 'lucide-square-terminal',
- 'lucide-square-terminal',
- 'lucide-square-terminal'
- ])
- })
-
- it('heads a run spanning categories with the generic tool glyph', () => {
- const blocks: NativeChatBlock[] = [
- call('shell', { command: 'npm test' }),
- call('read', { command: "sed -n '1,50p' a.ts", path: 'a.ts' })
- ]
-
- const { container } = render(
-
- )
-
- expect(leadingGlyphs(container)).toEqual([
- 'lucide-wrench',
- 'lucide-square-terminal',
- 'lucide-eye'
- ])
- })
-
- it('heads a single-call run with that call\u2019s own glyph', () => {
- const { container } = render(
-
- )
-
- expect(leadingGlyphs(container)).toEqual(['lucide-search', 'lucide-search'])
- })
-
- it.each([1, 3])('labels a run of %s unpaired results without inventing tool calls', (count) => {
- const blocks: NativeChatBlock[] = Array.from({ length: count }, (_, index) => ({
- type: 'tool-result',
- output: `first line ${index}`
- }))
-
- const { container } = render(
-
- )
-
- expect(runHeader(container)).toHaveAccessibleName('Result')
- expect(runHeader(container)).not.toHaveTextContent(/tool calls?/)
-
- expect(leadingGlyphs(container)).toEqual([
- 'lucide-check',
- ...blocks.map(() => 'lucide-chevron-right')
- ])
- })
-
- it('holds the run glyph across live and settled', () => {
- const blocks: NativeChatBlock[] = [
- call('read', { command: "sed -n '1,50p' a.ts", path: 'a.ts' }),
- { type: 'tool-call', name: 'shell', input: { command: 'npm test' }, state: 'running' }
- ]
-
- const { container, rerender } = render(
-
- )
-
- // Keep the run’s identity glyph across state transitions instead of using the latest call’s category.
- expect(leadingGlyphs(container)[0]).toBe('lucide-wrench')
- rerender()
- expect(leadingGlyphs(container)[0]).toBe('lucide-wrench')
- })
- })
-
- it('labels a bare list row by the command it ran rather than an invented path', () => {
- const blocks: NativeChatBlock[] = [
- {
- type: 'tool-call',
- name: 'list',
- input: { command: 'ls', cwd: '/repo' },
- state: 'completed'
- }
- ]
-
- const { container } = render()
-
- expect(container.querySelector('.lucide-folder')).toBeInTheDocument()
- expect(screen.getByTitle('ls')).toHaveTextContent('ls')
- })
-
// Unnamed group-hover matches the surrounding message too, revealing every member caret at once.
- describe('hover reveal', () => {
- const run: NativeChatBlock[] = [
- { type: 'tool-call', name: 'Bash', input: { command: 'ls -la' }, state: 'completed' },
- { type: 'tool-result', output: 'a\nb' },
- { type: 'tool-call', name: 'Bash', input: { command: 'pwd' }, state: 'completed' },
- { type: 'tool-result', output: '/tmp' }
- ]
-
- /** Read class attributes because SVG className is an SVGAnimatedString. */
- function hoverRevealed(container: HTMLElement): { element: Element; classes: string }[] {
- return [...container.querySelectorAll('[class*="group-hover"]')].map((element) => ({
- element,
- classes: element.getAttribute('class') ?? ''
- }))
- }
-
- it('scopes a row’s reveal to that row rather than the whole message', () => {
- // Open run, collapsed children — the state the turn caret leaves behind.
- const { container } = render()
-
- const revealed = hoverRevealed(container)
- expect(revealed.length).toBeGreaterThan(0)
- revealed.forEach(({ classes }) => {
- expect(classes).not.toMatch(/(^|\s)group-hover:/)
- })
- })
-
- it('puts every reveal inside the row button that governs it', () => {
- const { container } = render()
-
- hoverRevealed(container).forEach(({ element, classes }) => {
- const scope = /group-hover\/([a-z-]+):/.exec(classes)?.[1]
- expect(scope).toBeDefined()
- expect(element.closest(`.group\\/${scope}`)).not.toBeNull()
- })
- })
-
- it('keeps every row’s chevron in view at the row’s right edge', () => {
- const { container } = render()
-
- const chevrons = [...container.querySelectorAll('.border-l button svg.lucide-chevron-right')]
- expect(chevrons.length).toBeGreaterThan(1)
- chevrons.forEach((chevron) => {
- expect(chevron.getAttribute('class')).not.toContain('opacity-0')
- })
- })
- })
})
describe('NativeChatToolRun task lists', () => {
diff --git a/src/renderer/src/components/native-chat/NativeChatTranscriptChrome.test.tsx b/src/renderer/src/components/native-chat/NativeChatTranscriptChrome.test.tsx
index 2c1aac283eb..f0eb86ec481 100644
--- a/src/renderer/src/components/native-chat/NativeChatTranscriptChrome.test.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatTranscriptChrome.test.tsx
@@ -1,7 +1,7 @@
// @vitest-environment happy-dom
import { act, createElement } from 'react'
-import { fireEvent, screen, within } from '@testing-library/react'
+
import { createRoot } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { RuntimeFileOperationArgs } from '@/runtime/runtime-file-client-types'
@@ -146,103 +146,6 @@ describe('NativeChatImageAttachments', () => {
}
})
- it('pools visibility observation across image refs', async () => {
- class FakeIntersectionObserver {
- static instances: FakeIntersectionObserver[] = []
- readonly observe = vi.fn()
- readonly unobserve = vi.fn()
- readonly disconnect = vi.fn()
-
- constructor(_callback: IntersectionObserverCallback) {
- FakeIntersectionObserver.instances.push(this)
- }
- }
- vi.stubGlobal('IntersectionObserver', FakeIntersectionObserver)
-
- const container = document.createElement('div')
- const root = createRoot(container)
- await act(async () => {
- root.render(
- createElement(NativeChatImageAttachments, {
- blocks: [
- { type: 'image-ref' as const, path: '/repo/one.png' },
- { type: 'image-ref' as const, path: '/repo/two.png' },
- { type: 'image-ref' as const, path: '/repo/three.png' }
- ],
- runtimeContext: runtimeContext('wt-1')
- })
- )
- await flushPromises()
- })
-
- expect(FakeIntersectionObserver.instances).toHaveLength(1)
- expect(FakeIntersectionObserver.instances[0]?.observe).toHaveBeenCalledTimes(3)
-
- root.unmount()
- expect(FakeIntersectionObserver.instances[0]?.unobserve).toHaveBeenCalledTimes(3)
- expect(FakeIntersectionObserver.instances[0]?.disconnect).toHaveBeenCalledOnce()
- })
-
- it('offers the full-size file of a sent image to the chat copy menu', async () => {
- const container = document.createElement('div')
- const root = createRoot(container)
- await act(async () => {
- root.render(
- createElement(NativeChatImageAttachments, {
- blocks: [{ type: 'image-ref' as const, path: '/repo/image.png' }],
- runtimeContext: runtimeContext('wt-1')
- })
- )
- await flushPromises()
- })
-
- expect(container.querySelector('button')?.getAttribute('data-native-chat-copy-image-src')).toBe(
- 'blob:owner-1'
- )
-
- root.unmount()
- })
-
- it.each([
- 'data:image/png;base64,AA==',
- 'blob:sent-original',
- 'http://example.test/original.png',
- 'https://example.test/original.svg'
- ])(
- 'offers the original displayed source %s in the thumbnail and full-size preview',
- async (src) => {
- const container = document.body.appendChild(document.createElement('div'))
- const root = createRoot(container)
- try {
- await act(async () => {
- root.render(
- createElement(NativeChatImageAttachments, {
- blocks: [{ type: 'image-ref', url: src, alt: 'Sent image' }],
- runtimeContext: runtimeContext('wt-1')
- })
- )
- await flushPromises()
- })
- const thumbnail = within(container).getByRole('button', { name: 'View image: Sent image' })
- expect(thumbnail.getAttribute('data-native-chat-copy-image-src')).toBe(src)
- fireEvent.click(thumbnail)
- const preview = screen.getByRole('dialog', { name: 'Sent image' })
- expect(
- within(preview)
- .getByTestId('pan-zoom-image-canvas')
- .closest('[data-native-chat-copy-image-src]')
- ?.getAttribute('data-native-chat-copy-image-src')
- ).toBe(src)
- expect(window.api.fs.readFile).not.toHaveBeenCalled()
- fireEvent.click(within(preview).getByRole('button', { name: 'Close' }))
- expect(screen.queryByRole('dialog')).toBeNull()
- } finally {
- await act(async () => root.unmount())
- container.remove()
- }
- }
- )
-
it('preserves same-image errors but retries when the runtime owner changes', async () => {
const container = document.createElement('div')
const root = createRoot(container)
@@ -292,62 +195,6 @@ describe('NativeChatImageAttachments', () => {
root.unmount()
})
- it('keeps the shown image when an equal runtime context is rebuilt', async () => {
- const container = document.createElement('div')
- const root = createRoot(container)
- const blocks = [{ type: 'image-ref' as const, path: '/repo/image.png' }]
-
- await act(async () => {
- root.render(
- createElement(NativeChatImageAttachments, {
- blocks,
- runtimeContext: runtimeContext('wt-1')
- })
- )
- await flushPromises()
- })
- const img = container.querySelector('img')
- expect(img?.getAttribute('src')).toBe('blob:owner-1')
-
- for (let update = 0; update < 3; update += 1) {
- await act(async () => {
- root.render(
- createElement(NativeChatImageAttachments, {
- blocks,
- runtimeContext: runtimeContext('wt-1')
- })
- )
- await flushPromises()
- })
- }
-
- expect(URL.revokeObjectURL).not.toHaveBeenCalled()
- expect(window.api.fs.readFile).toHaveBeenCalledOnce()
- expect(container.querySelector('img')).toBe(img)
- expect(img?.getAttribute('src')).toBe('blob:owner-1')
-
- root.unmount()
- })
-
- it('shows a remounted row from the cache without re-reading the image', async () => {
- const props = {
- blocks: [{ type: 'image-ref' as const, path: '/repo/image.png' }],
- runtimeContext: runtimeContext('wt-1')
- }
- for (let mount = 0; mount < 2; mount += 1) {
- const container = document.createElement('div')
- const root = createRoot(container)
- await act(async () => {
- root.render(createElement(NativeChatImageAttachments, props))
- await flushPromises()
- })
- expect(container.querySelector('img')?.getAttribute('src')).toBe('blob:owner-1')
- await act(async () => root.unmount())
- }
- expect(window.api.fs.readFile).toHaveBeenCalledOnce()
- expect(URL.revokeObjectURL).not.toHaveBeenCalled()
- })
-
it('retries a failed thumbnail after the image cache refreshes', async () => {
const container = document.createElement('div')
const root = createRoot(container)
@@ -375,70 +222,4 @@ describe('NativeChatImageAttachments', () => {
expect(container.querySelector('img')?.getAttribute('src')).toBe('blob:owner-2')
root.unmount()
})
-
- it('keeps the observed element stable while a preview is materialized', async () => {
- let callback: IntersectionObserverCallback | undefined
- class FakeIntersectionObserver {
- readonly observe = vi.fn()
- readonly unobserve = vi.fn()
- readonly disconnect = vi.fn()
-
- constructor(nextCallback: IntersectionObserverCallback) {
- callback = nextCallback
- }
- }
- vi.stubGlobal('IntersectionObserver', FakeIntersectionObserver)
-
- const container = document.createElement('div')
- const root = createRoot(container)
- await act(async () => {
- root.render(
- createElement(NativeChatImageAttachments, {
- blocks: [{ type: 'image-ref' as const, path: '/repo/image.png' }],
- runtimeContext: runtimeContext('wt-1')
- })
- )
- await flushPromises()
- })
-
- const observedElement = container.firstElementChild
- expect(observedElement).not.toBeNull()
- if (!observedElement || !callback) {
- throw new Error('image preview did not register visibility observation')
- }
- const notifyVisibility = callback
- await act(async () => {
- notifyVisibility(
- [{ target: observedElement, isIntersecting: true } as IntersectionObserverEntry],
- {} as IntersectionObserver
- )
- await flushPromises()
- })
-
- expect(container.firstElementChild).toBe(observedElement)
- root.unmount()
- })
-
- it.each([
- ['a pasted screenshot in the temp folder', '/tmp/orca-paste-1.png'],
- ['an agent image outside the project', '/Users/me/.codex/generated/plot.png']
- ])('reads %s as a chat image, whoever sent it', async (_label, path) => {
- const container = document.createElement('div')
- const root = createRoot(container)
- await act(async () => {
- root.render(
- createElement(NativeChatImageAttachments, {
- blocks: [{ type: 'image-ref' as const, path }],
- runtimeContext: runtimeContext('wt-1')
- })
- )
- await flushPromises()
- })
-
- expect(vi.mocked(window.api.fs.readFile).mock.calls[0]?.[0]).toMatchObject({
- filePath: path,
- access: { kind: 'chat-image' }
- })
- root.unmount()
- })
})
diff --git a/src/renderer/src/components/native-chat/NativeChatTurnDiffRollup.test.tsx b/src/renderer/src/components/native-chat/NativeChatTurnDiffRollup.test.tsx
index a4b1e1071c8..c037a6970c0 100644
--- a/src/renderer/src/components/native-chat/NativeChatTurnDiffRollup.test.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatTurnDiffRollup.test.tsx
@@ -3,9 +3,9 @@ import '@testing-library/jest-dom/vitest'
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import type { NativeChatMessage } from '../../../../shared/native-chat/native-chat-types'
-import { nativeChatTurnDiffs } from '../../../../shared/native-chat/tools/native-chat-turn-diffs'
+
import { TooltipProvider } from '../ui/tooltip'
-import { NativeChatTurnDiffRollup } from './NativeChatTurnDiffRollup'
+
import { NativeChatMessageList } from './NativeChatMessageList'
import { session, stubLayout, stubResizeObserver } from './native-chat-windowing-test-harness'
@@ -96,29 +96,3 @@ it('keeps landed files visible after work folds, opens a patch in one click and
expect(screen.getByRole('button', { name: /status.ts/ })).toBeVisible()
expect(screen.queryByText('diagnostic-output')).toBeNull()
})
-
-it('mounts a bounded file prefix and no patch bodies until a file is selected', () => {
- const many: NativeChatMessage[] = Array.from({ length: 200 }, (_, index) => ({
- id: `e${index}`,
- role: 'assistant',
- source: 'transcript',
- timestamp: index,
- blocks: [
- { type: 'tool-call', name: 'Diff', input: { path: `file-${index}.ts` } },
- { type: 'tool-result', output: `@@ -1 +1 @@\n-old-${index}\n+new-${index}` }
- ]
- }))
- const diff = nativeChatTurnDiffs(
- many,
- many.map(() => 'u')
- ).get('u')!
- const { container } = render()
- expect(screen.getAllByRole('button', { name: /file-\d+.ts/ })).toHaveLength(20)
- expect(container.querySelectorAll('[data-native-chat-code-content]')).toHaveLength(0)
- fireEvent.click(screen.getByRole('button', { name: /file-0.ts/ }))
- expect(screen.getByText('new-0')).toBeVisible()
- expect(screen.queryByText('new-1')).toBeNull()
- expect(container.querySelectorAll('[data-native-chat-code-content]')).toHaveLength(1)
- fireEvent.click(screen.getByRole('button', { name: /Show 180 more/ }))
- expect(screen.getByRole('button', { name: /file-199.ts/ })).toBeVisible()
-})
diff --git a/src/renderer/src/components/native-chat/NativeChatUsageLimitActions.test.tsx b/src/renderer/src/components/native-chat/NativeChatUsageLimitActions.test.tsx
index 7aeb9b96862..d616995c84c 100644
--- a/src/renderer/src/components/native-chat/NativeChatUsageLimitActions.test.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatUsageLimitActions.test.tsx
@@ -7,13 +7,12 @@ import { NativeChatUsageLimitActionsProvider } from './NativeChatUsageLimitActio
import { NativeChatNoticeRow } from './NativeChatNoticeRow'
import type { NativeChatOrcaStopView } from './native-chat-orca-stop-context'
import { TooltipProvider } from '@/components/ui/tooltip'
-import { useAppStore } from '@/store'
-import { buildAgentSessionContinuationPrompt } from '@/lib/agent-session-continuation'
+
import type { NativeChatMessage } from '../../../../shared/native-chat/native-chat-types'
import type { AgentType } from '../../../../shared/agent-status/contracts/agent-status-types'
import type { RuntimeClientTarget } from '../../../../shared/runtime-client-target'
import type { AgentSessionStatusSummary } from '../../../../shared/agent-session/agent-session-wire'
-import { useNativeChatUsageLimitAutoContinue } from './use-native-chat-usage-limit-auto-continue'
+import type { useNativeChatUsageLimitAutoContinue } from './use-native-chat-usage-limit-auto-continue'
const mocks = vi.hoisted(() => ({
readSnapshot: {
@@ -184,104 +183,6 @@ function expectInlineActions(accountName: string | null) {
expect(account.parentElement).toHaveClass('w-32', 'shrink-0')
}
}
-function snapshot(count: number) {
- return {
- claude: {
- activeAccountId: 'a0',
- accounts: Array.from({ length: count }, (_, index) => ({
- id: `a${index}`,
- email: `account${index}@example.com`,
- authMethod: 'subscription-oauth',
- createdAt: 1,
- updatedAt: 1,
- lastAuthenticatedAt: 1
- }))
- }
- }
-}
-
-it('offers host-owned recovery actions for the current usage-limit cut', async () => {
- const target: RuntimeClientTarget = { kind: 'environment', environmentId: 'chat-host' }
- mocks.snapshot.mockResolvedValue(snapshot(2))
- mocks.select.mockResolvedValue(snapshot(2).claude)
- const original = useAppStore.getState()
- const openSettingsTarget = vi.fn()
- useAppStore.setState({ openSettingsTarget, openSettingsPage: vi.fn() })
- try {
- const view = render(
- row(target, true, 'claude', [], { readCurrent: false, canSwitchAccount: undefined })
- )
- expectInlineActions('Switch Account')
- expect(screen.getByRole('button', { name: 'Switch Account' })).toBeDisabled()
- expect(screen.getByRole('button', { name: 'Hand Off to Another Agent' })).toBeDisabled()
- view.rerender(row(target))
- expect(screen.getByRole('button', { name: 'Switch Account' })).toBeDisabled()
- await waitFor(() =>
- expect(screen.getByRole('button', { name: 'Switch Account' })).toBeEnabled()
- )
- expectInlineActions('Switch Account')
- fireEvent.pointerDown(screen.getByRole('button', { name: 'Switch Account' }), {
- button: 0,
- ctrlKey: false,
- pointerType: 'mouse'
- })
- fireEvent.click(await screen.findByRole('menuitem', { name: 'account1@example.com' }))
- await waitFor(() =>
- expect(mocks.select).toHaveBeenCalledWith(target, {
- accountId: 'a1',
- runtime: 'host',
- wslDistro: null
- })
- )
- fireEvent.click(screen.getByRole('button', { name: 'Hand Off to Another Agent' }))
- await waitFor(() => expect(mocks.dialog).toHaveBeenCalled())
- expect(mocks.dialog.mock.lastCall?.[0].request).toMatchObject({
- worktreeId: 'folder',
- avoidSourceAgent: true,
- launchSource: 'native_chat',
- source: { transcriptPath: '/work/chat.jsonl', sourceAgent: 'claude', sourceTitle: 'My chat' }
- })
- view.rerender(row(target, false))
- expect(screen.queryByRole('button', { name: 'Continue Now' })).toBeNull()
- expect(screen.queryByText('Continue this chat without waiting:')).toBeNull()
- cleanup()
- mocks.snapshot.mockResolvedValue(snapshot(1))
- render(row(target))
- await waitFor(() => expect(screen.queryByRole('button', { name: /Account/ })).toBeNull())
- expect(
- screen.getByText('Continue this chat without waiting:').parentElement?.querySelector('.w-32')
- ).toHaveClass('empty:hidden')
- const watcher = mocks.watch.mock.lastCall?.[1]
- watcher.onSnapshot(snapshot(2))
- await waitFor(() =>
- expect(screen.getByRole('button', { name: 'Switch Account' })).toBeEnabled()
- )
- expectInlineActions('Switch Account')
- cleanup()
- const local: RuntimeClientTarget = { kind: 'local' }
- render(row(local))
- fireEvent.click(await screen.findByRole('button', { name: 'Add Account…' }))
- expect(openSettingsTarget).toHaveBeenCalledWith({
- pane: 'accounts',
- repoId: null,
- sectionId: 'accounts-claude',
- hostScope: local,
- intent: 'native-accounts'
- })
- cleanup()
- mocks.follows = false
- render(row(target, true, 'codex'))
- expect(screen.getByRole('button', { name: 'Hand Off to Another Agent' })).toBeEnabled()
- expect(screen.queryByRole('button', { name: /Account/ })).toBeNull()
- expectInlineActions(null)
- } finally {
- useAppStore.setState({
- openSettingsPage: original.openSettingsPage,
- openSettingsTarget: original.openSettingsTarget
- })
- }
-})
-
it('admits one preparation and discards it after Continue supersedes the cut', async () => {
let answer: (value: typeof mocks.readSnapshot) => void = () => {}
mocks.options.mockImplementation(
@@ -311,72 +212,3 @@ it('admits one preparation and discards it after Continue supersedes the cut', a
fireEvent.click(screen.getByText('Cancel hand-off'))
expect(screen.queryByText('Cancel hand-off')).toBeNull()
})
-
-it('hands off database-backed chats using labelled recent messages when no native file exists', async () => {
- mocks.options.mockResolvedValue({ canSwitchAccount: false })
- render(
- row({ kind: 'local' }, true, 'opencode', [
- {
- id: 'recent',
- role: 'user',
- timestamp: 1,
- source: 'transcript',
- blocks: [{ type: 'text', text: 'Keep the recent task context' }]
- }
- ])
- )
- fireEvent.click(screen.getByRole('button', { name: 'Hand Off to Another Agent' }))
- await waitFor(() => expect(mocks.dialog).toHaveBeenCalled())
- const source = mocks.dialog.mock.lastCall?.[0].request.source
- expect(source.transcriptPath).toBeUndefined()
- expect(buildAgentSessionContinuationPrompt(source, 'focused')).toContain(
- 'A saved session transcript was unavailable, so use these recent chat messages:'
- )
- expect(source.capturedText).toContain('user: Keep the recent task context')
- expect(buildAgentSessionContinuationPrompt(source, 'full')).toBeNull()
-})
-
-it('withdraws the source schedule after hand-off starts without waiting for cancellation', async () => {
- const target: RuntimeClientTarget = { kind: 'environment', environmentId: 'chat-host' }
- const resetsAt = 1_800_000_000_000
- vi.useFakeTimers({ toFake: ['Date'] })
- vi.setSystemTime(resetsAt - 60_000)
- status.set('chat', { usageLimitContinue: { turnItemId: 'cut', resetsAt, state: 'scheduled' } })
- mocks.follows = false
- function ScheduledRow() {
- const automatic = useNativeChatUsageLimitAutoContinue(target, 'chat', {
- turnItemId: 'cut',
- resetsAt,
- turnEndedAt: resetsAt - 120_000
- })
- return row(target, true, 'codex', [], undefined, automatic)
- }
- const view = render()
- fireEvent.click(screen.getByRole('button', { name: 'Hand Off to Another Agent' }))
- fireEvent.click(await screen.findByText('Cancel hand-off'))
- expect(
- mocks.options.mock.calls.some((call) => call[1] === 'agentSession.cancelUsageLimitContinue')
- ).toBe(false)
- fireEvent.click(screen.getByRole('button', { name: 'Hand Off to Another Agent' }))
- await screen.findByText('Start hand-off')
- let finish = () => {}
- mocks.options.mockImplementation(
- () =>
- new Promise((resolve) => {
- finish = () => resolve({})
- })
- )
- fireEvent.click(screen.getByText('Start hand-off'))
- expect(screen.queryByText('Cancel hand-off')).toBeNull()
- expect(mocks.options).toHaveBeenLastCalledWith(target, 'agentSession.cancelUsageLimitContinue', {
- sessionId: 'chat',
- turnItemId: 'cut',
- resetsAt
- })
- status = new Map([
- ['chat', { usageLimitContinue: { turnItemId: 'cut', resetsAt, state: 'cancelled' } }]
- ])
- view.rerender()
- expect(screen.getByText("Orca won't continue this chat automatically.")).toBeInTheDocument()
- finish()
-})