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
+50
View File
@@ -194,6 +194,56 @@ describe("workspace reducer", () => {
]);
expect(state.runtimesById.a.modelError).toContain("RPC timed out");
});
it("preserves reasoning model metadata from runtime snapshots", () => {
let state = reconcileWorkspace(initialWorkspaceState, workspace);
state = workspaceReducer(state, {
type: "snapshotRequested",
runtimeId: "a",
generation: 1,
});
state = workspaceReducer(state, {
type: "snapshotLoaded",
runtimeId: "a",
generation: 1,
snapshot: {
bridgeInstanceId: "epoch",
latestSeq: 1,
runtime: runtime("a"),
state: {
data: {
model: {
provider: "provider",
id: "reasoning",
reasoning: true,
thinkingLevelMap: { high: null, xhigh: "xhigh" },
},
},
},
models: {
data: {
models: [
{
provider: "provider",
id: "reasoning",
reasoning: true,
thinkingLevelMap: { high: null, xhigh: "xhigh" },
},
],
},
},
extensions: [],
},
});
expect(state.runtimesById.a.state.model).toEqual({
provider: "provider",
id: "reasoning",
reasoning: true,
thinkingLevelMap: { high: null, xhigh: "xhigh" },
});
expect(state.runtimesById.a.models[0]).toEqual(
state.runtimesById.a.state.model,
);
});
it("removes the optimistic submission when Pi returns its structured user message", () => {
let state = reconcileWorkspace(initialWorkspaceState, workspace);
state = workspaceReducer(state, {
+13 -8
View File
@@ -60,19 +60,23 @@ export type Message = {
| string
| Array<{ type?: string; text?: string; thinking?: string; name?: string }>;
};
type ThinkingLevel =
| "off"
| "minimal"
| "low"
| "medium"
| "high"
| "xhigh"
| "max";
export type AgentState = {
isStreaming?: boolean;
thinkingLevel?: string;
thinkingLevel?: ThinkingLevel;
sessionName?: string;
sessionId?: string;
messageCount?: number;
pendingMessageCount?: number;
model?: {
provider?: string;
id?: string;
name?: string;
contextWindow?: number;
};
model?: Model & { contextWindow?: number };
};
export type SessionStats = {
tokens?: {
@@ -93,7 +97,8 @@ export type Model = {
provider?: string;
id?: string;
name?: string;
thinkingLevelMap?: Record<string, string>;
reasoning?: boolean;
thinkingLevelMap?: Partial<Record<ThinkingLevel, string | null>>;
};
export type Extension = {
id?: string;
+48 -1
View File
@@ -28,6 +28,9 @@ export function useWorkspace() {
const legacyMigrationPending = useRef(false);
const disposed = useRef(true);
const refreshGeneration = useRef(0);
const workspaceReady = useRef(false);
const pendingPromotedRuntime = useRef<string | undefined>(undefined);
const consumingPromotion = useRef(false);
const loadSnapshot = useCallback(
async (runtimeId: string, options: { throwOnError?: boolean } = {}) => {
@@ -150,10 +153,44 @@ export function useWorkspace() {
[loadSnapshot],
);
const consumePromotedRuntime = useCallback(() => {
if (!workspaceReady.current || consumingPromotion.current) return;
consumingPromotion.current = true;
void (async () => {
while (pendingPromotedRuntime.current && !disposed.current) {
const runtimeId = pendingPromotedRuntime.current;
pendingPromotedRuntime.current = undefined;
await refresh();
if (disposed.current) return;
dispatch({ type: "selectRuntime", runtimeId });
await loadSnapshot(runtimeId);
if (disposed.current) return;
await invoke("consume_pending_promoted_runtime", { runtimeId });
}
})()
.catch(() => {})
.finally(() => {
consumingPromotion.current = false;
if (pendingPromotedRuntime.current) consumePromotedRuntime();
});
}, [loadSnapshot, refresh]);
useEffect(() => {
disposed.current = false;
workspaceReady.current = false;
let stop: (() => void) | undefined;
let stopNewRuntime: (() => void) | undefined;
let stopPromotedRuntime: (() => void) | undefined;
const promotedListenerReady = listen<{ runtimeId: string }>(
"workspace-promoted-runtime",
(message) => {
pendingPromotedRuntime.current = message.payload.runtimeId;
consumePromotedRuntime();
},
).then((unlisten) => {
if (disposed.current) unlisten();
else stopPromotedRuntime = unlisten;
});
void listen<{
phase: "starting" | "ready" | "error";
runtimeId?: string;
@@ -197,6 +234,7 @@ export function useWorkspace() {
"runtime_recovered",
"runtime_renewed",
"runtime_restore_failed",
"runtime_promoted",
].includes(eventType ?? "")
) {
void refresh();
@@ -215,10 +253,17 @@ export function useWorkspace() {
});
void (async () => {
try {
await promotedListenerReady;
const loaded = await loadPresentation();
if (disposed.current) return;
await refresh(loaded);
if (disposed.current) return;
workspaceReady.current = true;
const runtimeId = await invoke<string | null>(
"get_pending_promoted_runtime",
).catch(() => null);
if (runtimeId) pendingPromotedRuntime.current = runtimeId;
consumePromotedRuntime();
initialized.current = true;
} catch (error) {
if (!disposed.current)
@@ -243,9 +288,11 @@ export function useWorkspace() {
document.addEventListener("visibilitychange", visibility);
return () => {
disposed.current = true;
workspaceReady.current = false;
refreshGeneration.current += 1;
stop?.();
stopNewRuntime?.();
stopPromotedRuntime?.();
void invoke("unsubscribe_workspace").catch(() => {});
window.removeEventListener("focus", visibility);
window.removeEventListener("blur", visibility);
@@ -253,7 +300,7 @@ export function useWorkspace() {
Object.values(timers.current).forEach(clearTimeout);
void appWindow;
};
}, [refresh, scheduleSnapshot]);
}, [consumePromotedRuntime, refresh, scheduleSnapshot]);
useEffect(() => {
if (!initialized.current) return;