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:
+68
-19
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user