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:
+330
-114
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user