Files
orca/tests/e2e/native-chat-copy-image.spec.ts
Brennan Benson 76cf164cf9 Chat UI leaves Experimental: each agent's Chat or CLI choice decides what opens (#36110)
* Chat UI leaves Experimental: each agent's Chat | CLI choice decides launches

Remove the experimentalNativeChat switch and every gate on it. The Chat
settings page, Default Agent Chat | CLI choice, + menu Chat/CLI rows,
source-control surface choice, Antigravity chat section and orchestration
chat-error switch are always present. What a new launch opens comes only
from the per-agent choice (no saved choice opens a chat) plus the existing
support checks.

- One lookup, nativeChatPreferenceFor, keyed by agent id so host-registered
  agents read the same way; unreadable settings still open a terminal.
- A relayed desktop stamp (nativeChatDefault) maps onto the launched agent;
  an explicit surface still wins. Old phones' createSupport keeps admitting
  by the host's per-agent choice.
- settings.get stops publishing the retired switch keys; nothing in the
  repo reads them from a host.
- Startup chat sync and hydration run when some agent can open as a chat
  or the machine already holds chats.
- The retired key is stripped on load. The one-time upgrade tip from the
  chat upgrade is retired with its audience record; a new record, captured
  once from the raw profile, marks which existing profiles had Chat UI off.
- The window-side AI-button tab path, which only ran with the switch off,
  is deleted.

* Keep profiles that had Chat UI off on the terminal

Existing users who never turned Chat UI on would otherwise see new agent
launches open as chats, since an agent without a saved choice now opens as
a chat. The one-time load step that records the new audience also sets
every known agent to CLI for those profiles, in the same save and only
when the record was absent, overwriting saved chat choices the old switch
masked. Profiles that had Chat UI on and new profiles are untouched.

Browser profiles keep their own settings, so the web client does the same
once: a stored switch that is not on seeds CLI choices, and dropping the
key marks the step done.

Agents first seen after this step open as chats.

* Agents without a saved Chat | CLI choice open as their CLI

An agent with no saved choice now opens in the terminal for everyone,
including fresh installs and agents first seen later. The one-time load
step carries what launches opened while Chat UI was experimental: with
the switch on, every known agent without a saved choice is saved as
chat; with it off, chat choices the switch masked are saved as CLI.
Existing profiles join the launch tip audience whether the switch was on
or off. Browser profiles get the same carry from their saved key.

The startup chat restore and host agent list now run only when some
agent is saved as chat or the runtime holds chats.

* Preserve owned terminal launches and local chat choices

* Clarify chat and terminal support

* Explain worker CLI integration timeout budget

* Use checked annotations for chat graduation test fixtures

* Store agent surface default separately from per-agent overrides

* Preserve explicit Chat intent across default changes

* Preserve current browser settings during host replies

* Correct browser settings factory and regression types

* Name exported launch fixture mock types and clarify agent defaults

* Preserve settings fallbacks and complete launch test lifecycles

* Check allocated tab identity before launch fixture interpolation

* Register combined integration project for compiler parity probes

* Use checked settings in the launch test fixture

* Preserve retired Chat preference data privately

* Align chat E2E fixtures and capture pane lifecycle evidence

* Use typed raw settings in retention contracts

* Consolidate transcript diagnostic imports

* test: align chat fixtures with live pane and native menu contracts

* test: capture relay tab selection loss diagnostics

* test: wait for owned shells before synthetic chat fixtures

* test: preserve all-host visibility in SSH observer

* test: bind synthetic transcript fixtures before provider seeding

* test: assert selected chat owner after narrow image preview

* ci: allow fifth compiler project to complete comparison

* Type the web-terminal fixture's chat map without an assertion

* Right-click the selection itself in the chat copy spec
2026-10-30 17:43:04 -07:00

