Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion desktop/electron/main.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,28 @@ ipcMain.handle("nav:session-stacks", async (_event, requestedSessionId) => {
method: "session.stacks",
params: { sessionId: targetSessionId },
});
return response.result.stacks;
return response.result;
});

ipcMain.handle(
"nav:session-stack-availability",
async (_event, requestedSessionId) => {
if (!backendUrl) {
throw new Error("chat session is not ready");
}
const targetSessionId = requestedSessionId || sessionId;
if (!targetSessionId) {
throw new Error("chat session is not ready");
}
const response = await sendRpc({
backendUrl,
method: "session.stackAvailability",
params: { sessionId: targetSessionId },
});
return response.result;
},
);

ipcMain.handle("nav:switch-session", async (_event, id) => {
if (!backendUrl) {
throw new Error("chat session is not ready");
Expand Down
7 changes: 7 additions & 0 deletions desktop/electron/preload.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,13 @@ contextBridge.exposeInMainWorld("nav", {
sessionId == null ? undefined : normalizeSessionId(sessionId),
);
},
// Advisory scan for whether stack records are currently retained.
sessionStackAvailability(sessionId) {
return ipcRenderer.invoke(
"nav:session-stack-availability",
sessionId == null ? undefined : normalizeSessionId(sessionId),
);
},
// Switch the active conversation to an existing session.
switchSession(sessionId) {
return ipcRenderer.invoke(
Expand Down
98 changes: 86 additions & 12 deletions desktop/electron/renderer/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export default function App() {
const [modelInfo, setModelInfo] = useState(null);
const [stopPending, setStopPending] = useState(false);
const [activeView, setActiveView] = useState("chat");
const [stackAvailable, setStackAvailable] = useState(false);
const [stackRefreshKey, setStackRefreshKey] = useState(0);
const [newSessionMode, setNewSessionMode] = useState("local");

Expand All @@ -24,6 +25,7 @@ export default function App() {
const stopSentForActiveRunRef = useRef(false);
const stopRequestedRef = useRef(false);
const nextMessageIdRef = useRef(0);
const stackAvailabilityRequestRef = useRef(0);

const setConnectedState = useCallback((isConnected) => {
connectedRef.current = isConnected;
Expand Down Expand Up @@ -69,6 +71,10 @@ export default function App() {
window.setTimeout(refreshStacks, 120);
}, [refreshStacks]);

const markStacksUnavailable = useCallback(() => {
setStackAvailable(false);
}, []);

const appendMessage = useCallback(
(role, text) => {
setMessages((current) => [
Expand Down Expand Up @@ -146,6 +152,42 @@ export default function App() {
}
}, []);

const refreshStackAvailability = useCallback(
async (sessionId, options = {}) => {
if (!window.nav) {
return;
}
const targetSessionId = sessionId ?? activeSessionIdRef.current;
if (!targetSessionId) {
setStackAvailable(false);
return;
}
const requestId = stackAvailabilityRequestRef.current + 1;
stackAvailabilityRequestRef.current = requestId;
if (options.reset) {
setStackAvailable(false);
}
try {
const availability =
await window.nav.sessionStackAvailability(targetSessionId);
if (
requestId === stackAvailabilityRequestRef.current &&
targetSessionId === activeSessionIdRef.current
) {
setStackAvailable(availability?.available === true);
}
} catch {
if (
requestId === stackAvailabilityRequestRef.current &&
targetSessionId === activeSessionIdRef.current
) {
setStackAvailable(false);
}
}
},
[],
);

const renderBackendStatus = useCallback(
(status) => {
switch (status.state) {
Expand Down Expand Up @@ -215,10 +257,12 @@ export default function App() {
setRunningState(false);
refreshSessions();
refreshModelInfo(status.sessionId);
refreshStackAvailability(status.sessionId, { reset: true });
},
[
refreshModelInfo,
refreshSessions,
refreshStackAvailability,
renderBackendStatus,
setActiveSession,
setConnectedState,
Expand Down Expand Up @@ -277,6 +321,7 @@ export default function App() {
setStopRequested(false);
refreshSessions();
refreshModelInfo();
refreshStackAvailability();
refreshStacksAfterTerminalEvent();
break;
case "run.failed":
Expand All @@ -286,6 +331,7 @@ export default function App() {
setStopRequested(false);
refreshSessions();
refreshModelInfo();
refreshStackAvailability();
refreshStacksAfterTerminalEvent();
break;
default:
Expand All @@ -296,6 +342,7 @@ export default function App() {
appendMessage,
refreshModelInfo,
refreshSessions,
refreshStackAvailability,
refreshStacks,
refreshStacksAfterTerminalEvent,
setRunningState,
Expand All @@ -310,10 +357,17 @@ export default function App() {
const activateCreatedSession = useCallback(
async (sessionId) => {
setActiveSession(sessionId);
setStackAvailable(false);
await refreshSessions();
refreshModelInfo(sessionId);
refreshStackAvailability(sessionId, { reset: true });
},
[refreshModelInfo, refreshSessions, setActiveSession],
[
refreshModelInfo,
refreshSessions,
refreshStackAvailability,
setActiveSession,
],
);

const selectSession = useCallback(
Expand All @@ -328,16 +382,26 @@ export default function App() {

const previousSessionId = activeSessionIdRef.current;
clearTranscript();
setActiveView("chat");
setStackAvailable(false);
setActiveSession(sessionId);
try {
await window.nav.switchSession(sessionId);
refreshModelInfo(sessionId);
refreshStackAvailability(sessionId, { reset: true });
} catch (error) {
setActiveSession(previousSessionId);
refreshStackAvailability(previousSessionId, { reset: true });
appendMessage("error", `Could not open session: ${error.message}`);
}
},
[appendMessage, clearTranscript, refreshModelInfo, setActiveSession],
[
appendMessage,
clearTranscript,
refreshModelInfo,
refreshStackAvailability,
setActiveSession,
],
);

const startNewChatInProject = useCallback(
Expand Down Expand Up @@ -427,11 +491,13 @@ export default function App() {
activeView={activeView}
connected={connected}
sessionId={activeSessionId}
showStacks={stackAvailable}
onSelectView={setActiveView}
/>
{activeView === "stacks" ? (
<StacksPage
key={`${activeSessionId ?? "none"}-${stackRefreshKey}`}
onUnavailable={markStacksUnavailable}
sessionId={activeSessionId}
/>
) : (
Expand All @@ -454,7 +520,13 @@ export default function App() {
);
}

function SessionToolbar({ activeView, connected, sessionId, onSelectView }) {
function SessionToolbar({
activeView,
connected,
sessionId,
showStacks,
onSelectView,
}) {
return (
<header className="session-toolbar">
<div className="session-toolbar-title">
Expand All @@ -472,15 +544,17 @@ function SessionToolbar({ activeView, connected, sessionId, onSelectView }) {
>
Chat
</button>
<button
type="button"
className="session-view-tab"
aria-current={activeView === "stacks" ? "page" : undefined}
disabled={!connected || !sessionId}
onClick={() => onSelectView("stacks")}
>
Stacks
</button>
{showStacks ? (
<button
type="button"
className="session-view-tab"
aria-current={activeView === "stacks" ? "page" : undefined}
disabled={!connected || !sessionId}
onClick={() => onSelectView("stacks")}
>
Stacks
</button>
) : null}
</nav>
</header>
);
Expand Down
31 changes: 26 additions & 5 deletions desktop/electron/renderer/src/components/StacksPage.jsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,32 @@
import { useEffect, useMemo, useState } from "react";

export default function StacksPage({ sessionId }) {
export default function StacksPage({ onUnavailable, sessionId }) {
const [stacks, setStacks] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [unavailableReason, setUnavailableReason] = useState(null);

useEffect(() => {
let alive = true;
if (!sessionId || !window.nav) {
setStacks([]);
setUnavailableReason(null);
return undefined;
}

setLoading(true);
setError(null);
setUnavailableReason(null);
window.nav
.sessionStacks(sessionId)
.then((nextStacks) => {
.then((result) => {
if (alive) {
setStacks(Array.isArray(nextStacks) ? nextStacks : []);
const nextStacks = Array.isArray(result?.stacks) ? result.stacks : [];
setStacks(nextStacks);
setUnavailableReason(result?.unavailableReason ?? null);
if (nextStacks.length === 0 && result?.unavailableReason) {
onUnavailable?.(result.unavailableReason);
}
}
})
.catch((fetchError) => {
Expand All @@ -35,7 +43,7 @@ export default function StacksPage({ sessionId }) {
return () => {
alive = false;
};
}, [sessionId]);
}, [onUnavailable, sessionId]);

const orderedStacks = useMemo(
() => [...stacks].sort((left, right) => left.sequence - right.sequence),
Expand Down Expand Up @@ -74,7 +82,7 @@ export default function StacksPage({ sessionId }) {
</div>

{orderedStacks.length === 0 ? (
<EmptyStacks text="No model calls captured for this live session yet" />
<EmptyStacks text={emptyStackText(unavailableReason)} />
) : (
<ol className="stack-call-list">
{orderedStacks.map((stack) => (
Expand Down Expand Up @@ -154,6 +162,19 @@ function EmptyStacks({ text }) {
return <div className="stacks-empty">{text}</div>;
}

function emptyStackText(reason) {
switch (reason) {
case "trimmed_or_missing":
return "Stack records for this session were no longer available. The stack log is capped at 800MB, so older records may have been trimmed.";
case "stack_store_unavailable":
return "Stack storage is unavailable for this backend run.";
case "stack_store_error":
return "Stack records could not be read from the local stack log.";
default:
return "No model calls captured for this live session yet";
}
}

function defaultOpen(layer) {
return [
"system_prompt",
Expand Down
12 changes: 8 additions & 4 deletions desktop/electron/renderer/window.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ declare global {
thinking?: string | null;
tokenUsage?: { used: number; contextWindow: number } | null;
}>;
sessionStacks(sessionId?: string): Promise<
Array<{
sessionStacks(sessionId?: string): Promise<{
stacks: Array<{
id: string;
runId: string;
sequence: number;
Expand All @@ -61,8 +61,12 @@ declare global {
text?: string;
json?: unknown;
}>;
}>
>;
}>;
unavailableReason?: string;
}>;
sessionStackAvailability(sessionId?: string): Promise<{
available: boolean;
}>;
switchSession(sessionId: string): Promise<void>;
newSession(
workspaceRoot?: string | null,
Expand Down
Loading