mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 17:37:40 +00:00
* plugins: orca plugin shot --editor, and plugin open --editor opens the file's folder plugin open --editor now opens the file in the workspace that holds it, matching its real path too (/tmp vs /private/tmp), and otherwise adds the file's folder as a folder workspace instead of failing with no_workspace. plugin shot <dir|key> --editor <file> [--theme] [--width --height] [--act] loads the editor afresh in a hidden tab, waits for it to get its document and ask for it (a new editor.subscribed frame event), captures the editor's guest with the panel shot's capture path, and sends --act clicks, drags and keys as real input via sendInputEvent. It exits 1 with the frame's errors when the editor throws, is blocked by its content policy, or never asks for its document within 5s; theme and size are put back. New RPC method plugins.shotEditor so older hosts fail cleanly. * plugins: shot --editor never changes the author's file With --act the shot copies the file into a temporary folder, shoots the copy in a temporary folder workspace, then closes its tab and removes the workspace and folder; --in-place acts on the file itself. Without --act it opens the real file, leaves its buffer alone and closes the tab if the shot opened it. Afterwards Orca shows the workspace it showed before. Regenerates the skill's CLI reference. * plugins: fix editor shot typecheck (refusal next step, RPC test mock) * plugins: editor shot targets its own guest, flushes before reload, scopes cleanup per capture - Main captures the exact guest WebContents the shot's tab mounted, not the first with its URL. - An open editor's pending edits are flushed before the shot reloads it; the shot fails if not. - --act --in-place keeps the acted tab and its unsaved edits. - end-capture undoes only its own capture (per-capture id), so overlapping shots don't clash. - The file's workspace lookup includes local folder workspaces. * plugins: shot --status-bar/--window, key acts focus the editor guest, panel shot selects a workspace - Editor --act keys first give the guest's <webview> DOM focus and refocus its target, so a canvas that keeps focus no longer drops later keys; never webContents.focus(), which focuses the window. - orca plugin shot <dir|key> --status-bar | --window [--theme]: captures Orca's status bar or the whole hidden window (plugins.shotChrome) with the existing capture helpers. - With no workspace showing, a panel shot selects this machine's most recent workspace in the hidden window instead of failing. * plugins: a shot's end restores only its own capture overrides * plugins: regenerate the testing runtime with the editor subscribed signal * plugins: editor capture refusals go through one CLI-message helper * fix(plugins): give the chrome shot refusal a next step
164 lines
6.1 KiB
TypeScript
164 lines
6.1 KiB
TypeScript
/**
|
|
* Invariant: `orca plugin shot --editor` opens a file in a plugin's custom editor from the hidden
|
|
* test window, waits for the editor to get and ask for its document, sends `--act` input into the
|
|
* editor's isolated guest on a temporary copy of the file, and captures what it drew without showing or focusing the window. A
|
|
* file no workspace holds opens its folder as a folder workspace; an editor that never asks for its
|
|
* document, or throws, fails the command.
|
|
*/
|
|
|
|
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import type { ElectronApplication } from '@stablyai/playwright-test'
|
|
import { expect, test } from './helpers/orca-app'
|
|
import { runCompiledOrcaCli } from './helpers/compiled-orca-cli'
|
|
import { disablePluginSystem, enableDevPlugins } from './helpers/plugin-smoke'
|
|
import { ISOLATED_FRAMES_E2E } from './helpers/plugin-frame-guest'
|
|
import { waitForActiveWorktree, waitForSessionReady } from './helpers/store'
|
|
|
|
test.use(ISOLATED_FRAMES_E2E)
|
|
|
|
// Red after a click anywhere, so the image proves the act reached the guest before the capture.
|
|
const EDITOR_HTML = `<!doctype html>
|
|
<html><body style="margin:0">
|
|
<div id="box" style="width:100vw;height:100vh;background:#123456"></div>
|
|
<script>
|
|
orca.document.onDidChange(() => {})
|
|
document.addEventListener('mousedown', () => {
|
|
document.getElementById('box').style.background = '#ff0000'
|
|
})
|
|
</script>
|
|
</body></html>`
|
|
|
|
const BROKEN_HTML = `<!doctype html><html><body><script>throw new Error('editor exploded')</script></body></html>`
|
|
|
|
/** Share of pure red pixels, decoded in Electron's main process. */
|
|
function redShare(app: ElectronApplication, path: string): Promise<number> {
|
|
return app.evaluate(({ nativeImage }, file) => {
|
|
const bgra = nativeImage.createFromPath(file).toBitmap()
|
|
let red = 0
|
|
for (let at = 0; at + 3 < bgra.length; at += 4) {
|
|
if ((bgra[at + 2] ?? 0) > 200 && (bgra[at + 1] ?? 0) < 60 && (bgra[at] ?? 0) < 60) {
|
|
red += 1
|
|
}
|
|
}
|
|
return red / Math.max(1, bgra.length / 4)
|
|
}, path)
|
|
}
|
|
|
|
async function editorPlugin(root: string, id: string, extension: string, html: string) {
|
|
await mkdir(join(root, id, 'editor'), { recursive: true })
|
|
await writeFile(
|
|
join(root, id, 'orca-plugin.json'),
|
|
JSON.stringify({
|
|
manifestVersion: 2,
|
|
execution: 'sandboxed-ui',
|
|
id,
|
|
name: id,
|
|
version: '1.0.0',
|
|
engines: { orca: '>=1.4.246' },
|
|
pluginApi: 1,
|
|
contributes: {
|
|
customEditors: [{ id, title: id, extensions: [extension], entry: 'editor/index.html' }]
|
|
}
|
|
})
|
|
)
|
|
await writeFile(join(root, id, 'editor', 'index.html'), html)
|
|
return join(root, id)
|
|
}
|
|
|
|
test('plugin shot --editor opens the file, acts in the guest and fails a broken editor', async ({
|
|
electronApp,
|
|
orcaPage
|
|
}) => {
|
|
await waitForSessionReady(orcaPage)
|
|
await waitForActiveWorktree(orcaPage)
|
|
const tempRoot = await mkdtemp(join(tmpdir(), 'orca-editor-shot-'))
|
|
try {
|
|
const good = await editorPlugin(tempRoot, 'red-click', '.redclick', EDITOR_HTML)
|
|
const broken = await editorPlugin(tempRoot, 'broken-editor', '.broken', BROKEN_HTML)
|
|
// Outside every workspace: the shot opens this folder as a folder workspace.
|
|
const docs = join(tempRoot, 'docs')
|
|
await mkdir(docs)
|
|
await writeFile(join(docs, 'a.redclick'), 'x')
|
|
await writeFile(join(docs, 'b.broken'), 'x')
|
|
await enableDevPlugins(orcaPage, [good, broken])
|
|
const userDataDir = await electronApp.evaluate(({ app }) => app.getPath('userData'))
|
|
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
const run = await runCompiledOrcaCli(userDataDir, [
|
|
'plugin',
|
|
'shot',
|
|
'local.red-click',
|
|
'--editor',
|
|
join(docs, 'a.redclick'),
|
|
'--width',
|
|
'320',
|
|
'--height',
|
|
'200',
|
|
'--act',
|
|
'[{"click":[20,20]}]',
|
|
'--out',
|
|
join(tempRoot, 'acted.png'),
|
|
'--json'
|
|
])
|
|
return run.json?.ok ? run.json.result : run.stderr
|
|
},
|
|
{ timeout: 60_000, message: 'the editor shot never succeeded' }
|
|
)
|
|
.toMatchObject({ width: 320, height: 200, subscribed: true, errors: [], acted: 1 })
|
|
expect(await redShare(electronApp, join(tempRoot, 'acted.png'))).toBeGreaterThan(0.9)
|
|
// The acts ran on a copy: the file is unchanged, and no tab or workspace of the copy is left.
|
|
expect(await readFile(join(docs, 'a.redclick'), 'utf8')).toBe('x')
|
|
expect(
|
|
await orcaPage.evaluate(() => {
|
|
const state = window.__store?.getState()
|
|
return {
|
|
tabs: state?.openFiles.filter((file) => file.filePath.endsWith('a.redclick')).length,
|
|
scratch: state?.repos.filter((repo) => repo.path.includes('orca-editor-shot-')).length
|
|
}
|
|
})
|
|
).toEqual({ tabs: 0, scratch: 0 })
|
|
expect(
|
|
await electronApp.evaluate(({ BrowserWindow }) =>
|
|
BrowserWindow.getAllWindows().some((window) => window.isVisible() || window.isFocused())
|
|
)
|
|
).toBe(false)
|
|
|
|
const failed = await runCompiledOrcaCli(userDataDir, [
|
|
'plugin',
|
|
'shot',
|
|
'local.broken-editor',
|
|
'--editor',
|
|
join(docs, 'b.broken'),
|
|
'--out',
|
|
join(tempRoot, 'broken.png')
|
|
])
|
|
expect(failed.code).toBe(1)
|
|
expect(failed.stdout).toContain('never asked for its document')
|
|
expect(failed.stdout).toContain('editor exploded')
|
|
|
|
// Orca's own chrome, where plugins draw status bar items, from the hidden window too.
|
|
const statusBar = await runCompiledOrcaCli(userDataDir, [
|
|
'plugin',
|
|
'shot',
|
|
'local.red-click',
|
|
'--status-bar',
|
|
'--out',
|
|
join(tempRoot, 'status-bar.png'),
|
|
'--json'
|
|
])
|
|
expect(statusBar.json?.result).toMatchObject({ region: 'status-bar', height: 24 })
|
|
expect(
|
|
await electronApp.evaluate(({ BrowserWindow }) =>
|
|
BrowserWindow.getAllWindows().some((window) => window.isVisible() || window.isFocused())
|
|
)
|
|
).toBe(false)
|
|
} finally {
|
|
await disablePluginSystem(orcaPage).catch(() => undefined)
|
|
await rm(tempRoot, { recursive: true, force: true })
|
|
}
|
|
})
|