119 lines
5.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { randomUUID } from 'node:crypto'
import { rm, writeFile } from 'node:fs/promises'
import path from 'node:path'
import { test, expect } from './helpers/orca-app'
import { ensureTerminalVisible, waitForActiveWorktree, waitForSessionReady } from './helpers/store'
import { waitForActivePaneHookDescriptor, waitForActiveTerminalManager } from './helpers/terminal'
test('Copy image puts the full-size image on the clipboard and keeps the preview open', async ({
orcaPage,
electronApp,
testRepoPath
}) => {
await waitForSessionReady(orcaPage)
await waitForActiveWorktree(orcaPage)
await ensureTerminalVisible(orcaPage)
await waitForActiveTerminalManager(orcaPage)
const descriptor = await waitForActivePaneHookDescriptor(orcaPage)
// An 800×600 copy proves the saved file was read instead of its 64×64 chip preview.
const [png, pastedPng] = await electronApp.evaluate(({ nativeImage }) =>
[800, 64].map((width) => {
const height = (width * 3) / 4
return nativeImage
.createFromBitmap(Buffer.alloc(width * height * 4, 200), { width, height })
.toPNG()
.toString('base64')
})
)
// Why the repo: previews only read files under allowed roots; the test output dir isn't one.
const imageName = `copy-image-proof-${randomUUID()}.png`
const imagePath = path.join(testRepoPath, imageName)
await writeFile(imagePath, Buffer.from(png, 'base64'))
try {
// Substitute only the clipboard IPC; never touch the user's system clipboard.
await electronApp.evaluate(
({ ipcMain, nativeImage }, { imagePath }) => {
ipcMain.removeHandler('clipboard:saveImageAsTempFile')
ipcMain.handle('clipboard:saveImageAsTempFile', () => imagePath)
ipcMain.removeHandler('clipboard:writeImage')
ipcMain.handle('clipboard:writeImage', (_event, dataUrl: string) => {
const { width, height } = nativeImage.createFromDataURL(dataUrl).getSize()
process.env.ORCA_E2E_COPIED_IMAGE_SIZE = `${width}x${height}`
})
},
{ imagePath }
)
const takeCopiedImageSize = (): Promise<string | null> =>
electronApp.evaluate(() => {
const size = process.env.ORCA_E2E_COPIED_IMAGE_SIZE ?? null
delete process.env.ORCA_E2E_COPIED_IMAGE_SIZE
return size
})
await orcaPage.evaluate(async ({ paneKey, worktreeId }) => {
const settings = await window.api.settings.set({
nativeChatProviders: { claude: true, codex: true }
})
const store = window.__store
if (!store) {
throw new Error('Store unavailable')
}
store.setState({ settings })
const state = store.getState()
state.setAgentStatus(
paneKey,
{ state: 'done', agentType: 'omp', prompt: '' },
'OMP',
undefined,
{ worktreeId }
)
const [tabId] = paneKey.split(':')
const tab = (state.unifiedTabsByWorktree[worktreeId] ?? []).find(
(candidate) => candidate.contentType === 'terminal' && candidate.entityId === tabId
)
if (!tab) {
throw new Error('Terminal tab unavailable')
}
state.toggleTabViewMode(tab.id)
}, descriptor)
const composer = orcaPage.getByRole('textbox', {
name: 'Ask anything, @ to mention files, / for commands',
exact: true
})
await expect(composer).toBeVisible()
await composer.evaluate((element, png) => {
const data = new DataTransfer()
const bytes = Uint8Array.from(atob(png), (character) => character.charCodeAt(0))
data.items.add(new File([bytes], 'pasted.png', { type: 'image/png' }))
element.dispatchEvent(
new ClipboardEvent('paste', { bubbles: true, cancelable: true, clipboardData: data })
)
}, pastedPng)
const thumbnail = orcaPage.getByRole('button', { name: `View image: ${imageName}` })
await expect(thumbnail).toHaveAttribute('data-native-chat-copy-image-src', /^blob:/)
const copyImage = orcaPage.getByRole('menuitem', { name: 'Copy image' })
await thumbnail.click({ button: 'right' })
await copyImage.click()
await expect.poll(takeCopiedImageSize).toBe('800x600')
await thumbnail.click()
const preview = orcaPage.getByRole('dialog')
await expect(preview.getByRole('img', { name: imageName })).toBeVisible()
// Why: the image ignores the pointer so the pan-and-zoom canvas under it takes the click.
const previewCanvas = preview.getByTestId('pan-zoom-image-canvas')
await previewCanvas.click({ button: 'right' })
await copyImage.click()
await expect.poll(takeCopiedImageSize).toBe('800x600')
// Wait for menu dismissal before checking the preview stayed open after its click handler.
await expect(orcaPage.getByRole('menu')).toHaveCount(0)
await expect(preview).toHaveAttribute('data-state', 'open')
await previewCanvas.click({ button: 'right' })
await expect(copyImage).toBeVisible()
await preview.getByRole('button', { name: 'Close' }).click()
await expect(preview).toHaveCount(0)
} finally {
await rm(imagePath, { force: true })
}
})