feat(status-ui): improve concurrent session workflows

Add in-place session renewal, searchable model selection, clearer progress feedback, tool-result previews, and per-session presentation state.

Keep commands, errors, loading, and lifecycle indicators attached to their owning runtime so parallel tabs cannot interfere.
This commit is contained in:
2026-08-25 14:56:42 +02:00
parent 9f2c840ae7
commit 166b065ad4
20 changed files with 2211 additions and 430 deletions
+117 -13
View File
@@ -224,12 +224,10 @@ test("restores durable runtime IDs with two-at-a-time concurrency and preserves
startAdapter,
});
const starting = restored.start();
while (gates.length < 2)
await new Promise((resolve) => setImmediate(resolve));
while (gates.length < 2) await new Promise((resolve) => setImmediate(resolve));
assert.equal(gates.length, 2);
gates.splice(0).forEach((resolve) => resolve());
while (gates.length < 1)
await new Promise((resolve) => setImmediate(resolve));
while (gates.length < 1) await new Promise((resolve) => setImmediate(resolve));
assert.equal(gates.length, 1);
gates.splice(0).forEach((resolve) => resolve());
await starting;
@@ -290,11 +288,7 @@ test("serializes close behind an in-flight command and aborts the working runtim
test("forces a child kill and retains its lease until exit is confirmed", async () => {
const paths = await fixture();
const saved = await sessionFile(
paths.sessionRoot,
paths.home,
"forced-close",
);
const saved = await sessionFile(paths.sessionRoot, paths.home, "forced-close");
let killed;
let releaseStop;
let blockedSavedAdapter = false;
@@ -445,6 +439,68 @@ test("legacy new session releases old identity and makes its history reopenable"
await registry.stop();
});
test("renews one runtime in place, aborts work, and clears its active identity", async () => {
const paths = await fixture();
const saved = await sessionFile(
paths.sessionRoot,
paths.home,
"renew-me",
"renew-me-id",
);
let currentState = {
sessionFile: saved,
sessionId: "renew-me-id",
sessionName: "Renew me",
};
const adapters = adapterFactory({ stateFor: () => currentState });
const registry = createAgentRegistry({
homeWorktree: paths.home,
sessionRoot: paths.sessionRoot,
startAdapter: adapters.startAdapter,
});
const original = await registry.start();
const adapter = adapters.calls[0].adapter;
const originalSend = adapter.send.bind(adapter);
adapter.send = async (command) => {
if (command.type === "new_session") currentState = {};
return originalSend(command);
};
adapters.calls[0].options.onEvent({
type: "extension_ui_request",
data: { event: { id: "stale-renew-request", method: "confirm" } },
});
adapters.calls[0].options.onEvent({
type: "agent_state",
data: { state: "streaming" },
});
const renewed = await registry.renewSession(original.id);
const active = registry.getWorkspace().directories[0].runtimes[0];
assert.equal(renewed.id, original.id);
assert.equal(renewed.runtimeId, original.runtimeId);
assert.equal(active.worktreePath, paths.home);
assert.equal(active.sessionPath, undefined);
assert.equal(active.sessionId, undefined);
assert.equal(active.label, "New session");
assert.equal(active.state, "idle");
assert.equal(active.attention, false);
assert.deepEqual(
adapter.sent.slice(-3).map((command) => command.type),
["abort", "new_session", "get_state"],
);
assert.equal(
registry
.workspaceEventsAfter(0)
.events.some((event) => event.type === "runtime_renewed"),
true,
);
currentState = { sessionFile: saved, sessionId: "renew-me-id" };
const reopened = await registry.openSessionRuntime(paths.home, saved);
assert.notEqual(reopened.runtimeId, original.runtimeId);
await registry.stop();
});
test("persists a first-prompt session when the runtime settles and awaits refresh on shutdown", async () => {
const paths = await fixture();
const createdPath = await sessionFile(
@@ -611,6 +667,28 @@ test("non-default runtime snapshots do not replace the legacy session pointer",
await registry.stop();
});
test("reports available-model RPC failures in runtime snapshots", async () => {
const paths = await fixture();
const adapters = adapterFactory();
const registry = createAgentRegistry({
homeWorktree: paths.home,
sessionRoot: paths.sessionRoot,
startAdapter: adapters.startAdapter,
});
const runtime = await registry.start();
const originalSend = adapters.calls[0].adapter.send;
adapters.calls[0].adapter.send = async function (command) {
if (command.type === "get_available_models")
throw new Error("model registry unavailable");
return originalSend.call(this, command);
};
const snapshot = await registry.getSessionRuntimeSnapshot(runtime.runtimeId);
assert.deepEqual(snapshot.models.data.models, []);
assert.match(snapshot.models.error, /model registry unavailable/);
await registry.stop();
});
test("retains a corrupt workspace manifest and reports its issue", async () => {
const paths = await fixture();
await mkdir(paths.sessionRoot, { recursive: true });
@@ -622,10 +700,7 @@ test("retains a corrupt workspace manifest and reports its issue", async () => {
startAdapter: adapterFactory().startAdapter,
});
await registry.start();
assert.equal(
registry.getWorkspace().issue.code,
"invalid_workspace_manifest",
);
assert.equal(registry.getWorkspace().issue.code, "invalid_workspace_manifest");
assert.equal(await readFile(manifestPath, "utf8"), "{broken");
await registry.stop();
});
@@ -1025,6 +1100,35 @@ test("manual restart ignores late extension dialogs from the replaced child", as
await registry.stop();
});
test("manual restart rejects exhausted recovery and marks the runtime failed", async () => {
const paths = await fixture();
const adapters = adapterFactory();
let starts = 0;
const registry = createAgentRegistry({
homeWorktree: paths.home,
sessionRoot: paths.sessionRoot,
startAdapter(options) {
starts += 1;
if (starts > 1) throw new Error("replacement failed to start");
return adapters.startAdapter(options);
},
maxRecoveryAttempts: 1,
recoveryDelayForAttempt: () => 0,
sleep: async () => {},
});
const runtime = await registry.start();
await assert.rejects(
registry.restart(runtime.id),
/Pi did not restart after the recovery attempts were exhausted/,
);
assert.equal(
registry.getWorkspace().directories[0].runtimes[0].state,
"failed",
);
await registry.stop();
});
test("child exit clears extension dialogs that recovery cannot answer", async () => {
const paths = await fixture();
const adapters = adapterFactory();
+9
View File
@@ -79,6 +79,15 @@ test("accepts bridge-managed directory and session operations", () => {
agentId: "agent-main",
},
);
assert.deepEqual(
parseRequestFrame(request({ op: "renew_session", agentId: "agent-main" })),
{
version: "v1",
id: "request-1",
op: "renew_session",
agentId: "agent-main",
},
);
assert.deepEqual(
parseRequestFrame(
request({
+6 -1
View File
@@ -18,11 +18,12 @@ test("wires the desktop to stable runtime workspace APIs", async () => {
"close_session_runtime",
"list_directory_sessions",
"get_session_runtime_snapshot",
"renew_session",
"subscribe_workspace",
])
assert.match(`${app}\n${hook}`, new RegExp(operation));
assert.doesNotMatch(app, /switch_session/);
assert.doesNotMatch(app, /new_session/);
assert.doesNotMatch(app, /["']new_session["']/);
assert.match(reducer, /WorkspaceState\["runtimesById"\]/);
assert.match(reducer, /requestGeneration/);
assert.match(reducer, /loadedThroughSeq/);
@@ -54,6 +55,10 @@ test("renders directory sidebar, accessible session tabs, warnings, and reduced
assert.match(app, /Many Pi sessions are open/);
assert.match(css, /prefers-reduced-motion/);
assert.match(css, /grid-template-columns:\s*178px minmax\(0, 1fr\)/);
assert.match(
css,
/\.session-row-content\s*>\s*strong\s*\{[^}]*color:\s*#f1f3f4/s,
);
});
test("preserves native session naming, compaction, composer, and extension responses", async () => {