feat(quick-pane): promote active runtime

This commit is contained in:
2026-08-28 12:07:50 +02:00
parent 0a318f5ab6
commit b44b1be4b7
19 changed files with 1298 additions and 126 deletions
+1
View File
@@ -125,6 +125,7 @@ test("accepts additive multi-session runtime operations and numeric workspace cu
{ op: "get_workspace_summary" },
{ op: "create_session_runtime", payload: { worktreePath } },
{ op: "create_quick_runtime", payload: { worktreePath } },
{ op: "promote_quick_runtime", payload: { runtimeId: "runtime-1" } },
{ op: "open_session_runtime", payload: { worktreePath, sessionPath } },
{ op: "close_session_runtime", payload: { runtimeId: "runtime-1" } },
{ op: "close_quick_runtime", payload: { runtimeId: "runtime-1" } },
+28 -2
View File
@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
test("Quick Pane polls snapshots, renders answers, and gates manual transcript handoff", async () => {
test("Quick Pane polls snapshots, renders answers, and promotes runtime without a handoff", async () => {
const source = await readFile(
new URL("../ui/src/quick-pane/QuickPane.tsx", import.meta.url),
"utf8",
@@ -13,7 +13,12 @@ test("Quick Pane polls snapshots, renders answers, and gates manual transcript h
assert.match(source, /className="quick-pane-notice escalation"/);
assert.match(source, /className="quick-pane-assistant-footer"/);
assert.match(source, /Continue in full session/);
assert.match(source, /Quick Pane transcript:/);
assert.match(
source,
/await invoke<\{ runtime: Runtime \}>\("promote_quick_runtime", \{\s+runtimeId: current\.runtimeId,/,
);
assert.doesNotMatch(source, /Quick Pane transcript:/);
assert.doesNotMatch(source, /create_session_runtime/);
assert.doesNotMatch(source, /maximumSeconds|Quick time budget/);
});
@@ -166,6 +171,27 @@ test("Quick Pane dismiss and mutually exclusive window visibility stay native-se
assert.ok(quickClearMain >= 0 && quickClearMain < showQuick);
});
test("native promotion owns visibility and Main consumes pending runtime after startup refresh", async () => {
const [nativeSource, workspaceSource] = await Promise.all([
readFile(new URL("../ui/src-tauri/src/lib.rs", import.meta.url), "utf8"),
readFile(new URL("../ui/src/workspace/useWorkspace.ts", import.meta.url), "utf8"),
]);
const promotion = nativeSource.slice(
nativeSource.indexOf("fn show_main_for_promoted_runtime"),
nativeSource.indexOf("fn toggle_quick_pane"),
);
assert.match(promotion, /get_or_create_main_with_visibility\(app, false\)/);
assert.ok(promotion.indexOf("quick_pane\n .hide()") < promotion.indexOf("window\n .show()"));
assert.ok(promotion.indexOf('visibility.hidden("quick-pane")') < promotion.indexOf("window\n .show()"));
assert.match(promotion, /\*pending = Some\(runtime_id\)/);
assert.match(nativeSource, /fn get_pending_promoted_runtime/);
assert.match(nativeSource, /fn consume_pending_promoted_runtime/);
assert.match(workspaceSource, /await promotedListenerReady/);
assert.match(workspaceSource, /await refresh\(loaded\)/);
assert.match(workspaceSource, /get_pending_promoted_runtime/);
assert.match(workspaceSource, /consume_pending_promoted_runtime/);
});
test("Quick Pane routes extension responses through existing bridge command", async () => {
const source = await readFile(
new URL("../ui/src/quick-pane/QuickPane.tsx", import.meta.url),
+473
View File
@@ -0,0 +1,473 @@
import assert from "node:assert/strict";
import {
lstat,
mkdir,
mkdtemp,
readFile,
rename,
rm,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import {
createAgentRegistry,
promoteQuickSessionDirectory,
sessionDirectoryPath,
} from "../src/bridge/agent-registry.js";
import { createWorkspaceStore } from "../src/bridge/workspace-store.js";
function adapters() {
const calls = [];
return {
calls,
startAdapter(options) {
const adapter = {
sent: [],
stopped: false,
state: {},
async send(command) {
this.sent.push(command);
return {
type: "response",
command: command.type,
success: true,
data: command.type === "get_state" ? this.state : {},
};
},
respondToExtension() {},
async stop() {
this.stopped = true;
},
};
calls.push({ options, adapter });
return adapter;
},
};
}
test("promotes an active quick runtime in place and makes stale quick cleanup harmless", async () => {
const root = await mkdtemp(join(tmpdir(), "pi-quick-promote-"));
const home = join(root, "home");
await mkdir(home);
const fixture = adapters();
const registry = createAgentRegistry({
homeWorktree: home,
sessionRoot: join(root, "sessions"),
startAdapter: fixture.startAdapter,
});
await registry.start();
const quick = await registry.createQuickRuntime(home);
const quickAdapter = fixture.calls.at(-1).adapter;
await registry.route(quick.agentId, "prompt", { message: "keep working" });
const events = [];
const subscription = registry.subscribeWorkspace(0, (event) =>
events.push(event),
);
const [promoted, repeated] = await Promise.all([
registry.promoteQuickRuntime(quick.runtimeId),
registry.promoteQuickRuntime(quick.runtimeId),
]);
assert.equal(promoted.runtimeId, quick.runtimeId);
assert.equal(repeated.agentId, quick.agentId);
assert.equal(fixture.calls.at(-1).adapter, quickAdapter);
assert.equal(quickAdapter.stopped, false);
assert.equal(promoted.sessionPath, undefined);
assert.ok(events.some((event) => event.type === "runtime_promoted"));
assert.equal(
registry
.getWorkspace()
.directories[0].runtimes.some(
(runtime) => runtime.runtimeId === quick.runtimeId,
),
true,
);
assert.match(
await readFile(join(root, "sessions", "bridge-workspace-v2.json"), "utf8"),
new RegExp(quick.runtimeId),
);
assert.match(
await readFile(
join(
sessionDirectoryPath(join(root, "sessions"), home),
"bridge-agent.json",
),
"utf8",
),
/"worktreePath"/,
);
await registry.closeQuickRuntime(quick.runtimeId);
assert.equal(quickAdapter.stopped, false);
await registry.route(quick.agentId, "abort");
subscription.unsubscribe();
await registry.stop();
});
test("promotion refreshes active JSONL identity without replacing runtime state", async () => {
const root = await mkdtemp(join(tmpdir(), "pi-quick-identity-"));
const home = join(root, "home");
await mkdir(home);
const fixture = adapters();
const registry = createAgentRegistry({
homeWorktree: home,
sessionRoot: join(root, "sessions"),
startAdapter: fixture.startAdapter,
});
await registry.start();
const quick = await registry.createQuickRuntime(home);
const call = fixture.calls.at(-1);
const quickPath = join(call.options.sessionDir, "active.jsonl");
await writeFile(quickPath, '{"type":"session"}\n');
call.adapter.state = { sessionFile: quickPath, sessionId: "session-active" };
call.options.onEvent({
type: "queue",
data: { event: { pendingMessageCount: 2 } },
});
call.options.onEvent({
type: "extension_ui_request",
data: { event: { id: "extension-1", method: "confirm" } },
});
await registry.route(quick.agentId, "set_model", {
provider: "test",
modelId: "model-1",
});
await registry.route(quick.agentId, "set_thinking_level", { level: "high" });
const promoted = await registry.promoteQuickRuntime(quick.runtimeId);
const snapshot = await registry.getSessionRuntimeSnapshot(quick.runtimeId);
assert.equal(promoted.runtimeId, quick.runtimeId);
assert.equal(promoted.agentId, quick.agentId);
assert.equal(promoted.sessionId, "session-active");
assert.match(promoted.sessionPath, /active\.jsonl$/);
assert.equal(snapshot.runtime.queueCount, 2);
assert.equal(snapshot.extensions[0].id, "extension-1");
assert.deepEqual(
call.adapter.sent.filter((command) =>
["set_model", "set_thinking_level"].includes(command.type),
),
[
{ type: "set_model", provider: "test", modelId: "model-1" },
{ type: "set_thinking_level", level: "high" },
],
);
assert.equal(fixture.calls.at(-1).adapter, call.adapter);
await registry.stop();
});
test("session-directory migration rolls back move and symlink failures for retry", async () => {
const root = await mkdtemp(join(tmpdir(), "pi-quick-directory-rollback-"));
const quickDir = join(root, "quick");
const sessionDir = join(root, "session");
await mkdir(quickDir);
await writeFile(join(quickDir, "active.jsonl"), "history");
let renameCalls = 0;
await assert.rejects(
promoteQuickSessionDirectory(quickDir, sessionDir, {
renameFile: async (...args) => {
renameCalls += 1;
if (renameCalls === 2) throw new Error("move failed");
return rename(...args);
},
}),
);
assert.equal(
await readFile(join(quickDir, "active.jsonl"), "utf8"),
"history",
);
await assert.rejects(
promoteQuickSessionDirectory(quickDir, sessionDir, {
makeSymlink: async () => {
throw new Error("symlink failed");
},
}),
);
assert.equal(
await readFile(join(quickDir, "active.jsonl"), "utf8"),
"history",
);
const transaction = await promoteQuickSessionDirectory(quickDir, sessionDir);
await transaction.finalize();
assert.equal(
await readFile(join(sessionDir, "active.jsonl"), "utf8"),
"history",
);
});
test("promotion queues abort and close until identity persistence completes", async () => {
const root = await mkdtemp(join(tmpdir(), "pi-quick-promote-race-"));
const home = join(root, "home");
await mkdir(home);
const fixture = adapters();
const registry = createAgentRegistry({
homeWorktree: home,
sessionRoot: join(root, "sessions"),
startAdapter: fixture.startAdapter,
});
await registry.start();
const quick = await registry.createQuickRuntime(home);
const call = fixture.calls.at(-1);
const quickPath = join(call.options.sessionDir, "active.jsonl");
await writeFile(quickPath, "history");
call.adapter.state = { sessionFile: quickPath, sessionId: "session-race" };
const send = call.adapter.send.bind(call.adapter);
let releaseIdentity;
let blockIdentity = true;
call.adapter.send = async (command) => {
if (command.type !== "get_state" || !blockIdentity) return send(command);
return new Promise((resolve) => {
releaseIdentity = () => {
blockIdentity = false;
resolve({
type: "response",
command: "get_state",
success: true,
data: call.adapter.state,
});
};
});
};
const promotion = registry.promoteQuickRuntime(quick.runtimeId);
while (!releaseIdentity) await new Promise((resolve) => setImmediate(resolve));
const abort = registry.route(quick.agentId, "abort");
const close = registry.closeSessionRuntime(quick.runtimeId);
assert.notEqual(call.adapter.sent.at(-1).type, "abort");
releaseIdentity();
await Promise.all([promotion, abort, close]);
assert.ok(call.adapter.sent.some((command) => command.type === "abort"));
assert.equal(call.adapter.stopped, true);
await registry.stop();
});
test("promotion reserves moved JSONL before blocking get_state and releases on rollback", async () => {
const root = await mkdtemp(
join(tmpdir(), "pi-quick-promote-get-state-lease-"),
);
const home = join(root, "home");
const sessionRoot = join(root, "sessions");
await mkdir(home);
const fixture = adapters();
const startAdapter = (options) => {
const adapter = fixture.startAdapter(options);
if (options.sessionPath) adapter.state = { sessionFile: options.sessionPath };
return adapter;
};
const registry = createAgentRegistry({
homeWorktree: home,
sessionRoot,
startAdapter,
});
await registry.start();
const quick = await registry.createQuickRuntime(home);
const call = fixture.calls.at(-1);
const quickPath = join(call.options.sessionDir, "active.jsonl");
const promotedPath = join(
sessionDirectoryPath(sessionRoot, home),
"active.jsonl",
);
await writeFile(quickPath, "history");
call.adapter.state = {
sessionFile: quickPath,
sessionId: "session-get-state-lease",
};
call.options.onEvent({ type: "agent_state", data: { state: "idle" } });
while (
!(await registry.getSessionRuntimeSnapshot(quick.runtimeId)).runtime
.sessionPath
)
await new Promise((resolve) => setImmediate(resolve));
const send = call.adapter.send.bind(call.adapter);
let rejectState;
call.adapter.send = async (command) => {
if (command.type !== "get_state") return send(command);
return new Promise((_, reject) => {
rejectState = () => reject(new Error("get_state failed"));
});
};
const promotion = registry.promoteQuickRuntime(quick.runtimeId);
while (!rejectState) await new Promise((resolve) => setImmediate(resolve));
assert.equal((await lstat(call.options.sessionDir)).isSymbolicLink(), true);
assert.equal(await readFile(promotedPath, "utf8"), "history");
const childrenBeforeOpen = fixture.calls.length;
await assert.rejects(
registry.openSessionRuntime(home, promotedPath),
/already open/,
);
assert.equal(fixture.calls.length, childrenBeforeOpen);
rejectState();
await assert.rejects(promotion, /get_state failed/);
await writeFile(promotedPath, "history");
await registry.openSessionRuntime(home, promotedPath);
assert.equal(fixture.calls.length, childrenBeforeOpen + 1);
await registry.stop();
});
test("promotion reserves its canonical JSONL before reference and manifest I/O", async () => {
const root = await mkdtemp(join(tmpdir(), "pi-quick-promote-lease-"));
const home = join(root, "home");
const sessionRoot = join(root, "sessions");
await mkdir(home);
const fixture = adapters();
let quick;
let releasePromotionSave;
const registry = createAgentRegistry({
homeWorktree: home,
sessionRoot,
startAdapter: fixture.startAdapter,
workspaceStoreFactory(rootPath) {
const store = createWorkspaceStore(rootPath);
return {
...store,
save(next) {
if (
!next.runtimes.some((runtime) => runtime.runtimeId === quick?.runtimeId)
)
return store.save(next);
return new Promise((resolve, reject) => {
releasePromotionSave = () => store.save(next).then(resolve, reject);
});
},
};
},
});
await registry.start();
quick = await registry.createQuickRuntime(home);
const call = fixture.calls.at(-1);
const quickPath = join(call.options.sessionDir, "active.jsonl");
await writeFile(quickPath, "history");
call.adapter.state = { sessionFile: quickPath, sessionId: "session-lease" };
const promotedPath = join(
sessionDirectoryPath(sessionRoot, home),
"active.jsonl",
);
const promotion = registry.promoteQuickRuntime(quick.runtimeId);
while (!releasePromotionSave)
await new Promise((resolve) => setImmediate(resolve));
await assert.rejects(
registry.openSessionRuntime(home, promotedPath),
/already open/,
);
releasePromotionSave();
await promotion;
await registry.stop();
});
test("concurrent manifest persistence retains promoted runtime for restart", async () => {
const root = await mkdtemp(join(tmpdir(), "pi-quick-promote-persist-"));
const home = join(root, "home");
const sessionRoot = join(root, "sessions");
await mkdir(home);
const fixture = adapters();
let releasePromotionSave;
let holdPromotionSave = true;
let quick;
const registry = createAgentRegistry({
homeWorktree: home,
sessionRoot,
startAdapter: fixture.startAdapter,
workspaceStoreFactory(rootPath) {
const store = createWorkspaceStore(rootPath);
return {
...store,
save(next) {
if (
!holdPromotionSave ||
!next.runtimes.some((runtime) => runtime.runtimeId === quick?.runtimeId)
)
return store.save(next);
holdPromotionSave = false;
return new Promise((resolve, reject) => {
releasePromotionSave = () => store.save(next).then(resolve, reject);
});
},
};
},
});
await registry.start();
quick = await registry.createQuickRuntime(home);
const call = fixture.calls.at(-1);
const quickPath = join(call.options.sessionDir, "active.jsonl");
await writeFile(quickPath, "history");
call.adapter.state = { sessionFile: quickPath, sessionId: "session-persist" };
const promotion = registry.promoteQuickRuntime(quick.runtimeId);
while (!releasePromotionSave)
await new Promise((resolve) => setImmediate(resolve));
const concurrentOpen = registry.createSessionRuntime(home);
releasePromotionSave();
await Promise.all([promotion, concurrentOpen]);
const manifest = JSON.parse(
await readFile(join(sessionRoot, "bridge-workspace-v2.json"), "utf8"),
);
assert.ok(
manifest.runtimes.some((runtime) => runtime.runtimeId === quick.runtimeId),
);
await registry.stop();
const restored = adapters();
const originalStart = restored.startAdapter;
restored.startAdapter = (options) => {
const adapter = originalStart(options);
if (options.sessionPath) adapter.state = { sessionFile: options.sessionPath };
return adapter;
};
const restarted = createAgentRegistry({
homeWorktree: home,
sessionRoot,
startAdapter: restored.startAdapter,
});
await restarted.start();
assert.ok(
restarted
.getWorkspace()
.directories.flatMap((directory) => directory.runtimes)
.some(
(runtime) =>
runtime.runtimeId === quick.runtimeId && runtime.state !== "failed",
),
);
await restarted.stop();
});
test("promotion reference and manifest failures roll back quick runtime for retry", async () => {
const root = await mkdtemp(join(tmpdir(), "pi-quick-persist-rollback-"));
const home = join(root, "home");
await mkdir(home);
const fixture = adapters();
const sessionRoot = join(root, "sessions");
const registry = createAgentRegistry({
homeWorktree: home,
sessionRoot,
startAdapter: fixture.startAdapter,
});
await registry.start();
const quick = await registry.createQuickRuntime(home);
const quickDir = fixture.calls.at(-1).options.sessionDir;
await writeFile(join(quickDir, "active.jsonl"), "history");
const sessionDir = sessionDirectoryPath(sessionRoot, home);
const referencePath = join(sessionDir, "bridge-agent.json");
await mkdir(referencePath);
await assert.rejects(registry.promoteQuickRuntime(quick.runtimeId));
assert.equal(
await readFile(join(quickDir, "active.jsonl"), "utf8"),
"history",
);
assert.equal((await lstat(quickDir)).isDirectory(), true);
await rm(referencePath, { recursive: true });
const workspacePath = join(sessionRoot, "bridge-workspace-v2.json");
await rm(workspacePath);
await mkdir(workspacePath);
await assert.rejects(registry.promoteQuickRuntime(quick.runtimeId));
assert.equal(
await readFile(join(quickDir, "active.jsonl"), "utf8"),
"history",
);
assert.equal((await lstat(quickDir)).isDirectory(), true);
await rm(workspacePath, { recursive: true });
assert.equal(
(await registry.promoteQuickRuntime(quick.runtimeId)).runtimeId,
quick.runtimeId,
);
await registry.stop();
});