From 8e31d98b5c6419d7d3ace54e814cbb37b1b7c973 Mon Sep 17 00:00:00 2001 From: Arul Sharma <31745423+arul28@users.noreply.github.com> Date: Thu, 14 May 2026 03:02:35 -0400 Subject: [PATCH 01/24] perf(boot): defer Linear quick view visibility check boot open-project score 694.26 -> 204.38; removed ade.cto.getLinearConnectionStatus 2039ms from startup summary --- .../components/app/LinearQuickViewButton.tsx | 7 ++++++- .../src/renderer/components/app/TopBar.test.tsx | 12 ++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/renderer/components/app/LinearQuickViewButton.tsx b/apps/desktop/src/renderer/components/app/LinearQuickViewButton.tsx index 31ee35ab0..a80142f68 100644 --- a/apps/desktop/src/renderer/components/app/LinearQuickViewButton.tsx +++ b/apps/desktop/src/renderer/components/app/LinearQuickViewButton.tsx @@ -15,6 +15,8 @@ import { LinearIssueBrowser, linearBrowserIssueToLaneIssue } from "./LinearIssue type PopoverPosition = { top: number; right: number } | null; +const INITIAL_VISIBILITY_CHECK_DELAY_MS = 8_000; + export function LinearQuickViewButton() { const project = useAppStore((s) => s.project); const refreshLanes = useAppStore((s) => s.refreshLanes); @@ -42,15 +44,18 @@ export function LinearQuickViewButton() { setVisible(false); setOpen(false); setQuickView(null); - void loadVisibility() + const timer = window.setTimeout(() => { + void loadVisibility() .then((nextVisible) => { if (!cancelled) setVisible(nextVisible); }) .catch(() => { if (!cancelled) setVisible(false); }); + }, INITIAL_VISIBILITY_CHECK_DELAY_MS); return () => { cancelled = true; + window.clearTimeout(timer); }; }, [loadVisibility, project?.rootPath]); diff --git a/apps/desktop/src/renderer/components/app/TopBar.test.tsx b/apps/desktop/src/renderer/components/app/TopBar.test.tsx index faf078181..aa97101c6 100644 --- a/apps/desktop/src/renderer/components/app/TopBar.test.tsx +++ b/apps/desktop/src/renderer/components/app/TopBar.test.tsx @@ -566,6 +566,11 @@ describe("TopBar", () => { render(); + await act(async () => { + window.dispatchEvent(new Event("focus")); + await Promise.resolve(); + }); + fireEvent.click(await screen.findByRole("button", { name: /linear quick view/i })); await waitFor(() => { @@ -617,6 +622,13 @@ describe("TopBar", () => { render(); + expect(getLinearConnectionStatus).not.toHaveBeenCalled(); + expect(screen.queryByRole("button", { name: /linear quick view/i })).toBeNull(); + + await act(async () => { + window.dispatchEvent(new Event("focus")); + await Promise.resolve(); + }); await waitFor(() => { expect(getLinearConnectionStatus).toHaveBeenCalledTimes(1); }); From a35ae3cfa997420407578335fd92c3e80750e444 Mon Sep 17 00:00:00 2001 From: Arul Sharma <31745423+arul28@users.noreply.github.com> Date: Thu, 14 May 2026 03:05:40 -0400 Subject: [PATCH 02/24] fix(tui): stream daemon chat events to ade code Adapt multi-project runtime chat envelopes from runtime/event into the TUI onChatEvent stream; verified attached print-state and focused connection tests. --- .../tuiClient/__tests__/connection.test.ts | 87 +++++++++++++++++++ apps/ade-cli/src/tuiClient/connection.ts | 66 +++++++++++++- 2 files changed, 149 insertions(+), 4 deletions(-) diff --git a/apps/ade-cli/src/tuiClient/__tests__/connection.test.ts b/apps/ade-cli/src/tuiClient/__tests__/connection.test.ts index bcdbfb181..178d68d06 100644 --- a/apps/ade-cli/src/tuiClient/__tests__/connection.test.ts +++ b/apps/ade-cli/src/tuiClient/__tests__/connection.test.ts @@ -221,6 +221,93 @@ describe("connectToAde embedded mode", () => { expect(requests.at(-1)?.params).toMatchObject({ projectId: "project-daemon" }); }); + it("adapts multi-project runtime chat events into the TUI chat stream", async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "ade-code-connection-")); + const socketPath = path.join(tmpDir, "ade.sock"); + const serverSocketRef: { current: net.Socket | null } = { current: null }; + const requests: Array<{ method: string; params?: Record }> = []; + const server = net.createServer((socket) => { + serverSocketRef.current = socket; + let buffer = ""; + socket.on("data", (chunk) => { + buffer += chunk.toString("utf8"); + while (true) { + const newline = buffer.indexOf("\n"); + if (newline < 0) return; + const line = buffer.slice(0, newline).trim(); + buffer = buffer.slice(newline + 1); + if (!line) continue; + const request = JSON.parse(line) as { id: number; method: string; params?: Record }; + requests.push({ method: request.method, params: request.params }); + const result = (() => { + if (request.method === "ade/initialize") { + return { + runtimeInfo: { multiProject: true }, + capabilities: { projects: true }, + }; + } + if (request.method === "projects.add") { + return { projectId: "project-daemon", rootPath: project.projectRoot }; + } + if (request.method === "runtimeEvents.subscribe") { + return { subscriptionId: "runtime-sub-1" }; + } + if (request.method === "runtimeEvents.unsubscribe") { + return { removed: true }; + } + return null; + })(); + socket.write(`${JSON.stringify({ jsonrpc: "2.0", id: request.id, result })}\n`); + } + }); + }); + await new Promise((resolve) => server.listen(socketPath, resolve)); + + const connection = await connectToAde({ + project, + socketPath, + }); + try { + const delivered = new Promise((resolve) => { + connection.onChatEvent(resolve); + }); + await vi.waitUntil( + () => requests.some((request) => request.method === "runtimeEvents.subscribe"), + { timeout: 1000 }, + ); + expect(requests.find((request) => request.method === "runtimeEvents.subscribe")?.params) + .toMatchObject({ projectId: "project-daemon", category: "runtime" }); + + const envelope = { + sessionId: "chat-1", + timestamp: "2026-05-14T00:00:00.000Z", + event: { type: "text", text: "hello from daemon" }, + sequence: 1, + } as AgentChatEventEnvelope; + serverSocketRef.current?.write(`${JSON.stringify({ + jsonrpc: "2.0", + method: "runtime/event", + params: { + subscriptionId: "runtime-sub-1", + projectId: "project-daemon", + event: { + id: 1, + timestamp: "2026-05-14T00:00:00.000Z", + category: "runtime", + payload: envelope, + }, + }, + })}\n`); + + await expect(delivered).resolves.toEqual(envelope); + } finally { + await connection.close(); + serverSocketRef.current?.destroy(); + await new Promise((resolve) => server.close(() => resolve())); + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + it("spawns the standalone binary directly when no CLI script entrypoint exists", async () => { const socketPath = useMissingMachineSocket(); const missingEntrypointDir = fs.mkdtempSync( diff --git a/apps/ade-cli/src/tuiClient/connection.ts b/apps/ade-cli/src/tuiClient/connection.ts index 864c5dc17..9a0ce122e 100644 --- a/apps/ade-cli/src/tuiClient/connection.ts +++ b/apps/ade-cli/src/tuiClient/connection.ts @@ -279,6 +279,20 @@ function isMultiProjectRuntime(result: InitializeResult): boolean { ); } +function isAgentChatEventEnvelope(value: unknown): value is AgentChatEventEnvelope { + if (!isRecord(value)) return false; + if (typeof value.sessionId !== "string" || typeof value.timestamp !== "string") { + return false; + } + const event = value.event; + return isRecord(event) && typeof event.type === "string"; +} + +function runtimeChatEnvelope(event: BufferedEvent): AgentChatEventEnvelope | null { + if (event.category !== "runtime") return null; + return isAgentChatEventEnvelope(event.payload) ? event.payload : null; +} + function withProjectId( method: string, params: unknown, @@ -356,7 +370,8 @@ async function connectAttachedSocket(args: { "ADE RPC socket did not finish initialization.", ); let request = rawRequest; - if (isMultiProjectRuntime(initializeResult)) { + const multiProjectRuntime = isMultiProjectRuntime(initializeResult); + if (multiProjectRuntime) { const project = await rawRequest("projects.add", { rootPath: args.project.projectRoot, }); @@ -382,10 +397,53 @@ async function connectAttachedSocket(args: { socketPath: args.socketPath, request, ...createAdeActionHelpers(request), - onChatEvent: (callback: (event: AgentChatEventEnvelope) => void) => - attachedClient.onNotification("chat/event", (params) => + onChatEvent: (callback: (event: AgentChatEventEnvelope) => void) => { + const stopChatNotification = attachedClient.onNotification("chat/event", (params) => callback(params as AgentChatEventEnvelope), - ), + ); + if (!multiProjectRuntime) return stopChatNotification; + + let disposed = false; + let subscriptionId: string | null = null; + const pending: RuntimeEventNotification[] = []; + const stopRuntimeNotification = attachedClient.onNotification("runtime/event", (params) => { + const payload = params as RuntimeEventNotification; + if (!isBufferedEvent(payload.event)) return; + if (!subscriptionId) { + pending.push(payload); + return; + } + if (payload.subscriptionId !== subscriptionId) return; + const envelope = runtimeChatEnvelope(payload.event); + if (envelope) callback(envelope); + }); + request<{ subscriptionId: string }>("runtimeEvents.subscribe", { + category: "runtime", + cursor: 0, + limit: 100, + }).then((response) => { + if (disposed) { + request("runtimeEvents.unsubscribe", { subscriptionId: response.subscriptionId }).catch(() => {}); + return; + } + subscriptionId = response.subscriptionId; + for (const payload of pending.splice(0)) { + if (payload.subscriptionId !== subscriptionId || !isBufferedEvent(payload.event)) continue; + const envelope = runtimeChatEnvelope(payload.event); + if (envelope) callback(envelope); + } + }).catch(() => { + stopRuntimeNotification(); + }); + return () => { + disposed = true; + stopChatNotification(); + stopRuntimeNotification(); + if (subscriptionId) { + request("runtimeEvents.unsubscribe", { subscriptionId }).catch(() => {}); + } + }; + }, subscribeRuntimeEvents: async (subscriptionArgs, callback) => { let subscriptionId: string | null = null; const pending: PendingRuntimeEvent[] = []; From 0b67b157caa086a58a27f6cd1b445906b69208dd Mon Sep 17 00:00:00 2001 From: Arul Sharma <31745423+arul28@users.noreply.github.com> Date: Thu, 14 May 2026 03:08:38 -0400 Subject: [PATCH 03/24] perf(lanes): avoid snapshot refresh for local git state --- .../src/renderer/components/lanes/LaneGitActionsPane.test.tsx | 2 +- .../src/renderer/components/lanes/LaneGitActionsPane.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/renderer/components/lanes/LaneGitActionsPane.test.tsx b/apps/desktop/src/renderer/components/lanes/LaneGitActionsPane.test.tsx index f62bf5e59..c536eee50 100644 --- a/apps/desktop/src/renderer/components/lanes/LaneGitActionsPane.test.tsx +++ b/apps/desktop/src/renderer/components/lanes/LaneGitActionsPane.test.tsx @@ -324,7 +324,7 @@ describe("LaneGitActionsPane rescue action", () => { await waitFor(() => { expect(mockStoreState.refreshLanes).toHaveBeenCalledWith({ includeStatus: true, - includeSnapshots: true, + includeSnapshots: false, }); }); await waitFor(() => { diff --git a/apps/desktop/src/renderer/components/lanes/LaneGitActionsPane.tsx b/apps/desktop/src/renderer/components/lanes/LaneGitActionsPane.tsx index 1f7844d7f..eb39d6090 100644 --- a/apps/desktop/src/renderer/components/lanes/LaneGitActionsPane.tsx +++ b/apps/desktop/src/renderer/components/lanes/LaneGitActionsPane.tsx @@ -721,7 +721,7 @@ export function LaneGitActionsPane({ const refreshLaneGitState = useCallback(async (targetLaneId: string | null) => { await Promise.all([ refreshChanges(targetLaneId), - refreshLanes({ includeStatus: true, includeSnapshots: true }), + refreshLanes({ includeStatus: true, includeSnapshots: false }), refreshGitMeta(targetLaneId), ]); }, [refreshChanges, refreshGitMeta, refreshLanes]); From 8af850dacad9818c0c3fdcbe5f58160898810d37 Mon Sep 17 00:00:00 2001 From: Arul Sharma <31745423+arul28@users.noreply.github.com> Date: Thu, 14 May 2026 03:19:54 -0400 Subject: [PATCH 04/24] feat(agents): bundle ADE operating skills --- apps/desktop/package.json | 7 ++ .../agent-skills/ade-app-control/SKILL.md | 51 +++++++++++++ .../agent-skills/ade-browser/SKILL.md | 37 ++++++++++ .../ade-cli-control-plane/SKILL.md | 32 ++++++++ .../agent-skills/ade-cto-missions/SKILL.md | 45 ++++++++++++ .../agent-skills/ade-ios-simulator/SKILL.md | 73 +++++++++++++++++++ .../agent-skills/ade-lanes-git/SKILL.md | 49 +++++++++++++ .../agent-skills/ade-macos-vm/SKILL.md | 35 +++++++++ .../agent-skills/ade-pr-workflows/SKILL.md | 37 ++++++++++ .../agent-skills/ade-proof-artifacts/SKILL.md | 33 +++++++++ .../scripts/validate-mac-artifacts.mjs | 2 + .../scripts/validate-win-artifacts.mjs | 2 + .../services/ai/tools/systemPrompt.test.ts | 6 +- .../chat/claudeSlashCommandDiscovery.test.ts | 51 ++++++++++++- .../chat/claudeSlashCommandDiscovery.ts | 72 +++++++++++++++--- .../services/memory/skillRegistryService.ts | 16 +++- apps/desktop/src/shared/adeCliGuidance.ts | 41 +++-------- 17 files changed, 538 insertions(+), 51 deletions(-) create mode 100644 apps/desktop/resources/agent-skills/ade-app-control/SKILL.md create mode 100644 apps/desktop/resources/agent-skills/ade-browser/SKILL.md create mode 100644 apps/desktop/resources/agent-skills/ade-cli-control-plane/SKILL.md create mode 100644 apps/desktop/resources/agent-skills/ade-cto-missions/SKILL.md create mode 100644 apps/desktop/resources/agent-skills/ade-ios-simulator/SKILL.md create mode 100644 apps/desktop/resources/agent-skills/ade-lanes-git/SKILL.md create mode 100644 apps/desktop/resources/agent-skills/ade-macos-vm/SKILL.md create mode 100644 apps/desktop/resources/agent-skills/ade-pr-workflows/SKILL.md create mode 100644 apps/desktop/resources/agent-skills/ade-proof-artifacts/SKILL.md diff --git a/apps/desktop/package.json b/apps/desktop/package.json index a2ad7c074..dddf4e406 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -228,6 +228,13 @@ "filter": [ "**/*" ] + }, + { + "from": "resources/agent-skills", + "to": "agent-skills", + "filter": [ + "**/*" + ] } ], "afterPack": "./scripts/after-pack-runtime-fixes.cjs", diff --git a/apps/desktop/resources/agent-skills/ade-app-control/SKILL.md b/apps/desktop/resources/agent-skills/ade-app-control/SKILL.md new file mode 100644 index 000000000..6267ae912 --- /dev/null +++ b/apps/desktop/resources/agent-skills/ade-app-control/SKILL.md @@ -0,0 +1,51 @@ +--- +name: ade-app-control +description: Use this skill when inspecting, launching, logging, clicking, typing, or selecting context from Electron apps through ADE App Control and the `ade app-control` CLI. +--- + +# ADE App Control + +## Use socket mode + +App Control is a live desktop drawer service. Prefer socket-backed commands: + +```bash +ade help app-control +ade --socket app-control status --text +ade --socket app-control launch --command "npm run dev" --text +ade --socket app-control connect --cdp-port --text +``` + +ADE sets `ADE_APP_CONTROL_CDP_PORT` and `ADE_APP_CONTROL_DEBUG_FLAGS` for launches. Custom Electron launchers should forward one of those values to `--remote-debugging-port`. + +## Inspect + +```bash +ade --socket app-control snapshot --text +ade --socket app-control elements --text +ade --socket app-control select --x --y --text +``` + +Use Inspect mode or `select` to return screenshot-backed DOM, selector, and source context. When the session is chat-owned, ADE can attach the selection to the drawer chat. + +## Act + +```bash +ade --socket app-control click --x --y --text +ade --socket app-control type --value "text" --text +``` + +Use Control mode for input. Re-snapshot after meaningful UI changes. + +## Logs and terminal + +Start with App Control status, then prefer App Control terminal/log commands: + +```bash +ade --socket app-control logs --text --max-bytes 8388608 +ade --socket app-control terminal write --data "y\n" +ade --socket app-control terminal signal --signal SIGINT +``` + +Only fall back to `ade --socket terminal list --text` and `ade --socket terminal read ...` when no App Control terminal is active. + diff --git a/apps/desktop/resources/agent-skills/ade-browser/SKILL.md b/apps/desktop/resources/agent-skills/ade-browser/SKILL.md new file mode 100644 index 000000000..fb72ef03d --- /dev/null +++ b/apps/desktop/resources/agent-skills/ade-browser/SKILL.md @@ -0,0 +1,37 @@ +--- +name: ade-browser +description: Use this skill when using ADE's built-in browser pane, shared browser tabs, screenshots, page inspection, or browser context selection through `ade browser`. +--- + +# ADE browser + +## Scope + +The ADE browser is global, not lane-scoped. Use socket mode so CLI calls and the Work sidebar share the same tabs. + +## Common commands + +```bash +ade help browser +ade --socket browser panel --text +ade --socket browser status --text +ade --socket browser open --new-tab --text +ade --socket browser tabs --text +ade --socket browser switch --tab --text +ade --socket browser screenshot --text +``` + +For inspection and chat context: + +```bash +ade --socket browser inspect-start --text +ade --socket browser select-current --text +ade --socket browser clear-selection --text +``` + +## Gotchas + +- Open localhost URLs and chat-output links in the ADE browser when the user expects them to show in the Work sidebar. +- Because tabs are global, confirm the active tab before taking a screenshot or selecting context. +- If there is no active browser panel/session, report the blocker rather than pretending to inspect the page. + diff --git a/apps/desktop/resources/agent-skills/ade-cli-control-plane/SKILL.md b/apps/desktop/resources/agent-skills/ade-cli-control-plane/SKILL.md new file mode 100644 index 000000000..43c0feff4 --- /dev/null +++ b/apps/desktop/resources/agent-skills/ade-cli-control-plane/SKILL.md @@ -0,0 +1,32 @@ +--- +name: ade-cli-control-plane +description: Use this skill when an agent needs to inspect or operate ADE itself through the `ade` CLI, including lanes, chats, actions, memory, proof, runtime/socket state, or help/flag discovery. +--- + +# ADE CLI control plane + +## Core rule + +Use normal shell commands for local repo edits, tests, and Git inspection. Use `ade` when you need ADE state or ADE-owned services: lanes, chats, missions, PR metadata, memory, proof/artifacts, managed terminals, App Control, iOS Simulator, browser, macOS VM, settings, usage, updates, or service actions. + +## First checks + +1. Run `ade doctor --text` when the ADE environment is unclear. +2. Run `ade help ` or `ade help ` before guessing flags. +3. Prefer `--text` for human-readable output and JSON output when scripting. +4. Use `ade actions list --text` or `ade actions list --domain --text` as the escape hatch for service methods without a typed command. + +## Socket mode + +Use `--socket` when the CLI and ADE desktop drawer must share live state. This matters for App Control, iOS Simulator, Preview Lab, browser tabs, terminal logs, context selection, and proof drawer updates. + +## Fallback path + +If `command -v ade` fails: + +1. Try `${ADE_CLI_PATH:-}` if set. +2. Try `${ADE_CLI_BIN_DIR:-}/ade` if set. +3. In an ADE source checkout, after confirming it exists, use `node apps/ade-cli/dist/cli.cjs ...`. + +The normal reason to skip ADE CLI is that it is truly unreachable after these fallbacks. + diff --git a/apps/desktop/resources/agent-skills/ade-cto-missions/SKILL.md b/apps/desktop/resources/agent-skills/ade-cto-missions/SKILL.md new file mode 100644 index 000000000..acb58b78b --- /dev/null +++ b/apps/desktop/resources/agent-skills/ade-cto-missions/SKILL.md @@ -0,0 +1,45 @@ +--- +name: ade-cto-missions +description: Use this skill when operating ADE CTO, missions, coordinator tools, worker agents, Linear routing, multi-agent orchestration, or mission run inspection. +--- + +# ADE CTO and missions + +## CTO + +Use CTO commands for team-lead state and Work chats: + +```bash +ade cto state --text +ade cto chats --text +ade help cto +``` + +## Missions + +Use missions for orchestrated multi-step work: + +```bash +ade missions list --text +ade missions launch --prompt "..." --manual --text +ade missions watch --text +ade missions graph --text +ade missions runs --text +``` + +## Coordinator and Linear + +```bash +ade coordinator --help +ade linear workflows --text +ade linear run --text +ade linear sync --text +``` + +## Operating rules + +- Keep worker briefs small and specific. +- Use project memory for non-obvious conventions and past pitfalls, not for information derivable from code or git. +- When polling long-running mission/worker state, return compact summaries instead of pasting full logs. +- If a worker result conflicts with repo evidence, inspect the files yourself before merging its conclusion. + diff --git a/apps/desktop/resources/agent-skills/ade-ios-simulator/SKILL.md b/apps/desktop/resources/agent-skills/ade-ios-simulator/SKILL.md new file mode 100644 index 000000000..dbd16fac7 --- /dev/null +++ b/apps/desktop/resources/agent-skills/ade-ios-simulator/SKILL.md @@ -0,0 +1,73 @@ +--- +name: ade-ios-simulator +description: Use this skill when working with ADE iOS Simulator, Preview Lab, SwiftUI preview rendering, simulator screenshots, taps, streams, or iOS drawer context via `ade ios-sim`. +--- + +# ADE iOS Simulator and Preview Lab + +## Start here + +Use socket mode so CLI actions and the desktop drawer share one simulator session: + +```bash +ade --socket ios-sim status --text +ade --socket ios-sim devices --text +ade --socket ios-sim apps --text +ade help ios-sim launch +``` + +Launch with a target from `apps`: + +```bash +ade --socket ios-sim launch --target --text +``` + +## Inspect and interact + +Capture current screen/context before acting: + +```bash +ade --socket ios-sim snapshot --text +ade --socket ios-sim elements --text +ade --socket ios-sim select --x --y --text +``` + +Interact with the running app: + +```bash +ade --socket ios-sim tap --x --y --text +ade --socket ios-sim drag --start-x --start-y --end-x --end-y --text +ade --socket ios-sim type --value "text" --text +``` + +## Streams + +Use `stream-status` to explain the active backend, latency, fallback reason, and blockers: + +```bash +ade --socket ios-sim window-start --fps 60 --text +ade --socket ios-sim live-start --fps 30 --text +ade --socket ios-sim stream-status --text +ade --socket ios-sim stream-stop --text +``` + +Low idle fps is normal on `iosurface-indigo` because frames are event-driven when the simulator is still. + +## Preview Lab + +For SwiftUI preview work: + +```bash +ade --socket ios-sim preview-status --text +ade --socket ios-sim previews --source --text +ade --socket ios-sim preview-render --source --index --text +``` + +Add a preview only when no useful nearby preview already exists. Preview fixtures must not require live sync, keychain, network, push, sockets, or production databases. + +## Gotchas + +- Do not create symlink projects, fake schemes, or repo-layout shims as the first fix for app detection. Re-run `ade --socket ios-sim apps --text` and report the selected project, scheme, and build output. +- If no simulator/session/snapshot exists, report the exact blocker instead of guessing the screen. +- When you own the simulator session and the task no longer needs it, run `ade --socket ios-sim shutdown --text`. + diff --git a/apps/desktop/resources/agent-skills/ade-lanes-git/SKILL.md b/apps/desktop/resources/agent-skills/ade-lanes-git/SKILL.md new file mode 100644 index 000000000..49e2f8453 --- /dev/null +++ b/apps/desktop/resources/agent-skills/ade-lanes-git/SKILL.md @@ -0,0 +1,49 @@ +--- +name: ade-lanes-git +description: Use this skill when creating, inspecting, syncing, committing, pushing, archiving, or rebasing ADE lanes and lane worktrees through `ade lanes` and `ade git`. +--- + +# ADE lanes and git + +## Lanes + +Lanes are ADE-managed git worktrees and branches. + +```bash +ade lanes list --text +ade lanes show --text +ade lanes create --name --description "..." --text +ade lanes child --lane --name --text +ade lanes archive --text +``` + +## ADE-aware Git + +Use ADE git commands when the operation should update ADE operation state and refresh lane status: + +```bash +ade git status --lane --text +ade git status --full --lane --text +ade git sync --lane --rebase --base main --text +ade git stage --lane +ade git stage-all --lane +ade git commit --lane -m "message" +ade git push --lane --set-upstream --text +``` + +## Rebase and conflicts + +```bash +ade git rebase --lane --ai --text +ade git conflict show --lane --text +ade git rebase continue --lane --text +``` + +For conflicts, inspect both sides and preserve intent from both branches. Do not blindly accept ours/theirs. + +## Gotchas + +- Use `--lane` for anything other than the active workspace. +- Use `ade diff changes --lane --text` when you need ADE's view of file changes. +- Do not archive or delete lanes unless the user asked for cleanup or the release workflow explicitly requires it. + diff --git a/apps/desktop/resources/agent-skills/ade-macos-vm/SKILL.md b/apps/desktop/resources/agent-skills/ade-macos-vm/SKILL.md new file mode 100644 index 000000000..1ad8a0f01 --- /dev/null +++ b/apps/desktop/resources/agent-skills/ade-macos-vm/SKILL.md @@ -0,0 +1,35 @@ +--- +name: ade-macos-vm +description: Use this skill when starting, inspecting, guiding, screenshotting, selecting, clicking, typing, or troubleshooting ADE lane-tied macOS VMs through `ade macos-vm`. +--- + +# ADE macOS VM + +## Start + +macOS VMs are lane-tied agent workspaces. + +```bash +ade help macos-vm +ade --socket macos-vm status --lane --text +ade --socket macos-vm start --lane --create --text +ade --socket macos-vm guide --lane --text +``` + +## Interact + +```bash +ade --socket macos-vm screenshot --lane --text +ade --socket macos-vm select --lane --x --y --text +ade --socket macos-vm click --lane +ade --socket macos-vm type --lane --value "text" +``` + +Click/select coordinates are window-relative by default. + +## Gotchas + +- Keep code edits under the guest shared path described by `guide`. +- Confirm provider readiness from `status` before promising VM interaction. +- If the VM is missing or blocked, report the exact status and next action. + diff --git a/apps/desktop/resources/agent-skills/ade-pr-workflows/SKILL.md b/apps/desktop/resources/agent-skills/ade-pr-workflows/SKILL.md new file mode 100644 index 000000000..b362f4f78 --- /dev/null +++ b/apps/desktop/resources/agent-skills/ade-pr-workflows/SKILL.md @@ -0,0 +1,37 @@ +--- +name: ade-pr-workflows +description: Use this skill when working with ADE PR workflows including PR tab data, PR checks/comments, queues, Path to Merge, rebase resolver, issue resolver agents, CI fixes, or merge readiness. +--- + +# ADE PR workflows + +## Start with typed PR commands + +```bash +ade prs list --text +ade prs show --text +ade prs checks --text +ade prs comments --text +ade prs path-to-merge --model --max-rounds 3 --no-auto-merge --text +``` + +Use `ade help prs` and `ade help git rebase` before guessing PR or rebase flags. + +## Use actions for niche surfaces + +```bash +ade actions list --domain pr --text +ade actions list --domain issue_inventory --text +ade actions run --input-json '{"key":"value"}' +``` + +## Resolver rules + +- Preserve both the lane's intent and main's intent during conflicts. +- Read conflict files and surrounding call sites before choosing a side. +- For review-thread or CI work, fetch current checks/comments first; do not rely on stale PR tab state. +- Prefer focused fixes and rerun the smallest relevant check before escalating to broader validation. + +## Release readiness + +Before treating a PR as merge-ready, verify working tree cleanliness, pushed branch status, required checks, unresolved review threads, and whether rebasing/merging main introduced conflicts or semantic drift. diff --git a/apps/desktop/resources/agent-skills/ade-proof-artifacts/SKILL.md b/apps/desktop/resources/agent-skills/ade-proof-artifacts/SKILL.md new file mode 100644 index 000000000..79e9a1341 --- /dev/null +++ b/apps/desktop/resources/agent-skills/ade-proof-artifacts/SKILL.md @@ -0,0 +1,33 @@ +--- +name: ade-proof-artifacts +description: Use this skill when the user asks for proof, screenshots, video, artifacts, test evidence, computer-use capture, or when work should appear in ADE's proof drawer. +--- + +# ADE proof and artifacts + +## Rule + +When the user asks to capture, send, attach, or provide proof, create evidence with the relevant tool, then register it through ADE so it appears in the proof drawer for the active chat, mission, or lane. + +## Commands + +```bash +ade proof status --text +ade proof list --text +ade proof screenshot --text +ade proof record --seconds 20 --text +ade help proof +``` + +## What counts as proof + +- Screenshot or video of the UI state. +- App Control, iOS Simulator, ADE browser, or macOS VM capture. +- Test output or log bundle when visual proof is not the right artifact. + +## Gotchas + +- Do not leave proof as an unregistered local file when the user expects ADE to show it. +- Include enough context in the artifact name/description to understand what was verified. +- Clean up stale processes you started before declaring proof complete. + diff --git a/apps/desktop/scripts/validate-mac-artifacts.mjs b/apps/desktop/scripts/validate-mac-artifacts.mjs index c5361b6df..9561dbab3 100644 --- a/apps/desktop/scripts/validate-mac-artifacts.mjs +++ b/apps/desktop/scripts/validate-mac-artifacts.mjs @@ -322,6 +322,7 @@ async function validatePackagedRuntime(appPath, description) { const adeCliTuiPath = path.join(resourcesPath, "ade-cli", "tuiClient", "cli.mjs"); const adeCliBinPath = path.join(resourcesPath, "ade-cli", "bin", "ade"); const adeCliInstallerPath = path.join(resourcesPath, "ade-cli", "install-path.sh"); + const bundledAgentSkillsPath = path.join(resourcesPath, "agent-skills", "ade-cli-control-plane", "SKILL.md"); const iosSimHelperRoot = path.join(resourcesPath, "native", "ios-sim-helpers"); const iosSimHelperBuildScript = path.join(iosSimHelperRoot, "build.sh"); const nodeModulesPath = path.join(unpackedPath, "node_modules"); @@ -338,6 +339,7 @@ async function validatePackagedRuntime(appPath, description) { await assertPathExists(adeCliTuiPath, "bundled ADE CLI TUI entry"); await assertPathExists(adeCliBinPath, "bundled ADE CLI wrapper"); await assertPathExists(adeCliInstallerPath, "bundled ADE CLI PATH installer"); + await assertPathExists(bundledAgentSkillsPath, "bundled ADE agent skills"); await assertPathExists(iosSimHelperBuildScript, "bundled iOS simulator helper build script"); await assertPathExists(path.join(iosSimHelperRoot, "sim-capture.swift"), "bundled iOS simulator capture helper source"); await assertPathExists(path.join(iosSimHelperRoot, "sim-input.m"), "bundled iOS simulator input helper source"); diff --git a/apps/desktop/scripts/validate-win-artifacts.mjs b/apps/desktop/scripts/validate-win-artifacts.mjs index 96450589d..8d0d1acc0 100644 --- a/apps/desktop/scripts/validate-win-artifacts.mjs +++ b/apps/desktop/scripts/validate-win-artifacts.mjs @@ -456,6 +456,7 @@ async function validatePackagedRuntime(appDir) { const adeCliTuiPath = path.join(resourcesPath, "ade-cli", "tuiClient", "cli.mjs"); const adeCliBinPath = path.join(resourcesPath, "ade-cli", "bin", "ade.cmd"); const adeCliInstallerPath = path.join(resourcesPath, "ade-cli", "install-path.cmd"); + const bundledAgentSkillsPath = path.join(resourcesPath, "agent-skills", "ade-cli-control-plane", "SKILL.md"); const nodeModulesPath = path.join(unpackedPath, "node_modules"); const nodePtyModulePath = path.join(nodeModulesPath, "node-pty"); const sqlJsModulePath = path.join(nodeModulesPath, "sql.js"); @@ -482,6 +483,7 @@ async function validatePackagedRuntime(appDir) { await assertPathExists(adeCliTuiPath, "bundled ADE CLI TUI entry"); await assertPathExists(adeCliBinPath, "bundled ADE CLI wrapper"); await assertPathExists(adeCliInstallerPath, "bundled ADE CLI PATH installer"); + await assertPathExists(bundledAgentSkillsPath, "bundled ADE agent skills"); await assertPathExists(nodePtyModulePath, "unpacked node-pty module"); await assertPathExists(sqlJsModulePath, "unpacked sql.js module"); await assertPathExists(path.join(onnxRuntimeWinPath, "onnxruntime_binding.node"), "Windows ONNX Runtime native addon"); diff --git a/apps/desktop/src/main/services/ai/tools/systemPrompt.test.ts b/apps/desktop/src/main/services/ai/tools/systemPrompt.test.ts index 8d6008179..a095fe47e 100644 --- a/apps/desktop/src/main/services/ai/tools/systemPrompt.test.ts +++ b/apps/desktop/src/main/services/ai/tools/systemPrompt.test.ts @@ -302,9 +302,9 @@ describe("buildCodingAgentSystemPrompt", () => { expect(result).toContain("## Operating Loop"); expect(result).toContain("## ADE CLI"); expect(result).toContain("only normal reason to skip ADE CLI"); - expect(result).toContain("can be driven from ADE CLI sessions"); - expect(result).toContain("ade help ios-sim"); - expect(result).toContain("preview-render --source "); + expect(result).toContain("ADE ships Agent Skills for deeper operating details"); + expect(result).toContain("ade-ios-simulator"); + expect(result).toContain("ade-cli-control-plane"); expect(result).toContain("## Editing Rules"); expect(result).toContain("## Verification Rules"); expect(result).toContain("## User-Facing Progress"); diff --git a/apps/desktop/src/main/services/chat/claudeSlashCommandDiscovery.test.ts b/apps/desktop/src/main/services/chat/claudeSlashCommandDiscovery.test.ts index 161e4c5de..d8dd4975d 100644 --- a/apps/desktop/src/main/services/chat/claudeSlashCommandDiscovery.test.ts +++ b/apps/desktop/src/main/services/chat/claudeSlashCommandDiscovery.test.ts @@ -147,12 +147,55 @@ describe("discoverClaudeSlashCommands", () => { "", ].join("\n")); - expect(discoverClaudeSlashCommands(tmpRoot)).toMatchObject([ - { + const commands = discoverClaudeSlashCommands(tmpRoot); + expect(commands).toEqual(expect.arrayContaining([ + expect.objectContaining({ name: "/fix-issue", description: "Fix a GitHub issue", - }, - ]); + }), + ])); + expect(commands).not.toEqual(expect.arrayContaining([ + expect.objectContaining({ name: "/background-context" }), + ])); + }); + + it("discovers cross-client .agents skills and ADE project skills", () => { + const agentsSkill = path.join(tmpRoot, ".agents", "skills", "ios-lab"); + const adeSkill = path.join(tmpRoot, ".ade", "skills", "pr-resolver"); + fs.mkdirSync(agentsSkill, { recursive: true }); + fs.mkdirSync(adeSkill, { recursive: true }); + fs.writeFileSync(path.join(agentsSkill, "SKILL.md"), [ + "---", + "name: ios-lab", + "description: Use this skill for simulator work", + "---", + "", + "Inspect the simulator.", + "", + ].join("\n")); + fs.writeFileSync(path.join(adeSkill, "SKILL.md"), [ + "---", + "name: pr-resolver", + "description: Use this skill for PR resolver work", + "---", + "", + "Resolve the PR.", + "", + ].join("\n")); + + const commands = discoverClaudeSlashCommands(tmpRoot); + expect(commands).toEqual(expect.arrayContaining([ + expect.objectContaining({ + name: "/ios-lab", + description: "Use this skill for simulator work", + source: "skill", + }), + expect.objectContaining({ + name: "/pr-resolver", + description: "Use this skill for PR resolver work", + source: "skill", + }), + ])); }); it("walks up parent directories to discover .claude/commands at workspace root from a lane subdir", () => { diff --git a/apps/desktop/src/main/services/chat/claudeSlashCommandDiscovery.ts b/apps/desktop/src/main/services/chat/claudeSlashCommandDiscovery.ts index eb7f1cece..764998058 100644 --- a/apps/desktop/src/main/services/chat/claudeSlashCommandDiscovery.ts +++ b/apps/desktop/src/main/services/chat/claudeSlashCommandDiscovery.ts @@ -251,15 +251,67 @@ function claudeRootsByPrecedence(cwd: string): string[] { return roots; } +function ancestorSkillRoots(cwd: string, dirName: ".agents" | ".ade"): string[] { + const roots: string[] = []; + const seen = new Set(); + const home = path.resolve(os.homedir()); + let current = path.resolve(cwd); + let depth = 0; + while (depth < 25) { + const candidate = path.join(current, dirName, "skills"); + if (!seen.has(candidate)) { + seen.add(candidate); + roots.push(candidate); + } + const parent = path.dirname(current); + if (parent === current) break; + if (current === home) break; + current = parent; + depth += 1; + } + return roots; +} + +function bundledSkillRoots(): string[] { + const candidates: string[] = []; + if (process.resourcesPath) { + candidates.push(path.join(process.resourcesPath, "agent-skills")); + } + return candidates; +} + +function skillRootsByPrecedence(cwd: string): string[] { + const roots: string[] = []; + const seen = new Set(); + const addRoot = (root: string): void => { + const resolved = path.resolve(root); + if (seen.has(resolved)) return; + seen.add(resolved); + roots.push(resolved); + }; + + for (const root of claudeRootsByPrecedence(cwd)) addRoot(path.join(root, "skills")); + for (const root of ancestorSkillRoots(cwd, ".agents")) addRoot(root); + for (const root of ancestorSkillRoots(cwd, ".ade")) addRoot(root); + addRoot(path.join(os.homedir(), ".agents", "skills")); + addRoot(path.join(os.homedir(), ".ade", "skills")); + for (const root of bundledSkillRoots()) addRoot(root); + return roots; +} + export function discoverClaudeSlashCommands(cwd: string): DiscoveredClaudeSlashCommand[] { - const roots = claudeRootsByPrecedence(cwd); + const claudeRoots = claudeRootsByPrecedence(cwd); const byName = new Map(); - for (const root of roots) { - const discovered = [ - ...discoverLegacyCommands(path.join(root, "commands")), - ...discoverSkills(path.join(root, "skills")), - ]; + for (const root of claudeRoots) { + const discovered = discoverLegacyCommands(path.join(root, "commands")); + for (const command of discovered) { + const key = slashCommandKey(command.name); + if (!byName.has(key)) byName.set(key, command); + } + } + for (const root of skillRootsByPrecedence(cwd)) { + const discovered = discoverSkills(root); for (const command of discovered) { const key = slashCommandKey(command.name); if (!byName.has(key)) byName.set(key, command); @@ -317,17 +369,17 @@ export function resolveClaudeSlashCommandInvocation( const name = match[1]; if (!name) return null; const argumentsText = match[2]?.trim() ?? ""; - const roots = claudeRootsByPrecedence(cwd); + const claudeRoots = claudeRootsByPrecedence(cwd); // Prefer command files; fall back to user-invocable skills (SKILL.md). let resolvedFile: string | null = null; - for (const root of roots) { + for (const root of claudeRoots) { resolvedFile = resolveLegacyCommandFile(path.join(root, "commands"), name); if (resolvedFile) break; } if (!resolvedFile) { - for (const root of roots) { - resolvedFile = resolveSkillFile(path.join(root, "skills"), name); + for (const root of skillRootsByPrecedence(cwd)) { + resolvedFile = resolveSkillFile(root, name); if (resolvedFile) break; } } diff --git a/apps/desktop/src/main/services/memory/skillRegistryService.ts b/apps/desktop/src/main/services/memory/skillRegistryService.ts index 0c2b542e9..767066415 100644 --- a/apps/desktop/src/main/services/memory/skillRegistryService.ts +++ b/apps/desktop/src/main/services/memory/skillRegistryService.ts @@ -24,9 +24,11 @@ type SkillIndexRow = { const WATCH_PATTERNS = [ ".ade/skills/**/*.md", + ".agents/skills/**/*.md", ".claude/skills/**/*.md", ".claude/commands/**/*.md", "CLAUDE.md", + "AGENTS.md", "agents.md", ]; @@ -139,6 +141,11 @@ function buildSkillMarkdown(input: { ]; const lines = [ + "---", + `name: ${slugify(input.title)}`, + `description: Use this skill when ${input.trigger.trim() || "the workflow applies"}.`, + "---", + "", `# ${input.title}`, "", "## When to use", @@ -295,7 +302,7 @@ export function createSkillRegistryService(args: { // Root docs (CLAUDE.md, agents.md) should be read directly from disk -- // do NOT import them as procedure memories. - if (kind !== "root_doc") { + if (kind !== "root_doc" && source !== "exported") { const existingMemory = existing?.memory_id ? memoryService.getMemory(existing.memory_id) : null; const importedProcedureContent = buildProcedureBody(title, content, absolutePath); @@ -349,7 +356,7 @@ export function createSkillRegistryService(args: { supersededByMemoryId: memoryId, }); } - } else { + } else if (kind === "root_doc") { // Root doc -- clear any previously-imported memory reference. memoryId = null; } @@ -384,9 +391,10 @@ export function createSkillRegistryService(args: { } }; crawl(path.join(projectRoot, ".ade", "skills")); + crawl(path.join(projectRoot, ".agents", "skills")); crawl(path.join(projectRoot, ".claude", "skills")); crawl(path.join(projectRoot, ".claude", "commands")); - for (const fileName of ["CLAUDE.md", "agents.md"]) { + for (const fileName of ["CLAUDE.md", "AGENTS.md", "agents.md"]) { const absolute = path.join(projectRoot, fileName); if (fs.existsSync(absolute)) discovered.add(path.resolve(absolute)); } @@ -415,7 +423,7 @@ export function createSkillRegistryService(args: { while (fs.existsSync(destinationPath)) { slug = `${slugBase}-${counter}`; counter += 1; - destinationDir = path.join(projectRoot, ".claude", "skills", slug); + destinationDir = path.join(projectRoot, ".ade", "skills", slug); destinationPath = path.join(destinationDir, "SKILL.md"); } fs.mkdirSync(destinationDir, { recursive: true }); diff --git a/apps/desktop/src/shared/adeCliGuidance.ts b/apps/desktop/src/shared/adeCliGuidance.ts index e87bdaaf4..8a32c6a8e 100644 --- a/apps/desktop/src/shared/adeCliGuidance.ts +++ b/apps/desktop/src/shared/adeCliGuidance.ts @@ -1,41 +1,22 @@ export const ADE_CLI_AGENT_GUIDANCE = [ "## ADE CLI", - "ADE is a local-first desktop development environment. It manages lanes (git worktrees), native ADE chats, terminal sessions, missions, PR workflows, memory, proof/artifacts, App Control, iOS Simulator/Preview Lab state, lane-tied macOS VMs, config, and managed processes.", - "`ade` is the default control plane for ADE-managed sessions. Use normal shell commands for the immediate repo inspection/edit/test in front of you; use ADE CLI when you need ADE state, drawer/session state, proof registration, missions, PR metadata, memory, or managed app/simulator control.", + "ADE is a local-first desktop development environment for lanes, chats, terminal sessions, missions, PR workflows, memory, proof/artifacts, App Control, iOS Simulator/Preview Lab state, lane-tied macOS VMs, config, and managed processes.", + "`ade` is the default control plane for ADE-managed sessions. Use normal shell commands for immediate repo inspection/edit/test work; use ADE CLI when you need ADE state, drawer/session state, proof registration, missions, PR metadata, memory, or managed app/simulator/browser/VM control.", "", - "### First orientation", - "- For ADE work beyond the immediate local edit, shell command, or repository inspection in front of you, check ADE CLI first.", + "### Discovery", "- Start with `ade doctor --text` when the environment is unclear.", - "- Use typed commands with `--text` for readable output: `ade lanes list --text`, `ade missions list --text`, `ade chats list --text`, `ade prs checks --text`, `ade proof status --text`, and `ade actions list --text`.", - "- Use `ade help ` and `ade help ` to discover exact flags instead of guessing. `ade actions list --text` / `ade actions run ...` is the escape hatch for service actions that do not yet have a friendly command.", + "- Use typed commands with `--text` for readable output: `ade lanes list --text`, `ade missions list --text`, `ade chat list --text`, `ade prs checks --text`, `ade proof status --text`, and `ade actions list --text`.", + "- Use `ade help ` and `ade help ` for exact flags. `ade actions list --text` / `ade actions run ...` is the escape hatch for service actions that do not yet have a friendly command.", "- If `command -v ade` fails, try `${ADE_CLI_PATH:-}` when set, then `${ADE_CLI_BIN_DIR:-}/ade`, and in an ADE source checkout fall back to `node apps/ade-cli/dist/cli.cjs ...` after confirming the file exists.", "- The only normal reason to skip ADE CLI for an ADE action is that the user truly does not have it installed or reachable after those fallbacks.", "", - "### Desktop socket surfaces", - "- Use `--socket` when the ADE desktop drawer and the CLI must share one live session. This matters for App Control, iOS Simulator, Preview Lab, terminal logs, selection/context capture, and proof drawer updates.", - "- iOS Simulator and Preview Lab can be driven from ADE CLI sessions when the desktop/runtime socket is available. In a standalone CLI session without `ADE_CHAT_SESSION_ID`, selections return inspect data and update the shared drawer service but do not automatically inject into a chat composer.", - "- For App Control on Electron apps, prefer socket mode: `ade help app-control`, `ade --socket app-control status --text`, `ade --socket app-control launch --command \"npm run dev\" --text`, or `ade --socket app-control connect --cdp-port --text`.", - "- For App Control terminal/log questions, check `ade --socket app-control status --text`, then prefer `ade --socket app-control logs --text --max-bytes 8388608`, `ade --socket app-control terminal write --data \"y\\n\"`, or `ade --socket app-control terminal signal --signal SIGINT`. Only fall back to `ade --socket terminal list --text` / `terminal read` when no App Control terminal is active.", - "- ADE sets `ADE_APP_CONTROL_CDP_PORT` and `ADE_APP_CONTROL_DEBUG_FLAGS` for App Control launches. Custom Electron launchers should forward one of those values to `--remote-debugging-port`.", - "- After App Control launch/connect, use `ade --socket app-control snapshot --text` or `elements --text`; use Control mode or `click`/`type` for input; use Inspect mode or `select --x --y ` to return screenshot-backed DOM/selector/source context and, when the session is chat-owned, attach it to the drawer chat.", - "- For ADE browser work, prefer socket mode so CLI calls and the Work sidebar share the same global browser tabs: `ade help browser`, `ade --socket browser panel --text`, `ade --socket browser status --text`, `ade --socket browser open --new-tab --text`, `ade --socket browser switch --tab --text`, `ade --socket browser screenshot --text`, and `ade --socket browser inspect-start --text` / `ade --socket browser select-current --text` / `ade --socket browser clear-selection --text`. The ADE browser is global rather than lane-scoped; links from chat output and localhost URLs in chat terminals should open there by default.", - "- For lane-tied macOS VM work, use `ade help macos-vm`, `ade --socket macos-vm status --lane --text`, `ade --socket macos-vm start --lane --create --text`, and `ade --socket macos-vm guide --lane --text`. To interact through ADE, use `ade --socket macos-vm screenshot --lane --text`, `ade --socket macos-vm select --lane --x --y --text`, `ade --socket macos-vm click --lane `, and `ade --socket macos-vm type --lane --value `; click/select coordinates are window-relative by default.", - "", - "### iOS Simulator and Preview Lab", - "- For iOS work inside an ADE chat, start with `ade help ios-sim`, `ade --socket ios-sim status --text`, `ade --socket ios-sim devices --text`, and `ade --socket ios-sim apps --device --text`, then launch with `ade --socket ios-sim launch --target --text`.", - "- For iOS launch target problems, do not create symlink projects, fake schemes, or repo-layout shims as the first fix. Re-list `ade --socket ios-sim apps --text`; ADE should discover root-level `.xcodeproj` bundles and `apps/*/*.xcodeproj` projects. If no app appears or the build fails, report the selected project, scheme, and exact build output instead of guessing.", - "- After launch, use `ade --socket ios-sim snapshot --text` or `ade --socket ios-sim elements --text` for screenshot + accessibility/ADEInspector grounding. Use `ade --socket ios-sim select --x --y ` to return simulator context and, when the session is chat-owned, attach it to the drawer chat. Use `ade --socket ios-sim tap`, `ade --socket ios-sim drag` / `ade --socket ios-sim swipe`, or `ade --socket ios-sim type` against the active app.", - "- Stream commands are `ade --socket ios-sim window-start`, `ade --socket ios-sim live-start`, `ade --socket ios-sim preview-start`, `ade --socket ios-sim stream-status`, and `ade --socket ios-sim stream-stop`. Use `ade --socket ios-sim stream-status --text` to explain the active backend/input path, fallback reason, helper pid, latency, and any blocker. Low idle fps is normal on `iosurface-indigo` because frames are event-driven while the simulator is still.", - "- For ADE Preview Lab, check `ade --socket ios-sim preview-status --text`, discover existing previews with `ade --socket ios-sim previews --source --text`, and finish with `ade --socket ios-sim preview-render --source --index --text`. Add a preview only when no matching preview already exists.", - "- If the simulator is not running or no active session/snapshot is available, warn the user with the exact blocker instead of guessing the screen.", - "- When you finish iOS Simulator work, run `ade --socket ios-sim shutdown` when you own the session and the task no longer needs the running simulator.", + "### Skills", + "- ADE ships Agent Skills for deeper operating details. Use the relevant skill instead of relying on trial and error: `ade-cli-control-plane`, `ade-ios-simulator`, `ade-app-control`, `ade-browser`, `ade-pr-workflows`, `ade-lanes-git`, `ade-cto-missions`, `ade-proof-artifacts`, and `ade-macos-vm`.", + "- If skills are not auto-listed by your runtime, look for them in project/user `.agents/skills`, `.ade/skills`, `.claude/skills`, or ADE's bundled `agent-skills` resources, then read that skill's `SKILL.md` on demand.", "", - "### SwiftUI previewability", - "- When changing SwiftUI from simulator or preview context, keep the affected UI previewable: add or repair nearby `#Preview` definitions and deterministic mock fixtures when needed.", - "- Preview fixtures should use representative visible UI context when useful, but must not depend on live sync, keychain, network, push, sockets, or a production database.", - "- For appearance-sensitive work, add named light/dark preview variants with `.preferredColorScheme(.light)` / `.preferredColorScheme(.dark)` and prefer adaptive system colors.", - "- When asked to make a preview reachable in the live simulator, add a DEBUG-only route, deep link, launch-argument handler, or small preview host. ADE may pass optional `ADE_PREVIEW_*` environment values.", - "- When an iOS visual inspect packet is attached to chat, ensure the affected source file or a nearby related Swift file contains a renderable `#Preview`/`PreviewProvider`, and add mock data or a preview harness if the screen would otherwise be blank.", + "### Socket-backed live surfaces", + "- Use `--socket` when the ADE desktop drawer and the CLI must share one live session. This matters for App Control, iOS Simulator, Preview Lab, terminal logs, selection/context capture, and proof drawer updates.", + "- Common starts: `ade --socket ios-sim status --text`, `ade --socket app-control status --text`, `ade --socket browser status --text`, and `ade --socket macos-vm status --lane --text`.", "", "### Proof and cleanup", "- When the user asks you to capture, send, attach, or provide proof, use the appropriate computer-use/browser/app-control tool to produce evidence, then register it with ADE via `ade proof ...` so it appears in the ADE proof drawer for the active chat, mission, or lane.", From b33388d4cd9ceaa755c3f7c80ee05516e3aba97e Mon Sep 17 00:00:00 2001 From: Arul Sharma <31745423+arul28@users.noreply.github.com> Date: Thu, 14 May 2026 03:21:08 -0400 Subject: [PATCH 05/24] fix(remote): retry sync reads after reconnect --- .../remoteConnectionPool.test.ts | 44 +++++++++++++++++++ .../remoteRuntime/remoteConnectionPool.ts | 13 +++--- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/main/services/remoteRuntime/remoteConnectionPool.test.ts b/apps/desktop/src/main/services/remoteRuntime/remoteConnectionPool.test.ts index 672a59338..80ae3dab9 100644 --- a/apps/desktop/src/main/services/remoteRuntime/remoteConnectionPool.test.ts +++ b/apps/desktop/src/main/services/remoteRuntime/remoteConnectionPool.test.ts @@ -449,6 +449,50 @@ describe("RemoteConnectionPool", () => { }); }); + it("retries project-scoped sync reads once when the connection closes during the request", async () => { + const firstClient = createClient(); + const firstSsh = createSsh(); + firstClient.call.mockRejectedValueOnce( + new Error("Remote ADE service connection closed."), + ); + bootstrapRemoteRuntimeMock.mockResolvedValueOnce({ + client: firstClient, + ssh: firstSsh, + result: connectResult("1.0.0"), + }); + const secondClient = createClient(); + secondClient.call.mockResolvedValueOnce({ + pairingPin: "654321", + connectedPeers: [{ id: "phone-1" }], + }); + bootstrapRemoteRuntimeMock.mockResolvedValueOnce({ + client: secondClient, + ssh: createSsh(), + result: connectResult("1.0.1"), + }); + const pool = new RemoteConnectionPool({} as RemoteTargetRegistry, "1.0.0"); + + await expect( + pool.callSyncForTarget(target, "project-1", "sync.getStatus", { + includeTransferReadiness: true, + }), + ).resolves.toEqual({ + pairingPin: "654321", + connectedPeers: [{ id: "phone-1" }], + }); + + expect(firstSsh.end).toHaveBeenCalledTimes(1); + expect(bootstrapRemoteRuntimeMock).toHaveBeenCalledTimes(2); + expect(firstClient.call).toHaveBeenCalledWith("sync.getStatus", { + projectId: "project-1", + includeTransferReadiness: true, + }); + expect(secondClient.call).toHaveBeenCalledWith("sync.getStatus", { + projectId: "project-1", + includeTransferReadiness: true, + }); + }); + it("subscribes to runtime event notifications and unsubscribes on cleanup", async () => { const client = createClient(); client.call.mockImplementation(async (method: string) => { diff --git a/apps/desktop/src/main/services/remoteRuntime/remoteConnectionPool.ts b/apps/desktop/src/main/services/remoteRuntime/remoteConnectionPool.ts index 76718bcac..c93faa4ec 100644 --- a/apps/desktop/src/main/services/remoteRuntime/remoteConnectionPool.ts +++ b/apps/desktop/src/main/services/remoteRuntime/remoteConnectionPool.ts @@ -142,11 +142,14 @@ export class RemoteConnectionPool { method: string, params: Record = {}, ): Promise { - const entry = await this.connectEntry(target); - return await entry.client.call(method, { - ...params, - projectId, - }); + return await this.withEntryForTarget( + target, + (entry) => entry.client.call(method, { + ...params, + projectId, + }), + { retryOnConnectionError: true }, + ); } private async addProjectWithEntry( From 557e6d67ce5860b595d35f371c65af6f5d97f265 Mon Sep 17 00:00:00 2001 From: Arul Sharma <31745423+arul28@users.noreply.github.com> Date: Thu, 14 May 2026 03:22:03 -0400 Subject: [PATCH 06/24] perf(prs): avoid snapshot refresh from header reload --- apps/desktop/src/renderer/components/prs/PRsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/renderer/components/prs/PRsPage.tsx b/apps/desktop/src/renderer/components/prs/PRsPage.tsx index 62af7acbc..73d82547a 100644 --- a/apps/desktop/src/renderer/components/prs/PRsPage.tsx +++ b/apps/desktop/src/renderer/components/prs/PRsPage.tsx @@ -102,7 +102,7 @@ function PRsPageInner() { const handleRefresh = React.useCallback(async () => { await Promise.all([ refresh(), - refreshLanes().catch(() => {}), + refreshLanes({ includeStatus: false, includeSnapshots: false }).catch(() => {}), ]); setIntegrationRefreshNonce((prev) => prev + 1); }, [refresh, refreshLanes]); From 228fadbc629411d75daa4dc249e1dfec0f6ee94e Mon Sep 17 00:00:00 2001 From: Arul Sharma <31745423+arul28@users.noreply.github.com> Date: Thu, 14 May 2026 03:23:04 -0400 Subject: [PATCH 07/24] chore(release): gate packaging on ci-pass --- .github/workflows/prepare-release.yml | 2 + .github/workflows/release-core.yml | 32 ++++ .github/workflows/release.yml | 2 + apps/desktop/resources/ade-cli-help.txt | 186 +++++++++++++++++------- 4 files changed, 172 insertions(+), 50 deletions(-) diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 10284f389..afbbbb48f 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -9,6 +9,8 @@ on: type: string permissions: + actions: read + checks: read contents: read jobs: diff --git a/.github/workflows/release-core.yml b/.github/workflows/release-core.yml index eb2b89c46..b503117cd 100644 --- a/.github/workflows/release-core.yml +++ b/.github/workflows/release-core.yml @@ -18,6 +18,8 @@ on: type: boolean permissions: + actions: read + checks: read contents: write jobs: @@ -34,6 +36,36 @@ jobs: git fetch origin main:refs/remotes/origin/main git merge-base --is-ancestor HEAD refs/remotes/origin/main + - name: Ensure CI passed for release commit + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + TARGET_REF: ${{ inputs.target_ref }} + run: | + set -euo pipefail + + ci_pass_json="$( + gh api "repos/$GH_REPO/commits/$TARGET_REF/check-runs" \ + -f per_page=100 \ + --paginate \ + --jq '[.check_runs[] | select(.name == "ci-pass")] | sort_by(.completed_at // "") | last // empty' + )" + + if [ -z "$ci_pass_json" ] || [ "$ci_pass_json" = "null" ]; then + echo "::error::No ci-pass check run was found for $TARGET_REF. Run CI before releasing." + exit 1 + fi + + status="$(printf '%s' "$ci_pass_json" | jq -r '.status // ""')" + conclusion="$(printf '%s' "$ci_pass_json" | jq -r '.conclusion // ""')" + url="$(printf '%s' "$ci_pass_json" | jq -r '.html_url // ""')" + if [ "$status" != "completed" ] || [ "$conclusion" != "success" ]; then + echo "::error::ci-pass for $TARGET_REF is $status/$conclusion. Release packaging requires green CI. $url" + exit 1 + fi + + echo "ci-pass succeeded for $TARGET_REF: $url" + build-mac-release: needs: - verify diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2e0c98683..c34da7510 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,6 +16,8 @@ on: type: string permissions: + actions: read + checks: read contents: write jobs: diff --git a/apps/desktop/resources/ade-cli-help.txt b/apps/desktop/resources/ade-cli-help.txt index 310a97705..87be9fcf6 100644 --- a/apps/desktop/resources/ade-cli-help.txt +++ b/apps/desktop/resources/ade-cli-help.txt @@ -10,17 +10,27 @@ _ ____ _____ Agent-focused command-line interface for ADE. - ADE CLI commands operate on the same project database and live desktop socket - used by the ADE app. By default the CLI connects to the app socket when it is - running; otherwise it falls back to a headless runtime for local-safe actions. + ADE CLI commands operate through the machine ADE runtime daemon by default. + If the daemon is not running, the CLI starts it, registers the selected + project, and routes project actions through that runtime. $ ade help Display help for a command $ ade auth status Check local ADE CLI readiness + $ ade code Open ADE Work chat in the terminal + $ ade desktop Launch the installed desktop app + $ ade runtime start | stop | status Manage the machine runtime daemon + $ ade serve Run the ADE runtime daemon in foreground + $ ade rpc --stdio Speak ADE JSON-RPC over stdin/stdout + $ ade init [path] Register a project with this machine runtime + $ ade projects list List projects registered on this machine + $ ade sync status | pin generate Manage machine sync and phone pairing $ ade doctor Inspect project, socket, runtime, and tool availability $ ade lanes list | show | create | child Work with lanes and lane stacks $ ade git status | commit | push | stash Run ADE-aware git operations - $ ade diff changes | file Inspect lane diffs + $ ade operations status | wait Poll operation/test/chat/run/mission status + $ ade diff changes | file | patch Inspect lane diffs (including raw git patch text) $ ade files tree | read | write | search Read and edit lane workspaces + $ ade missions launch | watch | graph Create, start, and inspect mission runs $ ade prs list | create | path-to-merge Manage PRs, queues, and Path to Merge repair rounds $ ade run defs | ps | start | logs Manage Run tab process definitions and runtime $ ade shell start | write | resize | close Launch and control tracked shell sessions @@ -33,21 +43,23 @@ _ ____ _____ $ ade coordinator Call coordinator runtime tools $ ade tests list | run | stop | runs | logs Run configured test suites $ ade proof status | list | screenshot | record Manage proof and computer-use artifacts - $ ade ios-sim devices | apps | launch | tap Control iOS Simulator apps, capture, and input - $ ade app-control launch | snapshot | click Inspect and drive Electron apps + $ ade ios-sim devices | apps | launch | tap Control iOS Simulator apps, capture, and input + $ ade app-control launch | snapshot | click Inspect and drive Electron apps + $ ade macos-vm status | start | guide Run lane-tied macOS VMs for agent work $ ade browser open | tabs | screenshot Use ADE's built-in browser pane $ ade memory add | search | pin Use ADE memory + $ ade usage snapshot | refresh | budget Read provider quota usage and edit automation guardrails $ ade settings action Call project config actions $ ade update status | check | install | dismiss Read auto-update state and drive install - $ ade actions list | run | status Escape hatch for every ADE service action + $ ade actions list | run | status | wait Escape hatch for every ADE service action $ ade cursor cloud agents | runs | artifacts | repos | models | me Drive Cursor Cloud agents via @cursor/sdk Global options: --project-root ADE project root. Inside .ade/worktrees/, this resolves to the parent project. --workspace-root Lane/worktree to treat as the active workspace. - --headless Skip the desktop socket and run an in-process ADE runtime. - --socket Require the desktop socket; fail instead of falling back to headless. + --headless Skip the runtime daemon and run an in-process ADE runtime. + --socket Require a live ADE socket; fail instead of falling back to headless. --json Print machine-readable JSON. This is the default output mode. --text Print a compact human-readable summary when a formatter exists. --timeout-ms Per-request timeout. Long agent/PR workflows may need several minutes. @@ -57,14 +69,19 @@ _ ____ _____ $ ade lanes list --text $ ade lanes create --name fix-login --description "Repair login redirect" $ ade git status --lane --text + $ ade git status --full --lane --text + $ ade git sync --lane --rebase --base main $ ade git stage --lane src/index.ts $ ade git commit --lane -m "Fix login redirect" + $ ade missions launch --prompt "Fix onboarding" --manual --text $ ade prs create --lane --base main --draft $ ade prs path-to-merge --model --max-rounds 3 --no-auto-merge $ ade proof record --seconds 20 $ ade ios-sim apps --text $ ade ios-sim launch --target --text $ ade app-control launch --command "pnpm dev" --text + $ ade macos-vm start --lane --create --text + $ ade macos-vm guide --lane --text $ ade --socket browser open http://localhost:5173 --new-tab --text $ ade terminal read --chat-session --text @@ -94,17 +111,27 @@ _ ____ _____ Agent-focused command-line interface for ADE. - ADE CLI commands operate on the same project database and live desktop socket - used by the ADE app. By default the CLI connects to the app socket when it is - running; otherwise it falls back to a headless runtime for local-safe actions. + ADE CLI commands operate through the machine ADE runtime daemon by default. + If the daemon is not running, the CLI starts it, registers the selected + project, and routes project actions through that runtime. $ ade help Display help for a command $ ade auth status Check local ADE CLI readiness + $ ade code Open ADE Work chat in the terminal + $ ade desktop Launch the installed desktop app + $ ade runtime start | stop | status Manage the machine runtime daemon + $ ade serve Run the ADE runtime daemon in foreground + $ ade rpc --stdio Speak ADE JSON-RPC over stdin/stdout + $ ade init [path] Register a project with this machine runtime + $ ade projects list List projects registered on this machine + $ ade sync status | pin generate Manage machine sync and phone pairing $ ade doctor Inspect project, socket, runtime, and tool availability $ ade lanes list | show | create | child Work with lanes and lane stacks $ ade git status | commit | push | stash Run ADE-aware git operations - $ ade diff changes | file Inspect lane diffs + $ ade operations status | wait Poll operation/test/chat/run/mission status + $ ade diff changes | file | patch Inspect lane diffs (including raw git patch text) $ ade files tree | read | write | search Read and edit lane workspaces + $ ade missions launch | watch | graph Create, start, and inspect mission runs $ ade prs list | create | path-to-merge Manage PRs, queues, and Path to Merge repair rounds $ ade run defs | ps | start | logs Manage Run tab process definitions and runtime $ ade shell start | write | resize | close Launch and control tracked shell sessions @@ -117,21 +144,23 @@ _ ____ _____ $ ade coordinator Call coordinator runtime tools $ ade tests list | run | stop | runs | logs Run configured test suites $ ade proof status | list | screenshot | record Manage proof and computer-use artifacts - $ ade ios-sim devices | apps | launch | tap Control iOS Simulator apps, capture, and input - $ ade app-control launch | snapshot | click Inspect and drive Electron apps + $ ade ios-sim devices | apps | launch | tap Control iOS Simulator apps, capture, and input + $ ade app-control launch | snapshot | click Inspect and drive Electron apps + $ ade macos-vm status | start | guide Run lane-tied macOS VMs for agent work $ ade browser open | tabs | screenshot Use ADE's built-in browser pane $ ade memory add | search | pin Use ADE memory + $ ade usage snapshot | refresh | budget Read provider quota usage and edit automation guardrails $ ade settings action Call project config actions $ ade update status | check | install | dismiss Read auto-update state and drive install - $ ade actions list | run | status Escape hatch for every ADE service action + $ ade actions list | run | status | wait Escape hatch for every ADE service action $ ade cursor cloud agents | runs | artifacts | repos | models | me Drive Cursor Cloud agents via @cursor/sdk Global options: --project-root ADE project root. Inside .ade/worktrees/, this resolves to the parent project. --workspace-root Lane/worktree to treat as the active workspace. - --headless Skip the desktop socket and run an in-process ADE runtime. - --socket Require the desktop socket; fail instead of falling back to headless. + --headless Skip the runtime daemon and run an in-process ADE runtime. + --socket Require a live ADE socket; fail instead of falling back to headless. --json Print machine-readable JSON. This is the default output mode. --text Print a compact human-readable summary when a formatter exists. --timeout-ms Per-request timeout. Long agent/PR workflows may need several minutes. @@ -141,14 +170,19 @@ _ ____ _____ $ ade lanes list --text $ ade lanes create --name fix-login --description "Repair login redirect" $ ade git status --lane --text + $ ade git status --full --lane --text + $ ade git sync --lane --rebase --base main $ ade git stage --lane src/index.ts $ ade git commit --lane -m "Fix login redirect" + $ ade missions launch --prompt "Fix onboarding" --manual --text $ ade prs create --lane --base main --draft $ ade prs path-to-merge --model --max-rounds 3 --no-auto-merge $ ade proof record --seconds 20 $ ade ios-sim apps --text $ ade ios-sim launch --target --text $ ade app-control launch --command "pnpm dev" --text + $ ade macos-vm start --lane --create --text + $ ade macos-vm guide --lane --text $ ade --socket browser open http://localhost:5173 --new-tab --text $ ade terminal read --chat-session --text @@ -178,17 +212,27 @@ _ ____ _____ Agent-focused command-line interface for ADE. - ADE CLI commands operate on the same project database and live desktop socket - used by the ADE app. By default the CLI connects to the app socket when it is - running; otherwise it falls back to a headless runtime for local-safe actions. + ADE CLI commands operate through the machine ADE runtime daemon by default. + If the daemon is not running, the CLI starts it, registers the selected + project, and routes project actions through that runtime. $ ade help Display help for a command $ ade auth status Check local ADE CLI readiness + $ ade code Open ADE Work chat in the terminal + $ ade desktop Launch the installed desktop app + $ ade runtime start | stop | status Manage the machine runtime daemon + $ ade serve Run the ADE runtime daemon in foreground + $ ade rpc --stdio Speak ADE JSON-RPC over stdin/stdout + $ ade init [path] Register a project with this machine runtime + $ ade projects list List projects registered on this machine + $ ade sync status | pin generate Manage machine sync and phone pairing $ ade doctor Inspect project, socket, runtime, and tool availability $ ade lanes list | show | create | child Work with lanes and lane stacks $ ade git status | commit | push | stash Run ADE-aware git operations - $ ade diff changes | file Inspect lane diffs + $ ade operations status | wait Poll operation/test/chat/run/mission status + $ ade diff changes | file | patch Inspect lane diffs (including raw git patch text) $ ade files tree | read | write | search Read and edit lane workspaces + $ ade missions launch | watch | graph Create, start, and inspect mission runs $ ade prs list | create | path-to-merge Manage PRs, queues, and Path to Merge repair rounds $ ade run defs | ps | start | logs Manage Run tab process definitions and runtime $ ade shell start | write | resize | close Launch and control tracked shell sessions @@ -201,21 +245,23 @@ _ ____ _____ $ ade coordinator Call coordinator runtime tools $ ade tests list | run | stop | runs | logs Run configured test suites $ ade proof status | list | screenshot | record Manage proof and computer-use artifacts - $ ade ios-sim devices | apps | launch | tap Control iOS Simulator apps, capture, and input - $ ade app-control launch | snapshot | click Inspect and drive Electron apps + $ ade ios-sim devices | apps | launch | tap Control iOS Simulator apps, capture, and input + $ ade app-control launch | snapshot | click Inspect and drive Electron apps + $ ade macos-vm status | start | guide Run lane-tied macOS VMs for agent work $ ade browser open | tabs | screenshot Use ADE's built-in browser pane $ ade memory add | search | pin Use ADE memory + $ ade usage snapshot | refresh | budget Read provider quota usage and edit automation guardrails $ ade settings action Call project config actions $ ade update status | check | install | dismiss Read auto-update state and drive install - $ ade actions list | run | status Escape hatch for every ADE service action + $ ade actions list | run | status | wait Escape hatch for every ADE service action $ ade cursor cloud agents | runs | artifacts | repos | models | me Drive Cursor Cloud agents via @cursor/sdk Global options: --project-root ADE project root. Inside .ade/worktrees/, this resolves to the parent project. --workspace-root Lane/worktree to treat as the active workspace. - --headless Skip the desktop socket and run an in-process ADE runtime. - --socket Require the desktop socket; fail instead of falling back to headless. + --headless Skip the runtime daemon and run an in-process ADE runtime. + --socket Require a live ADE socket; fail instead of falling back to headless. --json Print machine-readable JSON. This is the default output mode. --text Print a compact human-readable summary when a formatter exists. --timeout-ms Per-request timeout. Long agent/PR workflows may need several minutes. @@ -225,14 +271,19 @@ _ ____ _____ $ ade lanes list --text $ ade lanes create --name fix-login --description "Repair login redirect" $ ade git status --lane --text + $ ade git status --full --lane --text + $ ade git sync --lane --rebase --base main $ ade git stage --lane src/index.ts $ ade git commit --lane -m "Fix login redirect" + $ ade missions launch --prompt "Fix onboarding" --manual --text $ ade prs create --lane --base main --draft $ ade prs path-to-merge --model --max-rounds 3 --no-auto-merge $ ade proof record --seconds 20 $ ade ios-sim apps --text $ ade ios-sim launch --target --text $ ade app-control launch --command "pnpm dev" --text + $ ade macos-vm start --lane --create --text + $ ade macos-vm guide --lane --text $ ade --socket browser open http://localhost:5173 --new-tab --text $ ade terminal read --chat-session --text @@ -269,6 +320,7 @@ _ ____ _____ $ ade lanes show --text Inspect one lane status $ ade lanes create --name Create a lane from the current project context $ ade lanes create --linear-issue-json '{...}' Create a lane linked to a Linear issue + $ ade lanes create --branch-name Override the auto-generated branch name $ ade lanes child --lane --name Create a child lane under a parent $ ade lanes import --branch Register an existing branch/worktree $ ade lanes archive Archive a lane in ADE @@ -289,15 +341,24 @@ _ ____ _____ refresh lane state. Use --lane for anything other than the active workspace. $ ade git status --lane --text Show ADE-aware sync status + $ ade git status --full --lane --text Show full lane status, diff, and conflict state + $ ade git fetch --lane Fetch remote refs + $ ade git pull --lane Pull with ADE's ff-only lane operation + $ ade git sync --lane --rebase --base main + Sync the lane with its base branch $ ade git stage --lane src/file.ts Stage one file $ ade git stage-all --lane Stage all current changes $ ade git unstage --lane src/file.ts Unstage one file $ ade git commit --lane [-m ] Commit, adding Refs on linked Linear lanes $ ade git push --lane --set-upstream Push through ADE + $ ade git push --lane --force-with-lease Force-push through ADE with lease $ ade git branches --lane --text List branches with last-commit metadata $ ade git user-identity --lane --text Read lane checkout's git user.name/email $ ade git stash push|list|apply|pop Use ADE lane stash actions $ ade git rebase --lane --ai Rebase with ADE conflict support + $ ade git rebase continue --lane Continue an in-progress rebase + $ ade git conflict show --lane --text Inspect merge/rebase conflict state + $ ade git conflict resolve --kind rebase Continue after manual conflict resolution $ ade diff changes --lane --text Inspect changed files ## ade diff --help @@ -310,7 +371,8 @@ _ ____ _____ Diffs $ ade diff changes --lane --text Summarize staged/unstaged file changes - $ ade diff file --lane --text Show one file diff + $ ade diff file --lane --text Show one file diff (side-by-side text) + $ ade diff patch --lane --text Raw unified diff / patch for one file $ ade diff file --mode staged Inspect staged diff for one file $ ade diff actions --text List diff service actions @@ -372,8 +434,8 @@ _ ____ _____ Run tab - Run tab commands mirror ADE desktop process definitions and runtime state. - They require the desktop socket when live process state is needed. + Run tab commands mirror ADE process definitions and runtime state. They use + the machine runtime daemon when live process state is needed. $ ade run defs --text List configured run commands $ ade run ps --lane --text List process runtime state @@ -397,6 +459,8 @@ _ ____ _____ $ ade shell start --lane -- npm test Start a tracked shell session $ ade shell start --lane -c "npm test" Start with a command string + $ ade shell start-cli codex --lane --permission-mode edit + $ ade shell start --provider claude --lane --message "fix tests" $ ade shell start --lane --chat-session -c "npm test" $ ade shell write --data "q" Write data to a PTY $ ade shell resize --cols 120 --rows 36 @@ -412,7 +476,7 @@ _ ____ _____ Chat terminal Terminal commands control the active in-chat terminal for an ADE chat. Use - desktop socket mode when you want the same terminal the user sees in the app. + attached runtime mode when you want the same terminal the app is viewing. $ ade terminal list --chat-session --text List terminals for a chat $ ade terminal active --chat-session --text Show the active chat terminal @@ -431,13 +495,13 @@ _ ____ _____ Work chats Chat commands use ADE agent chat sessions. Live provider-backed chat normally - requires the desktop socket because the app owns provider/session state. + requires an attached runtime because the daemon owns provider/session state. $ ade chat list --text List chat sessions $ ade chat create --lane --provider codex --model [--fast] $ ade chat send --text "next step" Send a message $ ade chat interrupt Stop an active turn - $ ade chat resume Resume a session + $ ade chat slash --text List slash commands for a session $ ade agent spawn --lane --prompt "fix" Start a new agent work session ## ade agent --help @@ -479,7 +543,10 @@ _ ____ _____ Linear workflows - $ ade linear quick-view --text Show connected workspace, projects, and issues + $ ade --role cto linear quick-view --text Show connected workspace, projects, and issues + $ ade --role cto linear picker-data --text Read projects/users/states for the issue picker + $ ade --role cto linear search-issues --query "auth" --state-type started,unstarted --first 50 + Search issues for the lane Linear-issue picker $ ade linear workflows --text List configured workflows $ ade linear sync dashboard --text Show sync dashboard $ ade linear sync run Trigger a sync run @@ -502,13 +569,15 @@ _ ____ _____ $ ade automations update --from-file $ ade automations delete Remove a local rule $ ade automations toggle --enabled true|false - $ ade automations run [--dry-run] Trigger a rule manually + $ ade automations run [--lane ] [--dry-run] + $ ade automations trigger [--lane ] + Trigger a rule manually $ ade automations runs [--rule ] [--status ] [--limit 50] $ ade automations run-show [--json] Inspect a run $ ade automations example Print an example rule (stdout) Lane mode flags (apply to create/update on top of --from-file/--stdin/--text): - --lane-mode Spawn a new lane per run, or reuse one + --lane-mode Create, reuse, or require lane at trigger time --lane Target lane (only with --lane-mode reuse) --lane-name-preset --lane-name-template Template (only with preset custom) @@ -577,8 +646,8 @@ _ ____ _____ Prefer screenshots/images, screen recordings, and browser captures/traces. Console logs are supporting diagnostics, not a replacement for visual proof. Local screenshot/video fallback is macOS-only and runs headless by default - unless --socket is explicitly requested. Desktop socket mode has the best - parity for UI-owned proof state. + unless --socket is explicitly requested. Runtime socket mode has the best + parity for shared proof state. $ ade proof status --text Show proof backend capabilities $ ade proof list --text List captured artifacts @@ -599,7 +668,7 @@ _ ____ _____ iOS simulator commands build, launch, mirror, inspect, and control the ADE drawer simulator. Aliases: `ade ios` and `ade simulator` route to the same - surface. For drawer/shared session state, prefer desktop socket mode + surface. For drawer/shared session state, prefer runtime socket mode (--socket) so launch/select/tap operate on the same long-lived ADE service. Launch is headless by default; use --foreground only when you need the native Simulator window in front. idb is optional for direct @@ -641,7 +710,7 @@ _ ____ _____ $ ade ios-sim stream-stop Stop preview/live streaming (stopStream) Input and selection: - $ ade --socket ios-sim select --x 120 --y 420 Add UI context to drawer chat (selectPoint) + $ ade --socket ios-sim select --x 120 --y 420 Return/select simulator UI context (chat-owned sessions auto-attach) $ ade ios-sim tap 120 420 Tap active simulator app (tap) $ ade ios-sim drag 120 700 120 250 Drag active simulator app (drag) $ ade ios-sim swipe 120 700 120 250 Swipe active simulator app (swipe) @@ -695,7 +764,7 @@ _ ____ _____ $ ade app-control screenshot --text Capture the active renderer screenshot $ ade app-control snapshot --text Screenshot + DOM element refs $ ade app-control inspect --x 120 --y 420 Hit-test a point without committing context - $ ade app-control select --x 120 --y 420 Add selected app context to the drawer chat + $ ade app-control select --x 120 --y 420 Return/select app context (chat-owned sessions auto-attach) Input: $ ade app-control click 120 420 Click screenshot coordinates @@ -773,17 +842,27 @@ _ ____ _____ Agent-focused command-line interface for ADE. - ADE CLI commands operate on the same project database and live desktop socket - used by the ADE app. By default the CLI connects to the app socket when it is - running; otherwise it falls back to a headless runtime for local-safe actions. + ADE CLI commands operate through the machine ADE runtime daemon by default. + If the daemon is not running, the CLI starts it, registers the selected + project, and routes project actions through that runtime. $ ade help Display help for a command $ ade auth status Check local ADE CLI readiness + $ ade code Open ADE Work chat in the terminal + $ ade desktop Launch the installed desktop app + $ ade runtime start | stop | status Manage the machine runtime daemon + $ ade serve Run the ADE runtime daemon in foreground + $ ade rpc --stdio Speak ADE JSON-RPC over stdin/stdout + $ ade init [path] Register a project with this machine runtime + $ ade projects list List projects registered on this machine + $ ade sync status | pin generate Manage machine sync and phone pairing $ ade doctor Inspect project, socket, runtime, and tool availability $ ade lanes list | show | create | child Work with lanes and lane stacks $ ade git status | commit | push | stash Run ADE-aware git operations - $ ade diff changes | file Inspect lane diffs + $ ade operations status | wait Poll operation/test/chat/run/mission status + $ ade diff changes | file | patch Inspect lane diffs (including raw git patch text) $ ade files tree | read | write | search Read and edit lane workspaces + $ ade missions launch | watch | graph Create, start, and inspect mission runs $ ade prs list | create | path-to-merge Manage PRs, queues, and Path to Merge repair rounds $ ade run defs | ps | start | logs Manage Run tab process definitions and runtime $ ade shell start | write | resize | close Launch and control tracked shell sessions @@ -796,21 +875,23 @@ _ ____ _____ $ ade coordinator Call coordinator runtime tools $ ade tests list | run | stop | runs | logs Run configured test suites $ ade proof status | list | screenshot | record Manage proof and computer-use artifacts - $ ade ios-sim devices | apps | launch | tap Control iOS Simulator apps, capture, and input - $ ade app-control launch | snapshot | click Inspect and drive Electron apps + $ ade ios-sim devices | apps | launch | tap Control iOS Simulator apps, capture, and input + $ ade app-control launch | snapshot | click Inspect and drive Electron apps + $ ade macos-vm status | start | guide Run lane-tied macOS VMs for agent work $ ade browser open | tabs | screenshot Use ADE's built-in browser pane $ ade memory add | search | pin Use ADE memory + $ ade usage snapshot | refresh | budget Read provider quota usage and edit automation guardrails $ ade settings action Call project config actions $ ade update status | check | install | dismiss Read auto-update state and drive install - $ ade actions list | run | status Escape hatch for every ADE service action + $ ade actions list | run | status | wait Escape hatch for every ADE service action $ ade cursor cloud agents | runs | artifacts | repos | models | me Drive Cursor Cloud agents via @cursor/sdk Global options: --project-root ADE project root. Inside .ade/worktrees/, this resolves to the parent project. --workspace-root Lane/worktree to treat as the active workspace. - --headless Skip the desktop socket and run an in-process ADE runtime. - --socket Require the desktop socket; fail instead of falling back to headless. + --headless Skip the runtime daemon and run an in-process ADE runtime. + --socket Require a live ADE socket; fail instead of falling back to headless. --json Print machine-readable JSON. This is the default output mode. --text Print a compact human-readable summary when a formatter exists. --timeout-ms Per-request timeout. Long agent/PR workflows may need several minutes. @@ -820,14 +901,19 @@ _ ____ _____ $ ade lanes list --text $ ade lanes create --name fix-login --description "Repair login redirect" $ ade git status --lane --text + $ ade git status --full --lane --text + $ ade git sync --lane --rebase --base main $ ade git stage --lane src/index.ts $ ade git commit --lane -m "Fix login redirect" + $ ade missions launch --prompt "Fix onboarding" --manual --text $ ade prs create --lane --base main --draft $ ade prs path-to-merge --model --max-rounds 3 --no-auto-merge $ ade proof record --seconds 20 $ ade ios-sim apps --text $ ade ios-sim launch --target --text $ ade app-control launch --command "pnpm dev" --text + $ ade macos-vm start --lane --create --text + $ ade macos-vm guide --lane --text $ ade --socket browser open http://localhost:5173 --new-tab --text $ ade terminal read --chat-session --text From 60f60578bfb81d5831d424093327a92fabf2f5e0 Mon Sep 17 00:00:00 2001 From: Arul Sharma <31745423+arul28@users.noreply.github.com> Date: Thu, 14 May 2026 03:33:25 -0400 Subject: [PATCH 08/24] perf(boot): defer header runtime status reads boot.open-project fitness 902.72 -> 53.71; localRuntime.callAction p95 2025ms -> 97ms; slow IPC channels 2 -> 0. --- .../renderer/components/app/TopBar.test.tsx | 104 ++++++++++++------ .../src/renderer/components/app/TopBar.tsx | 51 ++++++--- .../components/usage/HeaderUsageControl.tsx | 48 ++++---- 3 files changed, 136 insertions(+), 67 deletions(-) diff --git a/apps/desktop/src/renderer/components/app/TopBar.test.tsx b/apps/desktop/src/renderer/components/app/TopBar.test.tsx index aa97101c6..bba09ebae 100644 --- a/apps/desktop/src/renderer/components/app/TopBar.test.tsx +++ b/apps/desktop/src/renderer/components/app/TopBar.test.tsx @@ -143,6 +143,14 @@ function fireProjectTabDragEnd( fireEvent(element, event); } +async function advancePhoneSyncStartupDelay() { + await act(async () => { + vi.advanceTimersByTime(5_000); + await Promise.resolve(); + await Promise.resolve(); + }); +} + describe("TopBar", () => { const originalAde = globalThis.window.ade; @@ -360,24 +368,32 @@ describe("TopBar", () => { }); it("opens the phone sync drawer from the host status control", async () => { - render(); + vi.useFakeTimers(); + try { + render(); - expect(await screen.findByText("1 phone connected to ADE Desktop")).toBeTruthy(); + expect(screen.getByText("Phone sync")).toBeTruthy(); + expect(globalThis.window.ade.sync.getStatus).not.toHaveBeenCalled(); - fireEvent.click(screen.getByTitle("Connect a phone to this machine")); + await advancePhoneSyncStartupDelay(); + expect(screen.getByText("1 phone connected to ADE Desktop")).toBeTruthy(); - expect(screen.getByText("Connect to the ADE mobile app")).toBeTruthy(); - expect(screen.getByTestId("sync-devices-section")).toBeTruthy(); - expect(screen.getByTitle("Connect a phone to this machine").getAttribute("aria-expanded")).toBe("true"); + fireEvent.click(screen.getByTitle("Connect a phone to this machine")); - fireEvent.click(screen.getByTitle("Close phone sync")); + expect(screen.getByText("Connect to the ADE mobile app")).toBeTruthy(); + expect(screen.getByTestId("sync-devices-section")).toBeTruthy(); + expect(screen.getByTitle("Connect a phone to this machine").getAttribute("aria-expanded")).toBe("true"); + + fireEvent.click(screen.getByTitle("Close phone sync")); - await waitFor(() => { expect(screen.queryByTestId("sync-devices-section")).toBeNull(); - }); + } finally { + vi.useRealTimers(); + } }); it("refreshes the phone sync label from global sync events", async () => { + vi.useFakeTimers(); let syncEventHandler: ((event: any) => void) | null = null; const getStatus = vi.fn() .mockResolvedValueOnce(makeSyncSnapshot({ connectedPeers: [] })); @@ -389,25 +405,31 @@ describe("TopBar", () => { }; }) as any; - render(); + try { + render(); - expect(await screen.findByText("Phone sync ready")).toBeTruthy(); + await advancePhoneSyncStartupDelay(); + expect(screen.getByText("Phone sync ready")).toBeTruthy(); - await act(async () => { - syncEventHandler?.({ - type: "sync-status", - snapshot: makeSyncSnapshot({ - connectedPeers: [ - { deviceId: "phone-1", deviceName: "Arul iPhone", platform: "iOS", deviceType: "phone" }, - ], - }), + await act(async () => { + syncEventHandler?.({ + type: "sync-status", + snapshot: makeSyncSnapshot({ + connectedPeers: [ + { deviceId: "phone-1", deviceName: "Arul iPhone", platform: "iOS", deviceType: "phone" }, + ], + }), + }); }); - }); - expect(await screen.findByText("1 phone connected to ADE Desktop")).toBeTruthy(); + expect(screen.getByText("1 phone connected to ADE Desktop")).toBeTruthy(); + } finally { + vi.useRealTimers(); + } }); it("labels disabled local runtime sync as unavailable", async () => { + vi.useFakeTimers(); const snapshot = makeSyncSnapshot(); globalThis.window.ade.sync.getStatus = vi.fn(async () => makeSyncSnapshot({ connectedPeers: [], @@ -420,10 +442,15 @@ describe("TopBar", () => { }, })) as any; - render(); + try { + render(); - expect(await screen.findByText("Phone sync unavailable")).toBeTruthy(); - expect(screen.queryByText("Phone sync ready")).toBeNull(); + await advancePhoneSyncStartupDelay(); + expect(screen.getByText("Phone sync unavailable")).toBeTruthy(); + expect(screen.queryByText("Phone sync ready")).toBeNull(); + } finally { + vi.useRealTimers(); + } }); it("does not refresh phone sync status on an idle interval", async () => { @@ -438,7 +465,7 @@ describe("TopBar", () => { await Promise.resolve(); }); - expect(getStatus).toHaveBeenCalledTimes(1); + expect(getStatus).not.toHaveBeenCalled(); await act(async () => { vi.advanceTimersByTime(15_000); @@ -452,21 +479,34 @@ describe("TopBar", () => { }); it("refreshes phone sync status when the window regains focus", async () => { + vi.useFakeTimers(); const getStatus = vi.fn() .mockResolvedValueOnce(makeSyncSnapshot({ connectedPeers: [] })) .mockResolvedValueOnce(makeSyncSnapshot()); globalThis.window.ade.sync.getStatus = getStatus as any; - render(); + try { + render(); - expect(await screen.findByText("Phone sync ready")).toBeTruthy(); + await act(async () => { + window.dispatchEvent(new Event("focus")); + await Promise.resolve(); + await Promise.resolve(); + }); - await act(async () => { - window.dispatchEvent(new Event("focus")); - }); + expect(screen.getByText("Phone sync ready")).toBeTruthy(); + + await act(async () => { + window.dispatchEvent(new Event("focus")); + await Promise.resolve(); + await Promise.resolve(); + }); - expect(await screen.findByText("1 phone connected to ADE Desktop")).toBeTruthy(); - expect(getStatus).toHaveBeenCalledTimes(2); + expect(screen.getByText("1 phone connected to ADE Desktop")).toBeTruthy(); + expect(getStatus).toHaveBeenCalledTimes(2); + } finally { + vi.useRealTimers(); + } }); it("opens Linear quick view and creates a linked lane from an issue", async () => { diff --git a/apps/desktop/src/renderer/components/app/TopBar.tsx b/apps/desktop/src/renderer/components/app/TopBar.tsx index b5973da0f..7227922c7 100644 --- a/apps/desktop/src/renderer/components/app/TopBar.tsx +++ b/apps/desktop/src/renderer/components/app/TopBar.tsx @@ -66,6 +66,7 @@ const projectIconCache = new Map(); const PROJECT_ICON_ACCENT_CACHE_MAX = 48; const projectIconAccentCache = new Map(); const RECENT_PROJECTS_CACHE_TTL_MS = 2_500; +const PHONE_SYNC_STARTUP_DELAY_MS = 5_000; let recentProjectsCache: | { rows: RecentProjectSummary[]; fetchedAtMs: number } | null = null; @@ -690,6 +691,7 @@ export function TopBar() { const connectedRemoteCount = remoteSnapshot?.connectedCount ?? 0; const remoteButtonLabel = connectedRemoteCount > 0 ? `Remote ${connectedRemoteCount}` : "Remote"; + const showSyncControl = workspaceProjectOpen; const applyZoom = useCallback((pct: number) => { const clamped = Math.max(MIN_ZOOM_LEVEL, Math.min(MAX_ZOOM_LEVEL, pct)); @@ -852,6 +854,9 @@ export function TopBar() { useEffect(() => { let cancelled = false; let statusRequestVersion = 0; + let started = false; + let startupTimer: number | null = null; + let disposeSyncEvents: (() => void) | null = null; if (!project?.rootPath || remoteBinding) { setSyncSnapshot(null); setPhoneSyncOpen(false); @@ -873,24 +878,40 @@ export function TopBar() { }); }; setSyncSnapshot(null); - refreshSyncStatus(); - window.addEventListener("focus", refreshSyncStatus); - const dispose = window.ade.sync.onEvent((event) => { - if (!cancelled && event.type === "sync-status") { - statusRequestVersion += 1; - setSyncSnapshot(event.snapshot); + const startSyncStatus = () => { + if (cancelled || started) return; + started = true; + refreshSyncStatus(); + disposeSyncEvents = window.ade.sync.onEvent((event) => { + if (!cancelled && event.type === "sync-status") { + statusRequestVersion += 1; + setSyncSnapshot(event.snapshot); + } + }); + }; + const onFocus = () => { + if (started) { + refreshSyncStatus(); + } else { + startSyncStatus(); } - }); + }; + startupTimer = window.setTimeout( + startSyncStatus, + phoneSyncOpen ? 0 : PHONE_SYNC_STARTUP_DELAY_MS, + ); + window.addEventListener("focus", onFocus); return () => { cancelled = true; - window.removeEventListener("focus", refreshSyncStatus); - dispose(); + if (startupTimer != null) window.clearTimeout(startupTimer); + window.removeEventListener("focus", onFocus); + disposeSyncEvents?.(); }; // Background projects don't broadcast sync-status events (main.ts filters // them to the active project), so we re-run this effect on rootPath change - // to force an immediate refetch. Focus refresh covers state changes that - // happen while ADE is not active. - }, [project?.rootPath, remoteBinding]); + // and let the delayed startup check pick up the current state. Focus and + // explicit drawer opens still refresh immediately. + }, [phoneSyncOpen, project?.rootPath, remoteBinding]); const checkForActiveWorkloads = useCallback( async (projectRootPath: string): Promise => { @@ -1334,7 +1355,7 @@ export function TopBar() { [], ); - const syncLabel = deriveSyncLabel(syncSnapshot); + const syncLabel = deriveSyncLabel(syncSnapshot) ?? "Phone sync"; const transitionTargetName = projectTransition?.rootPath ? (projectTabs.find( (entry) => entry.rootPath === projectTransition.rootPath, @@ -1832,7 +1853,7 @@ export function TopBar() { {remoteButtonLabel} - {syncSnapshot && syncLabel ? ( + {showSyncControl ? ( +
{loading ? "loading" : "idle"}
+ + ); +} + function RouteHarness() { const { activeTab, selectedPrId, selectedQueueGroupId, selectedRebaseItemId } = usePrs(); return ( @@ -212,6 +224,27 @@ describe("PrsContext refresh", () => { }); }); + it("passes targeted PR refresh requests through to the PR service", async () => { + const user = userEvent.setup(); + + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByTestId("loading").textContent).toBe("idle"); + }); + expect(window.ade.prs.refresh).not.toHaveBeenCalled(); + + await user.click(screen.getByRole("button", { name: "refresh pr-1" })); + + await waitFor(() => { + expect(window.ade.prs.refresh).toHaveBeenCalledWith({ prId: "pr-1" }); + }); + }); + it("does not run a GitHub PR refresh just because the local PR tab changes", async () => { const user = userEvent.setup(); diff --git a/apps/desktop/src/renderer/components/prs/state/PrsContext.tsx b/apps/desktop/src/renderer/components/prs/state/PrsContext.tsx index 5b4753153..784840e1f 100644 --- a/apps/desktop/src/renderer/components/prs/state/PrsContext.tsx +++ b/apps/desktop/src/renderer/components/prs/state/PrsContext.tsx @@ -114,7 +114,7 @@ type PrsContextValue = PrsState & { loadConvergenceState: (prId: string, options?: { force?: boolean }) => Promise; saveConvergenceState: (prId: string, state: PrConvergenceStatePatch) => Promise; resetConvergenceState: (prId: string) => Promise; - refresh: () => Promise; + refresh: (args?: { prId?: string; prIds?: string[] }) => Promise; // Timeline + rails controls setPrsTimelineRailsEnabled: (enabled: boolean) => void; @@ -772,6 +772,7 @@ export function PrsProvider({ children }: { children: React.ReactNode }) { const refreshCore = useCallback(async (options: { skipFreshWarmCache?: boolean; githubRefreshMode?: "await" | "background"; + githubRefreshArgs?: { prId?: string; prIds?: string[] }; } = {}) => { if (refreshInFlight.current) { refreshPending.current = true; @@ -796,7 +797,7 @@ export function PrsProvider({ children }: { children: React.ReactNode }) { setError(null); try { if (options.githubRefreshMode === "await") { - await window.ade.prs.refresh().catch(() => {}); + await window.ade.prs.refresh(options.githubRefreshArgs).catch(() => {}); } const shouldLoadWorkflowDiagnostics = activeTabRef.current !== "normal" || selectedPrIdRef.current !== null || currentRouteRequestsPrDiagnostics(); @@ -827,8 +828,17 @@ export function PrsProvider({ children }: { children: React.ReactNode }) { } }, [applyLocalPrState]); - const refresh = useCallback(async () => { - await refreshCore({ githubRefreshMode: "await" }); + const refresh = useCallback(async (args: { prId?: string; prIds?: string[] } = {}) => { + const prIds = [ + ...(args.prId ? [args.prId] : []), + ...(args.prIds ?? []), + ].map((prId) => String(prId ?? "").trim()).filter(Boolean); + const githubRefreshArgs = prIds.length === 1 + ? { prId: prIds[0] } + : prIds.length > 1 + ? { prIds } + : undefined; + await refreshCore({ githubRefreshMode: "await", githubRefreshArgs }); }, [refreshCore]); // Initial load diff --git a/apps/desktop/src/renderer/components/prs/tabs/GitHubTab.tsx b/apps/desktop/src/renderer/components/prs/tabs/GitHubTab.tsx index aadc33f00..3d99c77f6 100644 --- a/apps/desktop/src/renderer/components/prs/tabs/GitHubTab.tsx +++ b/apps/desktop/src/renderer/components/prs/tabs/GitHubTab.tsx @@ -27,7 +27,7 @@ type GitHubTabProps = { onSelectPr: (id: string | null) => void; selectedDetailTab?: PrDetailRouteTab | null; onDetailTabChange?: (tab: PrDetailRouteTab) => void; - onRefreshAll: () => Promise; + onRefreshAll: (args?: { prId?: string; prIds?: string[] }) => Promise; onOpenRebaseTab?: (laneId?: string) => void; onOpenQueueView?: (groupId: string) => void; }; @@ -738,19 +738,24 @@ export function GitHubTab({ }; }, [mergeContextByPrId, selectedLinkedPr]); - const handleSync = React.useCallback(async () => { + const handleSync = React.useCallback(async (args: { prId?: string; prIds?: string[] } = {}) => { setSyncing(true); startHotRefreshWindow(); try { + const targeted = Boolean(args.prId || (args.prIds?.length ?? 0) > 0); const includeExternalClosed = externalHistoryLoadedRef.current || filterRef.current !== "open"; - await Promise.all([ - onRefreshAll().catch(() => {}), - loadSnapshot({ - force: true, - ...(includeExternalClosed ? { includeExternalClosed: true } : {}), - }), - ]); + if (targeted) { + await onRefreshAll(args).catch(() => {}); + } else { + await Promise.all([ + onRefreshAll().catch(() => {}), + loadSnapshot({ + force: true, + ...(includeExternalClosed ? { includeExternalClosed: true } : {}), + }), + ]); + } } finally { setSyncing(false); } From ade313c9bc2932f20f4c0556e677df2547a85a05 Mon Sep 17 00:00:00 2001 From: Arul Sharma <31745423+arul28@users.noreply.github.com> Date: Thu, 14 May 2026 04:01:24 -0400 Subject: [PATCH 19/24] fix(work): reveal group runs and narrow conflicts --- .../components/files/FilesPage.test.tsx | 39 +++++++- .../renderer/components/files/FilesPage.tsx | 33 +++++-- .../renderer/components/run/RunPage.test.tsx | 70 +++++++++++++ .../src/renderer/components/run/RunPage.tsx | 97 +++++++++++++++---- 4 files changed, 209 insertions(+), 30 deletions(-) diff --git a/apps/desktop/src/renderer/components/files/FilesPage.test.tsx b/apps/desktop/src/renderer/components/files/FilesPage.test.tsx index 488586973..96d41f581 100644 --- a/apps/desktop/src/renderer/components/files/FilesPage.test.tsx +++ b/apps/desktop/src/renderer/components/files/FilesPage.test.tsx @@ -207,11 +207,11 @@ function LanesNavCapture() { return
{`${loc.pathname}${loc.search}`}
; } -function renderFilesPage(initialState?: Record) { +function renderFilesPage(initialState?: Record, props?: React.ComponentProps) { return render( - } /> + } /> } /> , @@ -556,6 +556,41 @@ describe("FilesPage", () => { expect(screen.queryByTestId("mock-monaco-editor")).toBeNull(); }); + it("stacks merge conflict panes when Files is embedded in a narrow Work drawer", async () => { + fileContents["src/index.ts"] = [ + "export function title() {", + "<<<<<<< HEAD", + " return \"ours\";", + "=======", + " return \"theirs\";", + ">>>>>>> feature", + "}", + "", + ].join("\n"); + + renderFilesPage( + { + openFilePath: "src/index.ts", + preferPrimaryWorkspace: true, + }, + { embedded: true }, + ); + + await waitForEditorText("<<<<<<< HEAD"); + fireEvent.click(screen.getByRole("button", { name: "MERGE" })); + + const layout = await screen.findByTestId("files-conflict-layout"); + expect(layout.getAttribute("data-layout")).toBe("stacked"); + expect(layout.className).toContain("flex-col"); + expect(layout.style.gridTemplateColumns).toBe(""); + + const hunks = screen.getByTestId("files-conflict-hunks"); + expect(hunks.style.maxHeight).toBe("42%"); + const mergeEditor = screen.getAllByRole("textbox") + .find((node): node is HTMLTextAreaElement => node instanceof HTMLTextAreaElement); + expect(mergeEditor?.value).toContain("<<<<<<< HEAD"); + }); + it("remaps clean open tabs when files are renamed", async () => { renderFilesPage({ openFilePath: "src/index.ts", diff --git a/apps/desktop/src/renderer/components/files/FilesPage.tsx b/apps/desktop/src/renderer/components/files/FilesPage.tsx index 5c17523ab..3bcf27cb2 100644 --- a/apps/desktop/src/renderer/components/files/FilesPage.tsx +++ b/apps/desktop/src/renderer/components/files/FilesPage.tsx @@ -2012,8 +2012,26 @@ export function FilesPage({ ) ) : ( -
-
+
+
CONFLICT HUNKS
-
+