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
+68 -19
View File
@@ -16,6 +16,28 @@ const idleProgress = {
toolCount: 0,
};
function snapshotProgress(runtime: RuntimeSummary) {
if (runtime.state === "streaming")
return {
phase: "working" as const,
detail: "Thinking and preparing a response",
toolCount: 0,
};
if (runtime.state === "recovering")
return {
phase: "recovering" as const,
detail: "Restoring this Pi session",
toolCount: 0,
};
if (runtime.state === "failed")
return {
phase: "error" as const,
detail: runtime.error?.message ?? "This Pi session failed",
toolCount: 0,
};
return idleProgress;
}
function messageText(message: RuntimeView["messages"][number]) {
if (typeof message.content === "string") return message.content;
return (message.content ?? [])
@@ -57,6 +79,7 @@ export function runtimeView(
messages: previous?.messages ?? [],
commands: previous?.commands ?? [],
models: previous?.models ?? [],
modelError: previous?.modelError,
extensions: previous?.extensions ?? [],
extensionStatuses: previous?.extensionStatuses ?? {},
extensionWidgets: previous?.extensionWidgets ?? {},
@@ -67,6 +90,7 @@ export function runtimeView(
scroll: previous?.scroll ?? { atBottom: true },
unread: previous?.unread ?? false,
loading: previous?.loading ?? false,
snapshotRetry: previous?.snapshotRetry ?? false,
error: previous?.error,
requestGeneration: previous?.requestGeneration ?? 0,
loadedThroughSeq: previous?.loadedThroughSeq ?? 0,
@@ -84,6 +108,7 @@ function resetRuntimeView(
messages: [],
commands: [],
models: [],
modelError: undefined,
extensions: [],
extensionStatuses: {},
extensionWidgets: {},
@@ -92,6 +117,7 @@ function resetRuntimeView(
unread: false,
progress: idleProgress,
loading: false,
snapshotRetry: false,
error: undefined,
requestGeneration: 0,
loadedThroughSeq: 0,
@@ -147,9 +173,7 @@ export function reconcileWorkspace(
ids,
);
}
const paths = workspace.directories.map(
(directory) => directory.worktreePath,
);
const paths = workspace.directories.map((directory) => directory.worktreePath);
const directoryOrder = ordered(
persisted?.directoryOrder ?? state.directoryOrder,
paths,
@@ -158,9 +182,7 @@ export function reconcileWorkspace(
const selectedRuntimeId =
preferred && runtimesById[preferred]
? preferred
: directoryOrder.flatMap(
(path) => runtimeOrderByDirectory[path] ?? [],
)[0];
: directoryOrder.flatMap((path) => runtimeOrderByDirectory[path] ?? [])[0];
const selectedDirectoryPath = selectedRuntimeId
? runtimesById[selectedRuntimeId]?.summary.worktreePath
: directoryOrder[0];
@@ -261,6 +283,26 @@ export function applyWorkspaceEvent(
},
};
const id = event.data?.runtimeId;
if (
event.type === "runtime_renewed" &&
id &&
state.runtimesById[id] &&
event.data?.runtime
) {
const renewed = resetRuntimeView(event.data.runtime, state.runtimesById[id]);
return {
...state,
bridge: { ...state.bridge, latestSeq: event.seq, status: "resyncing" },
runtimesById: {
...state.runtimesById,
[id]: {
...renewed,
draft: "",
scroll: { atBottom: true },
},
},
};
}
if (
["runtime_opened", "runtime_recovered", "runtime_restore_failed"].includes(
event.type ?? "",
@@ -456,8 +498,7 @@ export function workspaceReducer(
return {
...state,
selectedDirectoryPath: action.worktreePath,
selectedRuntimeId:
state.runtimeOrderByDirectory[action.worktreePath]?.[0],
selectedRuntimeId: state.runtimeOrderByDirectory[action.worktreePath]?.[0],
};
if (action.type === "selectRuntime")
return {
@@ -483,20 +524,29 @@ export function workspaceReducer(
const view = { ...state.runtimesById[runtimeId] };
if (action.type === "snapshotRequested") {
view.loading = true;
view.snapshotRetry = false;
view.error = undefined;
view.requestGeneration = action.generation;
} else if (action.type === "snapshotFailed") {
if (view.requestGeneration !== action.generation) return state;
view.loading = false;
view.snapshotRetry = false;
view.error = action.error;
} else if (action.type === "snapshotLoaded") {
if (view.requestGeneration !== action.generation) return state;
if (
view.requestGeneration !== action.generation ||
action.snapshot.bridgeInstanceId !== state.bridge.instanceId ||
action.snapshot.latestSeq < view.loadedThroughSeq
)
return state;
) {
view.loading = false;
view.snapshotRetry = true;
return {
...state,
runtimesById: { ...state.runtimesById, [runtimeId]: view },
};
}
view.loading = false;
view.snapshotRetry = false;
view.summary = action.snapshot.runtime;
view.state = action.snapshot.state?.data ?? {};
view.stats = action.snapshot.stats?.data ?? {};
@@ -506,16 +556,15 @@ export function workspaceReducer(
view.messages,
);
view.commands = action.snapshot.commands?.data?.commands ?? [];
view.models = action.snapshot.models?.data?.models ?? [];
if (action.snapshot.models?.error) {
view.modelError = `Could not load available Pi models: ${action.snapshot.models.error}`;
} else {
view.models = action.snapshot.models?.data?.models ?? [];
view.modelError = undefined;
}
view.extensions = action.snapshot.extensions ?? [];
view.loadedThroughSeq = action.snapshot.latestSeq;
view.progress = view.state.isStreaming
? {
phase: "working",
detail: "Thinking and preparing a response",
toolCount: 0,
}
: idleProgress;
view.progress = snapshotProgress(action.snapshot.runtime);
} else if (action.type === "draftChanged") view.draft = action.draft;
else if (action.type === "scrollChanged")
view.scroll = { atBottom: action.atBottom, scrollTop: action.scrollTop };