mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
* Fix new workspace composer focus restore * Unify new workspace source selection * WIP: selected source pill in smart workspace name field Co-authored-by: Orca <help@stably.ai> * fix(new-workspace): truncate source pill so it doesn't expand the dialog Co-authored-by: Orca <help@stably.ai> * feat(new-workspace): add open-in-browser button to source pill, fix vertical alignment Co-authored-by: Orca <help@stably.ai> * refactor(new-workspace): drop redundant kind suffix, distinct PR/issue icons, tooltips on pill actions Co-authored-by: Orca <help@stably.ai> * fix(new-workspace): type linked URL into agent input without auto-submit Co-authored-by: Orca <help@stably.ai> * fix(new-workspace): use bracketed-paste for draft URL injection so it actually appears in the agent input Co-authored-by: Orca <help@stably.ai> * feat(agents): per-agent draft injection strategy (codex slow paste, pi/opencode type-chars) Co-authored-by: Orca <help@stably.ai> * fix(agents): smarter TUI-ready heuristic + bracketed paste for codex/pi/opencode Replaces per-agent strategy guesswork with a measured readiness check: title-idle / non-shell-foreground stable for 1.5s / 2.5s minimum floor. Verified against codex, pi, opencode, claude in a node-pty + xterm-headless test rig — bracketed paste lands in the input buffer for all four. Co-authored-by: Orca <help@stably.ai> * refactor(agents): drop unused per-agent draft strategy abstraction The TUI-ready heuristic in agent-paste-draft.ts works for every tested agent (claude/codex/pi/opencode), so the AgentDraftInjectionStrategy field, type-chars + bracketed-paste-slow code paths, and per-agent overrides are dead. Keep the `agent` arg on pasteDraftWhenAgentReady for future per-agent escape hatches without touching every call site. Co-authored-by: Orca <help@stably.ai> * feat(agents): skip draft URL injection for copilot + cursor-agent Both TUIs open with a 'Do you trust this folder?' menu on first launch that consumes keystrokes as menu input — pasting a URL there either selects an arbitrary option or quits the session. Mark them with skipDraftUrlInjection so the workspace still opens cleanly; the user types/pastes the URL themselves once past the trust menu. Co-authored-by: Orca <help@stably.ai> * feat(agents): native --prefill for claude, trust pre-write for cursor/copilot Replaces the empirical TUI-ready waits with two deterministic mechanisms: 1) `claude --prefill <text>` flag — Claude launches with the URL already in its input box, no submit. Eliminates the readiness/paste race entirely for the most common agent. 2) DECSET 2004 (`\x1b[?2004h`) detection on the PTY data stream for every other agent. That escape is the protocol-level "input layer ready, accepting bracketed paste" handshake — emitted by claude/codex/pi/ opencode/gemini/cursor-agent/copilot the moment the input box mounts. We tap it via a sidecar subscription on pty-dispatcher (no interference with the primary xterm handler) and paste as soon as it lands. The 8s budget is now an upper bound, not a target. Cursor-agent and Copilot's "Do you trust this folder?" menus are bypassed by writing the same trust artifacts the CLIs themselves write after the user accepts: - Cursor: `~/.cursor/projects/<slug>/.workspace-trusted` (slug = abs path with leading `/` stripped, remaining `/` → `-`). - Copilot: append cwd to `trustedFolders` in `~/.copilot/config.json` (the same array the bundled `addTrustedFolder` writes). Verified against the cursor-agent CLI bundle (versions/2026.04.17-787b533/ index.js: `_=".workspace-trusted"`) and the @github/copilot 1.0.32 bundle (`isFolderTrusted` / `addTrustedFolder` both read/write `trustedFolders`). Both check via realpath() before string-comparing, so the trust preset canonicalizes too. skipDraftUrlInjection is dropped — both agents now get the draft URL paste once the trust menu is pre-resolved. Tests: 24 passing across tui-agent-startup, agent-trust-presets, pty-dispatcher routing. Co-authored-by: Orca <help@stably.ai> * fix(agents): wait for post-?2004h render burst to settle before paste OpenCode emits DECSET 2004 at ~500ms during alt-screen setup, then runs a 1.3s splash render with NO bytes on the PTY, then paints the actual input box at ~1.85s. Pasting on the bare ?2004h signal lands during the silent gap and the bytes are dropped. The fix: take ?2004h as the necessary precondition, then wait for the TUI's render burst to finish — defined as 1500ms of stream silence after the most recent post-?2004h byte. This captures both the fast TUIs (claude/pi/codex emit setup escapes in one burst then go quiet) and the slow ones (opencode emits, sleeps for the splash, emits again, then goes quiet). Verified against opencode/claude/pi in a node-pty rig: paste lands on the first try with the new strategy. The hard 8s timeout still caps the wait when an agent fails to launch. Co-authored-by: Orca <help@stably.ai> * fix(agents): guard agentTrust IPC so stale preload doesn't crash launch If the preload bundle is older than the renderer (a real situation in electron-vite dev because preload changes only apply on full restart, not HMR), `window.api.agentTrust` is undefined and the launch crashes with "Cannot read properties of undefined (reading 'markTrusted')" before the worktree even opens. Guard the call sites in launch-work-item-direct and useComposerState to skip the trust pre-write when the IPC isn't exposed, and wrap the invoke in try/catch so an IPC error never blocks the launch — the user just sees the trust menu and accepts it manually, same as before this feature shipped. Co-authored-by: Orca <help@stably.ai> * feat(tasks): route 'Use' through the New Workspace dialog instead of yolo-create The Use CTA on the Tasks page used to create+activate a worktree synchronously, which surprised users — the worktree appeared in the sidebar before they had a chance to confirm name / agent / setup. The unified New Workspace dialog landed in this branch already supports opening with a linked work item pre-filled (see openComposerForItem / openComposerForLinearItem), so just route Use through it. The launchWorkItemDirect helper stays exported for ProjectViewWrapper, which has its own UX where the immediate-create flow is the right call. Co-authored-by: Orca <help@stably.ai> * test(agents): include `agent` field in autohand startup-plan assertion Merging main brought in the Autohand Code agent test (PR #1382), which predated this branch's addition of `agent` to AgentStartupPlan. Aligning the assertion fixes the lone CI test failure on this PR. Co-authored-by: Orca <help@stably.ai> * refactor(agents): drop unused expectedProcess arg + snapshot sidecar set Two minor follow-ups from self-review: 1. `pasteDraftWhenAgentReady` no longer reads `expectedProcess` — readiness is gated on DECSET 2004 alone now, not on PTY foreground process. Drop it from the signature and from the two callers (launch-work-item-direct, new-workspace). 2. The pty-dispatcher's sidecar fan-out iterates the live Set, which is safe against deleting the current element but not against a watcher that synchronously subscribes a sibling. Snapshot via Array.from before the loop. Cheap (Set is tiny) and removes the latent footgun. No behavior change. Co-authored-by: Orca <help@stably.ai> * test(e2e): match the unified smart-name input's new placeholder The CreateFromTab refactor in this branch replaced the separate "Workspace name" Input with a single SmartWorkspaceNameField whose default-mode placeholder is "Type a name, #1234, branch, GitHub or Linear URL". The worktree-create e2e test was still anchoring on the old "Workspace name" text and could not find the input. Update the placeholder regex to match the new copy. Free-form text typed into smart mode is treated as a workspace name by submitQuick — same contract the test used before. Verified locally: targeted e2e passes in 2.2s. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai>
158 lines
6.6 KiB
TypeScript
158 lines
6.6 KiB
TypeScript
/**
|
|
* E2E tests for the "Create Workspace" flow in Orca.
|
|
*
|
|
* Why: the old 'create-worktree' modal was replaced by the composer modal
|
|
* (`activeModal === 'new-workspace-composer'`) in #710. A prior version of
|
|
* this spec bypassed the UI entirely — it called `state.createWorktree(...)`
|
|
* directly on the store — which is why the #1186 regression (a React #31
|
|
* crash when `StartFromField` rendered the new `getBaseRefDefault` envelope
|
|
* as JSX) shipped despite a green suite.
|
|
*
|
|
* The spec now drives the real user flow: open the composer, type a
|
|
* workspace name, click Create, and assert the worktree actually
|
|
* materialized and became active. See `tests/e2e/AGENTS.md` for the rule
|
|
* that E2E assertions must target the DOM, not the store.
|
|
*
|
|
* Note: the original StartFromField regression guard was removed with #1191
|
|
* (Tabbed Create Workspace), which deleted StartFromField/StartFromPicker
|
|
* entirely. The render-error sweep below still catches any React #31-class
|
|
* crash in whatever replaces it.
|
|
*/
|
|
|
|
import type { ConsoleMessage } from '@stablyai/playwright-test'
|
|
import { test, expect } from './helpers/orca-app'
|
|
import {
|
|
waitForSessionReady,
|
|
waitForActiveWorktree,
|
|
getActiveWorktreeId,
|
|
ensureTerminalVisible,
|
|
worktreeExists
|
|
} from './helpers/store'
|
|
|
|
test.describe('Create Workspace', () => {
|
|
test.beforeEach(async ({ orcaPage }) => {
|
|
await waitForSessionReady(orcaPage)
|
|
await waitForActiveWorktree(orcaPage)
|
|
})
|
|
|
|
test('creates a worktree through the composer UI and activates it', async ({ orcaPage }) => {
|
|
const worktreeIdBefore = await getActiveWorktreeId(orcaPage)
|
|
|
|
// Capture render errors for the #1186 guard. React logs "Objects are not
|
|
// valid as a React child" via console.error before throwing the
|
|
// minified-production error #31; capture both paths so the test fails
|
|
// loudly whether the build is dev or prod.
|
|
const pageErrors: Error[] = []
|
|
orcaPage.on('pageerror', (err) => {
|
|
pageErrors.push(err)
|
|
})
|
|
const consoleErrors: string[] = []
|
|
const onConsole = (msg: ConsoleMessage): void => {
|
|
if (msg.type() === 'error') {
|
|
consoleErrors.push(msg.text())
|
|
}
|
|
}
|
|
orcaPage.on('console', onConsole)
|
|
|
|
const workspaceName = `e2e-create-${Date.now()}`
|
|
|
|
try {
|
|
// 1. Open the composer. Using the store setter (not clicking the
|
|
// sidebar affordance) keeps the spec stable under sidebar refactors;
|
|
// the modal open path itself is not what #1186 broke.
|
|
await orcaPage.evaluate(() => {
|
|
window.__store?.getState().openModal('new-workspace-composer')
|
|
})
|
|
|
|
const dialog = orcaPage.getByRole('dialog', { name: /Create Workspace/i })
|
|
await expect(dialog).toBeVisible()
|
|
|
|
// Wait for the composer to settle. The card fires several async effects
|
|
// on mount (detected-agent probe, repo combobox autofocus + hydration,
|
|
// setup-hooks fetch). Clicking before those settle can race Radix's
|
|
// FocusScope reparenting.
|
|
await expect(dialog.getByRole('combobox').first()).toBeVisible()
|
|
|
|
// Force the `getBaseRefDefault` IPC to round-trip so any consumer that
|
|
// renders the envelope (e.g. SourceControl) has a chance to crash
|
|
// inside the open modal's React tree — the console/pageerror sweep
|
|
// below is what catches #1186-class regressions now that the
|
|
// StartFromField trigger no longer exists (#1191).
|
|
await orcaPage.evaluate(async () => {
|
|
const repoId = Object.values(window.__store!.getState().worktreesByRepo).flat()[0]?.repoId
|
|
if (!repoId) {
|
|
return
|
|
}
|
|
await window.api.repos.getBaseRefDefault({ repoId })
|
|
})
|
|
await orcaPage.waitForTimeout(100)
|
|
|
|
// 3. Type the workspace name into the unified smart-name input.
|
|
// The composer's default mode is 'smart'; its placeholder advertises
|
|
// multiple input shapes ("Type a name, #1234, branch, GitHub or
|
|
// Linear URL"). Plain free-form text is treated as a workspace name
|
|
// by submitQuick, which is what we want here.
|
|
const nameInput = dialog.getByPlaceholder(/Type a name/i)
|
|
await expect(nameInput).toBeVisible()
|
|
await nameInput.fill(workspaceName)
|
|
|
|
// 4. Click Create Workspace. This fires the full submitQuick path:
|
|
// createWorktree IPC, applyWorktreeMeta, activateAndRevealWorktree,
|
|
// and closeModal via onCreated.
|
|
const createButton = dialog.getByRole('button', { name: /Create Workspace/i })
|
|
await expect(createButton).toBeEnabled()
|
|
await createButton.click()
|
|
|
|
// 5. The modal closes once submitQuick completes successfully. If
|
|
// something inside the flow threw (IPC failure, hook error), the modal
|
|
// would stay open with a createError banner — catch that as a fail.
|
|
await expect(dialog).toBeHidden({ timeout: 15_000 })
|
|
|
|
// 6. The new worktree must actually exist on disk and in the store.
|
|
await expect
|
|
.poll(async () => worktreeExists(orcaPage, workspaceName), {
|
|
timeout: 10_000,
|
|
message: `Worktree "${workspaceName}" did not appear in the store`
|
|
})
|
|
.toBe(true)
|
|
|
|
// 7. The new worktree must become active (different from whatever was
|
|
// active before we opened the composer).
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
const id = await getActiveWorktreeId(orcaPage)
|
|
return id !== null && id !== worktreeIdBefore
|
|
},
|
|
{ timeout: 10_000, message: 'New worktree did not become the active worktree' }
|
|
)
|
|
.toBe(true)
|
|
|
|
// 8. A terminal tab must auto-create for the new worktree. This is
|
|
// the downstream signal that `activateAndRevealWorktree` actually
|
|
// fired, not just that the store row exists.
|
|
await ensureTerminalVisible(orcaPage)
|
|
|
|
// Final render-error sweep. Any render crash during the flow (whether
|
|
// it tore down the modal or bubbled past it) shows up here.
|
|
expect(pageErrors, `pageerror fired: ${pageErrors.map((e) => e.message).join(', ')}`).toEqual(
|
|
[]
|
|
)
|
|
const reactChildErrors = consoleErrors.filter((text) =>
|
|
/Objects are not valid as a React child|Minified React error #31/i.test(text)
|
|
)
|
|
expect(reactChildErrors, `React render error: ${reactChildErrors.join(', ')}`).toEqual([])
|
|
} finally {
|
|
orcaPage.off('console', onConsole)
|
|
// Best-effort close if the test failed mid-flow and left the modal open.
|
|
await orcaPage
|
|
.evaluate(() => {
|
|
window.__store?.getState().closeModal()
|
|
})
|
|
.catch(() => {
|
|
/* page may already be torn down */
|
|
})
|
|
}
|
|
})
|
|
})
|