mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 09:27:29 +00:00
* feat(plugins): orca plugin open and shot; dev watch --shot - orca plugin open <key> [--panel|--editor] shows a panel in the right sidebar or a file in the plugin's custom editor, by changing renderer state only (no focus or window raise). - orca plugin shot <key> [--panel] [--theme] [--width] [--out] [--json] waits for the panel's current code to draw, then crops webContents.capturePage to the panel (html guests included), with background throttling off while hidden. --width and --theme are temporary in-memory overrides; the PNG returns base64 (5 MiB cap), so --environment works. - dev watch --shot <dir> writes a PNG per panel after the load and each good reload. - The authoring skill's loop is now dev watch --check --shot → read the PNG → plugin test. * fix(plugins): shots never disturb a visible Orca - shot --theme is skipped while the Orca window is on screen (themeApplied: false with a themeReason); the override applies only to a hidden or minimized window. - dev watch --shot opens the first panel (or --shot-panel <id>) once, then shoots it with ifShown: if the user moved the sidebar away it emits one shot_skipped instead of switching. * fix(plugins): dev loop dogfood fixes; orca plugin dev emit - dev watch: approval re-run command drops watch-only flags and old --approve; a folder added by a refused load is owned by the next approved load (Ctrl-C unloads it); check events carry the plugin's status (no green while pending/errored); reloaded prints before that reload's logs; --json errors are one NDJSON line. - Errors: log, banner and check share source-mapped positions (a line-1 prelude hands sucrase mappings to the components runtime); check dedupes issues and locates syntax errors; Orca runtime frames and the error module's bogus location are dropped from logs. - .orca/types tsconfig sets preserveSymlinks (tsc 7 loaded typings twice under /tmp). - Templates' READMEs use dev watch. - orca plugin dev emit <dir|key> <event> [--data] sends a test event to a dev plugin's worker through the real delivery rules (plugins.devEmit). * test(plugins): fixed timestamp in the dev emit test * fix(plugins): shot captures an html panel's guest itself On Linux the main window's capturePage leaves a <webview> guest blank. The renderer now reports the guest's rect, and main captures the guest's own webContents and draws it over the window capture at that rect (raw bitmaps, nativeImage only). Components panels are drawn by the host, so the window capture alone stays right for them. * refactor(plugins): keep plugin-service and main-process-plugins under max-lines After rebasing onto #37013 both files crossed 300 lines. The guest log sink moves into the desktop view binding (bindPluginDesktopViews), and plugin-service tightens emitEvent and its event-delivery imports.
129 lines
5.0 KiB
TypeScript
129 lines
5.0 KiB
TypeScript
/**
|
|
* Invariant: saving a dev components panel hot reloads it. Keyed memory atoms survive the reload
|
|
* (unless their `version` changes), a "Reloaded" mark shows, and a save that does not load keeps
|
|
* the last good tree with an inline error naming file:line:column until the next save fixes it.
|
|
*/
|
|
|
|
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import type { Page } from '@stablyai/playwright-test'
|
|
import { expect, test } from './helpers/orca-app'
|
|
import { disablePluginSystem, enableDevPlugins } from './helpers/plugin-smoke'
|
|
import { ISOLATED_FRAMES_E2E } from './helpers/plugin-frame-guest'
|
|
|
|
test.use(ISOLATED_FRAMES_E2E)
|
|
|
|
const TITLE = 'Hot Reload'
|
|
|
|
function panelSource(label: string, draftOptions = ''): string {
|
|
return [
|
|
"import { atom, Heading, Input, read, set, Stack, Text } from 'orca'",
|
|
'',
|
|
`const draft = atom('draft', ''${draftOptions})`,
|
|
'',
|
|
'export default function Panel() {',
|
|
' return (',
|
|
' <Stack>',
|
|
` <Heading>Panel ${label}</Heading>`,
|
|
' <Input label="Draft" placeholder="Draft" value={read(draft)} onChange={(v) => set(draft, v)} />',
|
|
" <Text>Draft: {read(draft) || '(empty)'}</Text>",
|
|
' </Stack>',
|
|
' )',
|
|
'}',
|
|
''
|
|
].join('\n')
|
|
}
|
|
|
|
async function openPanel(page: Page): Promise<void> {
|
|
await page.evaluate(async () => {
|
|
const store = window.__store?.getState()
|
|
if (store && !store.rightSidebarOpen) {
|
|
store.toggleRightSidebar()
|
|
}
|
|
await window.api.plugins.refresh()
|
|
})
|
|
const button = page.getByRole('button', { name: TITLE })
|
|
await expect(button).toBeVisible({ timeout: 15_000 })
|
|
await button.click()
|
|
}
|
|
|
|
test('a dev components panel hot reloads with its memory atoms and survives a broken save', async ({
|
|
orcaPage
|
|
}) => {
|
|
const tempRoot = await mkdtemp(join(tmpdir(), 'orca-components-hot-reload-'))
|
|
const pluginRoot = join(tempRoot, 'hot-reload')
|
|
const panelFile = join(pluginRoot, 'src', 'panel.tsx')
|
|
try {
|
|
await mkdir(join(pluginRoot, 'src'), { recursive: true })
|
|
await writeFile(
|
|
join(pluginRoot, 'orca-plugin.json'),
|
|
JSON.stringify({
|
|
manifestVersion: 2,
|
|
execution: 'sandboxed-ui',
|
|
id: 'hot-reload',
|
|
name: TITLE,
|
|
version: '1.0.0',
|
|
engines: { orca: '>=1.4.246' },
|
|
pluginApi: 1,
|
|
contributes: {
|
|
panels: [{ id: 'main', title: TITLE, entry: 'src/panel.tsx', ui: 'components' }]
|
|
}
|
|
})
|
|
)
|
|
await writeFile(panelFile, panelSource('v1'))
|
|
await enableDevPlugins(orcaPage, [pluginRoot])
|
|
await openPanel(orcaPage)
|
|
await expect(orcaPage.getByRole('heading', { name: 'Panel v1' })).toBeVisible({
|
|
timeout: 15_000
|
|
})
|
|
|
|
// Filling is idempotent, so a retry after the dev watcher's own first remount stays exact.
|
|
await expect(async () => {
|
|
await orcaPage.getByPlaceholder('Draft').fill('kept across saves')
|
|
await expect(orcaPage.getByText('Draft: kept across saves')).toBeVisible({ timeout: 2_000 })
|
|
}).toPass({ timeout: 15_000 })
|
|
|
|
await writeFile(panelFile, panelSource('v2'))
|
|
await expect(orcaPage.getByRole('heading', { name: 'Panel v2' })).toBeVisible({
|
|
timeout: 15_000
|
|
})
|
|
await expect(orcaPage.getByText('Draft: kept across saves')).toBeVisible()
|
|
await expect(orcaPage.getByText('Reloaded', { exact: true })).toBeVisible()
|
|
|
|
// A save that does not transpile: the v2 tree stays, with the error located in the source.
|
|
await writeFile(panelFile, "import { atom } from 'orca'\n\nconst label = 'broken'\nconst = 1\n")
|
|
const loadError = orcaPage.getByRole('alert').filter({ hasText: 'failed to load' })
|
|
await expect(loadError).toContainText('src/panel.tsx:4:7: Unexpected token', {
|
|
timeout: 15_000
|
|
})
|
|
await expect(orcaPage.getByRole('heading', { name: 'Panel v2' })).toBeVisible()
|
|
await expect(orcaPage.getByText('Draft: kept across saves')).toBeVisible()
|
|
|
|
// A throw is located at the source column (as `orca plugin check` reports), not the
|
|
// type-stripped one Chromium's stack names.
|
|
await writeFile(
|
|
panelFile,
|
|
"import { atom } from 'orca'\n\nconst n: number = Number('1'); if (n) throw new Error('boom')\n"
|
|
)
|
|
await expect(loadError).toContainText('src/panel.tsx:3:45: boom', { timeout: 15_000 })
|
|
|
|
await writeFile(panelFile, panelSource('v3'))
|
|
await expect(orcaPage.getByRole('heading', { name: 'Panel v3' })).toBeVisible({
|
|
timeout: 15_000
|
|
})
|
|
await expect(loadError).toHaveCount(0)
|
|
await expect(orcaPage.getByText('Draft: kept across saves')).toBeVisible()
|
|
|
|
// A new `version` means a new shape: that atom starts over instead of taking the old value.
|
|
await writeFile(panelFile, panelSource('v4', ', { version: 2 }'))
|
|
await expect(orcaPage.getByRole('heading', { name: 'Panel v4' })).toBeVisible({
|
|
timeout: 15_000
|
|
})
|
|
await expect(orcaPage.getByText('Draft: (empty)')).toBeVisible()
|
|
} finally {
|
|
await disablePluginSystem(orcaPage).catch(() => undefined)
|
|
await rm(tempRoot, { recursive: true, force: true })
|
|
}
|
|
})
|