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
+330 -114
View File
@@ -31,15 +31,57 @@ type LocalOperation = {
| "closing"
| "listing"
| "submitting"
| "command";
| "command"
| "restarting"
| "renewing";
detail: string;
runtimeId?: string;
command?: string;
};
type CommandNotice = {
kind: "error";
message: string;
runtimeId: string;
};
const modelRpcCommands = new Set([
"get_available_models",
"set_model",
"cycle_model",
"set_thinking_level",
"cycle_thinking_level",
]);
function requestedCommand(command: string, args: Record<string, unknown>) {
return command === "pi_rpc_command" && typeof args.command === "string"
? args.command
: command;
}
function commandProgress(command: string) {
if (command === "restart") return "Restarting Pi and restoring the session…";
if (command === "renew_session")
return "Clearing session history and starting anew…";
if (command === "get_available_models") return "Loading available Pi models…";
if (command === "set_model") return "Changing Pi model…";
if (command === "cycle_model") return "Cycling Pi model…";
if (command === "set_thinking_level") return "Updating thinking level…";
if (command === "cycle_thinking_level") return "Cycling thinking level…";
return "Updating Pi session…";
}
export default function App() {
const { state, dispatch, refresh, loadSnapshot } = useWorkspace();
const [status, setStatus] = useState("Connecting to Pi Status Bridge…");
const [commandForm, setCommandForm] = useState<RpcCommand | undefined>();
const [runtimeStatuses, setRuntimeStatuses] = useState<Record<string, string>>(
{},
);
const [commandForms, setCommandForms] = useState<Record<string, RpcCommand>>(
{},
);
const [commandNotices, setCommandNotices] = useState<
Record<string, CommandNotice>
>({});
const [view, setView] = useState<"conversation" | "settings">("conversation");
const [adding, setAdding] = useState(false);
const [folderPath, setFolderPath] = useState("");
@@ -63,24 +105,50 @@ export default function App() {
state.runtimesById,
],
);
const previousSelected = useRef<string | undefined>(undefined);
const mountedRef = useRef(true);
const operationSequence = useRef(0);
const [operation, setOperation] = useState<LocalOperation | undefined>();
const [abortingRuntimeId, setAbortingRuntimeId] = useState<string>();
const activeAgentCommands = useRef(new Set<string>());
const [operations, setOperations] = useState<Record<number, LocalOperation>>(
{},
);
const [abortingRuntimeIds, setAbortingRuntimeIds] = useState<Set<string>>(
() => new Set(),
);
const [restoreTabFocus, setRestoreTabFocus] = useState(false);
const setRuntimeStatus = (runtimeId: string, detail: string) =>
setRuntimeStatuses((current) => ({ ...current, [runtimeId]: detail }));
const clearRuntimeStatus = (runtimeId: string) =>
setRuntimeStatuses((current) => {
if (!(runtimeId in current)) return current;
const next = { ...current };
delete next[runtimeId];
return next;
});
const startOperation = (
kind: LocalOperation["kind"],
detail: string,
runtimeId?: string,
command?: string,
) => {
const id = ++operationSequence.current;
setOperation({ id, kind, detail, runtimeId });
setStatus(detail);
return id;
const operation = {
id: ++operationSequence.current,
kind,
detail,
runtimeId,
command,
};
setOperations((current) => ({ ...current, [operation.id]: operation }));
if (runtimeId) clearRuntimeStatus(runtimeId);
else setStatus(detail);
return operation.id;
};
const finishOperation = (id: number) =>
setOperation((current) => (current?.id === id ? undefined : current));
setOperations((current) => {
if (!(id in current)) return current;
const next = { ...current };
delete next[id];
return next;
});
useEffect(() => {
mountedRef.current = true;
@@ -89,34 +157,29 @@ export default function App() {
};
}, []);
const selectedRuntimeId = selected?.summary.runtimeId;
useEffect(() => {
if (!selected || operation) return;
setStatus(
selected.summary.state === "streaming"
? "Pi working"
: (selected.error ?? "Pi ready"),
);
if (previousSelected.current !== selected.summary.runtimeId)
previousSelected.current = selected.summary.runtimeId;
if (!selectedRuntimeId) return;
requestAnimationFrame(() =>
dispatch({ type: "markRead", runtimeId: selected.summary.runtimeId }),
dispatch({ type: "markRead", runtimeId: selectedRuntimeId }),
);
}, [selected, dispatch, operation]);
}, [selectedRuntimeId, dispatch]);
useEffect(() => {
if (
abortingRuntimeId &&
state.runtimesById[abortingRuntimeId]?.summary.state !== "streaming"
)
setAbortingRuntimeId(undefined);
}, [abortingRuntimeId, state.runtimesById]);
setAbortingRuntimeIds((current) => {
const next = new Set(
[...current].filter(
(runtimeId) =>
state.runtimesById[runtimeId]?.summary.state === "streaming",
),
);
return next.size === current.size ? current : next;
});
}, [state.runtimesById]);
async function createRuntime(path = state.selectedDirectoryPath) {
if (!path || !mountedRef.current) return;
const operationId = startOperation(
"creating",
"Starting a new Pi session…",
);
const operationId = startOperation("creating", "Starting a new Pi session…");
try {
const result = await invoke<{ runtime: RuntimeView["summary"] }>(
"create_session_runtime",
@@ -157,7 +220,10 @@ export default function App() {
if (mountedRef.current) setRestoreTabFocus(true);
} catch (error) {
if (mountedRef.current)
setStatus(`Could not close session: ${String(error)}`);
setRuntimeStatus(
runtime.summary.runtimeId,
`Could not close session: ${String(error)}`,
);
} finally {
if (mountedRef.current) finishOperation(operationId);
}
@@ -237,20 +303,50 @@ export default function App() {
const runtimeId = target?.summary.runtimeId,
agentId = target?.summary.agentId;
if (!runtimeId || !agentId) {
setStatus("This session is not currently available");
if (runtimeId)
setRuntimeStatus(runtimeId, "This session is not currently available");
else setStatus("This session is not currently available");
return false;
}
if (activeAgentCommands.current.has(runtimeId)) return false;
const operationCommand = requestedCommand(command, args);
const restarting = operationCommand === "restart";
const renewing = operationCommand === "renew_session";
activeAgentCommands.current.add(runtimeId);
setCommandNotices((current) => {
if (!(runtimeId in current)) return current;
const next = { ...current };
delete next[runtimeId];
return next;
});
const operationId = showOperation
? startOperation("command", "Updating Pi session…", runtimeId)
? startOperation(
renewing ? "renewing" : restarting ? "restarting" : "command",
commandProgress(operationCommand),
runtimeId,
operationCommand,
)
: undefined;
try {
await invoke(command, { agentId, ...args });
if (reload) await loadSnapshot(runtimeId);
if (reload) await loadSnapshot(runtimeId, { throwOnError: true });
return true;
} catch (error) {
setStatus(`Pi command failed: ${String(error)}`);
const subject = modelRpcCommands.has(operationCommand)
? "Pi model command failed"
: renewing
? "Session renewal failed"
: restarting
? "Pi restart failed"
: "Pi command failed";
const message = `${subject}: ${String(error)}`;
setCommandNotices((current) => ({
...current,
[runtimeId]: { kind: "error", message, runtimeId },
}));
return false;
} finally {
activeAgentCommands.current.delete(runtimeId);
if (mountedRef.current && operationId) finishOperation(operationId);
}
}
@@ -279,7 +375,10 @@ export default function App() {
const name = text.match(/^\/name\s+([\s\S]+)$/)?.[1]?.trim();
if (text === "/name" || name) {
if (!name) {
setStatus("Enter a session name after /name");
setRuntimeStatus(
selected.summary.runtimeId,
"Enter a session name after /name",
);
return;
}
await agentCommand("set_session_name", { name });
@@ -334,13 +433,9 @@ export default function App() {
);
const delivery = result.delivery ?? initialDelivery;
dispatch({ type: "submissionSent", runtimeId, id, delivery });
setStatus(
delivery === "follow_up"
? "Follow-up queued · waiting for Pi…"
: "Prompt accepted · waiting for Pi…",
);
clearRuntimeStatus(runtimeId);
} catch (error) {
setStatus(`Pi command failed: ${String(error)}`);
setRuntimeStatus(runtimeId, `Pi command failed: ${String(error)}`);
dispatch({ type: "submissionRemoved", runtimeId, id });
dispatch({ type: "draftChanged", runtimeId, draft: text });
} finally {
@@ -350,22 +445,39 @@ export default function App() {
async function abortSelected() {
const target = selected;
if (!target?.summary.agentId) return;
setAbortingRuntimeId(target.summary.runtimeId);
setStatus("Aborting Pi…");
const runtimeId = target.summary.runtimeId;
setAbortingRuntimeIds((current) => new Set(current).add(runtimeId));
clearRuntimeStatus(runtimeId);
try {
await invoke("abort", { agentId: target.summary.agentId });
} catch (error) {
setStatus(`Pi command failed: ${String(error)}`);
setAbortingRuntimeId(undefined);
setRuntimeStatus(runtimeId, `Pi command failed: ${String(error)}`);
setAbortingRuntimeIds((current) => {
const next = new Set(current);
next.delete(runtimeId);
return next;
});
}
}
async function runRpcCommand(
command: RpcCommand,
input: Record<string, unknown> = {},
target = selected,
) {
if (!selected) return;
await agentCommand("pi_rpc_command", { command: command.command, input });
setCommandForm(undefined);
if (!target) return;
const runtimeId = target.summary.runtimeId;
const succeeded = await agentCommand(
"pi_rpc_command",
{ command: command.command, input },
true,
target,
);
if (succeeded)
setCommandForms((current) => {
const next = { ...current };
delete next[runtimeId];
return next;
});
}
async function respond(response: Record<string, unknown>) {
const target = selected;
@@ -379,11 +491,54 @@ export default function App() {
);
}
const closePicker = useCallback(() => setSessions(undefined), []);
const activeOperations = Object.values(operations);
const globalOperation = activeOperations
.filter((entry) => !entry.runtimeId)
.reduce<LocalOperation | undefined>(
(latest, entry) => (!latest || entry.id > latest.id ? entry : latest),
undefined,
);
const selectedOperations = selectedRuntimeId
? activeOperations.filter((entry) => entry.runtimeId === selectedRuntimeId)
: [];
const selectedOperation = selectedOperations.reduce<
LocalOperation | undefined
>(
(latest, entry) => (!latest || entry.id > latest.id ? entry : latest),
undefined,
);
const pendingSubmissions = selected?.pendingSubmissions ?? [];
const pendingSubmission = pendingSubmissions[pendingSubmissions.length - 1];
const isSubmitting =
pendingSubmissions.some((entry) => entry.phase === "sending") ||
selectedOperations.some((entry) => entry.kind === "submitting");
const isRestarting = selectedOperations.some(
(entry) => entry.kind === "restarting",
);
const isRenewing = selectedOperations.some(
(entry) => entry.kind === "renewing",
);
const isAborting = selectedRuntimeId
? abortingRuntimeIds.has(selectedRuntimeId)
: false;
const isSessionLocked = isRestarting || isRenewing;
const visibleCommandForm = selectedRuntimeId
? commandForms[selectedRuntimeId]
: undefined;
const commandOperation = selectedOperations.find(
(entry) => entry.kind === "command",
);
const commandFormPending = Boolean(commandOperation);
const modelCommandPending =
commandFormPending && modelRpcCommands.has(commandOperation?.command ?? "");
const visibleCommandNotice = selectedRuntimeId
? commandNotices[selectedRuntimeId]
: undefined;
const displayedStatus =
operation?.detail ??
state.newSessionLaunch?.detail ??
globalOperation?.detail ??
(isAborting ? "Aborting Pi…" : undefined) ??
selectedOperation?.detail ??
(pendingSubmission
? pendingSubmission.phase === "sending"
? pendingSubmission.delivery === "follow_up"
@@ -393,33 +548,54 @@ export default function App() {
? "Follow-up queued · waiting for Pi…"
: "Prompt accepted · waiting for Pi…"
: undefined) ??
(selectedRuntimeId ? runtimeStatuses[selectedRuntimeId] : undefined) ??
(selected?.progress.phase === "working" ||
selected?.progress.phase === "recovering"
? selected.progress.detail
: undefined) ??
selected?.error ??
selected?.summary.error?.message ??
selected?.progress.detail ??
status;
const isWorking =
state.newSessionLaunch?.phase === "starting" ||
isSessionLocked ||
isSubmitting ||
selected?.progress.phase === "working" ||
selected?.progress.phase === "recovering";
const isStartingSession =
state.newSessionLaunch?.phase === "starting" ||
operation?.kind === "creating";
activeOperations.some((entry) => entry.kind === "creating");
const sessionTabsBusy = activeOperations.some(
(entry) =>
!entry.runtimeId &&
(entry.kind === "creating" ||
entry.kind === "opening" ||
entry.kind === "listing"),
);
const closingRuntimeIds = new Set(
activeOperations
.filter((entry) => entry.kind === "closing" && entry.runtimeId)
.map((entry) => entry.runtimeId as string),
);
const renewingRuntimeIds = new Set(
activeOperations
.filter((entry) => entry.kind === "renewing" && entry.runtimeId)
.map((entry) => entry.runtimeId as string),
);
const sameDirectoryCollision = directory && directory.openCount > 1;
return (
<main
className="app-shell"
style={{ zoom: state.interfaceScale }}
aria-busy={isStartingSession}
aria-busy={isStartingSession || isSessionLocked}
>
<header
className="workflow-header"
onMouseDown={(event) => {
if (
event.button === 0 &&
!(event.target as HTMLElement).closest(
"button,input,select,textarea",
)
!(event.target as HTMLElement).closest("button,input,select,textarea")
)
void getCurrentWindow().startDragging();
}}
@@ -429,15 +605,19 @@ export default function App() {
<span className="brand">PI</span>
<h1>{selected?.summary.label ?? "Pi workspace"}</h1>
{selected && (
<span className={`state-pill ${selected.progress.phase}`}>
{isWorking && (
<span className="working-pulse" aria-hidden="true" />
)}
<span
className={`state-pill ${isWorking ? "working" : selected.progress.phase}`}
>
{isWorking && <span className="working-pulse" aria-hidden="true" />}
{state.newSessionLaunch?.phase === "starting"
? "Starting"
: isWorking
? "Working"
: selected.summary.state}
: isRenewing
? "Renewing"
: isRestarting
? "Restarting"
: isWorking
? "Working"
: selected.summary.state}
</span>
)}
</div>
@@ -447,7 +627,11 @@ export default function App() {
<span className="working-pulse" aria-hidden="true" />{" "}
{state.newSessionLaunch?.phase === "starting"
? "Starting new session"
: "Pi is working"}
: isRenewing
? "Renewing session"
: isRestarting
? "Restarting Pi"
: "Pi is working"}
</span>
)}
<span role="status" aria-live="polite">
@@ -478,8 +662,8 @@ export default function App() {
{sameDirectoryCollision && !state.dismissedCollisionWarning && (
<div className="workspace-warning collision" role="alert">
<span>
Multiple sessions are open in the same directory. Concurrent work
can collide in files and Git; separate worktrees are safer.
Multiple sessions are open in the same directory. Concurrent work can
collide in files and Git; separate worktrees are safer.
</span>
<button
type="button"
@@ -490,6 +674,30 @@ export default function App() {
</button>
</div>
)}
{visibleCommandNotice && !visibleCommandForm && (
<div className="command-error" role="alert">
<span>{visibleCommandNotice.message}</span>
<button
type="button"
className="quiet warning-dismiss"
onClick={() => {
if (!selectedRuntimeId) return;
setCommandNotices((current) => {
const next = { ...current };
delete next[selectedRuntimeId];
return next;
});
}}
>
Dismiss
</button>
</div>
)}
{selected?.modelError && (
<div className="command-error" role="alert">
<span>{selected.modelError}</span>
</div>
)}
{selected?.notifications.map((notice, index) => (
<div
className={`notification ${notice.notifyType ?? "info"}`}
@@ -533,11 +741,7 @@ export default function App() {
Browse…
</button>
<button type="submit">Add</button>
<button
type="button"
className="quiet"
onClick={() => setAdding(false)}
>
<button type="button" className="quiet" onClick={() => setAdding(false)}>
Cancel
</button>
</form>
@@ -545,23 +749,21 @@ export default function App() {
<SessionTabs
runtimes={runtimes}
selectedId={state.selectedRuntimeId}
onSelect={(runtimeId) =>
dispatch({ type: "selectRuntime", runtimeId })
}
onSelect={(runtimeId) => dispatch({ type: "selectRuntime", runtimeId })}
onClose={(runtime) => void closeRuntime(runtime)}
onRenew={(runtime) =>
void agentCommand("renew_session", {}, true, runtime)
}
onNew={() => void createRuntime()}
onResume={() => void showSessions()}
busy={
operation?.kind === "creating" || operation?.kind === "listing"
}
closingRuntimeId={
operation?.kind === "closing" ? operation.runtimeId : undefined
}
busy={sessionTabsBusy}
closingRuntimeIds={closingRuntimeIds}
renewingRuntimeIds={renewingRuntimeIds}
restoreFocus={restoreTabFocus}
onFocusRestored={() => setRestoreTabFocus(false)}
/>
{view === "settings" ? (
<section className="settings">
<section className="settings" aria-busy={isSessionLocked}>
<h2>Session controls</h2>
<label className="default-worktree-control">
Default new-session directory
@@ -582,8 +784,7 @@ export default function App() {
))}
</select>
<small>
Used by Ctrl+Super+Space. Only currently open directories are
listed.
Used by Ctrl+Super+Space. Only currently open directories are listed.
</small>
</label>
{selected ? (
@@ -594,16 +795,29 @@ export default function App() {
{selected.summary.queueCount} queued
</p>
<div className="settings-actions">
<button onClick={() => void agentCommand("retry")}>
<button
disabled={isSessionLocked}
onClick={() => void agentCommand("retry")}
>
Retry
</button>
<button
className="danger"
disabled={isSessionLocked}
onClick={() => void agentCommand("restart")}
>
Restart Pi
{isRestarting ? "Restarting Pi…" : "Restart Pi"}
</button>
</div>
{isRestarting && (
<div className="restart-progress" role="status" aria-live="polite">
<span className="working-pulse" aria-hidden="true" />
<span>
<strong>Restarting Pi</strong>
<small>Restoring this session and its extensions…</small>
</span>
</div>
)}
<label>
Interface scale{" "}
<input
@@ -653,35 +867,32 @@ export default function App() {
);
}}
onModel={(model: Model) =>
void agentCommand("set_model", {
agentCommand("set_model", {
provider: model.provider,
modelId: model.id,
})
}
onThinking={(level) =>
void agentCommand("set_thinking_level", { level })
}
isSubmitting={
operation?.kind === "submitting" &&
operation.runtimeId === selected.summary.runtimeId
}
isAborting={abortingRuntimeId === selected.summary.runtimeId}
onCommand={(command) =>
command.input?.length
? setCommandForm(command)
: void runRpcCommand(command)
}
onThinking={(level) => agentCommand("set_thinking_level", { level })}
isModelUpdating={modelCommandPending}
locked={isSessionLocked}
lockReason={isRenewing ? "renewing" : "restarting"}
isSubmitting={isSubmitting}
isAborting={isAborting}
onCommand={(command) => {
if (command.input?.length)
setCommandForms((current) => ({
...current,
[selected.summary.runtimeId]: command,
}));
else void runRpcCommand(command, {}, selected);
}}
/>
) : (
<section className="empty-workspace">
<h2>No open sessions</h2>
<p>
Start a session in this directory, or add another directory.
</p>
<p>Start a session in this directory, or add another directory.</p>
{state.selectedDirectoryPath && (
<button onClick={() => void createRuntime()}>
New session
</button>
<button onClick={() => void createRuntime()}>New session</button>
)}
</section>
)}
@@ -695,10 +906,20 @@ export default function App() {
/>
)}
<CommandFormDialog
command={commandForm}
onCancel={() => setCommandForm(undefined)}
command={visibleCommandForm}
pending={commandFormPending}
commandError={visibleCommandNotice?.message}
onCancel={() => {
if (!selectedRuntimeId) return;
setCommandForms((current) => {
const next = { ...current };
delete next[selectedRuntimeId];
return next;
});
}}
onSubmit={(input) =>
commandForm && void runRpcCommand(commandForm, input)
visibleCommandForm &&
void runRpcCommand(visibleCommandForm, input, selected)
}
/>
<ExtensionDialog
@@ -722,13 +943,8 @@ export default function App() {
</span>
<div>
<strong id="startup-blocker-title">Starting new session</strong>
<p>
{state.newSessionLaunch?.detail ??
"Creating a fresh Pi runtime…"}
</p>
<small>
Pi is loading its session and extensions. Please wait.
</small>
<p>{state.newSessionLaunch?.detail ?? "Creating a fresh Pi runtime…"}</p>
<small>Pi is loading its session and extensions. Please wait.</small>
</div>
</div>
</div>