Skip to content
Open
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
114 changes: 73 additions & 41 deletions packages/app/src/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -891,9 +891,6 @@ export default function App() {
}),
);

await loadSessionsWithReady(workspaceStore.activeWorkspaceRoot().trim()).catch(
() => undefined
);
} else {
const result = await c.session.promptAsync({
sessionID,
Expand All @@ -915,10 +912,6 @@ export default function App() {
delete copy[sessionID];
return copy;
});

await loadSessionsWithReady(workspaceStore.activeWorkspaceRoot().trim()).catch(
() => undefined
);
}
} catch (e) {
const message = e instanceof Error ? e.message : safeStringify(e);
Expand Down Expand Up @@ -1069,8 +1062,16 @@ export default function App() {
const root = workspaceStore.activeWorkspaceRoot().trim();
const params = root ? { sessionID: trimmed, directory: root } : { sessionID: trimmed };
unwrap(await c.session.delete(params));
await loadSessions(root || undefined).catch(() => undefined);
await refreshSidebarWorkspaceSessions(workspaceStore.activeWorkspaceId()).catch(() => undefined);

// Remove the deleted session from the store and sidebar locally.
// SSE will handle any further sync — calling loadSessions/refreshSidebarWorkspaceSessions
// here races with SSE and can wipe unrelated sessions from the store.
setSessions(sessions().filter((s) => s.id !== trimmed));
const activeWsId = workspaceStore.activeWorkspaceId();
setSidebarSessionsByWorkspaceId((prev) => ({
...prev,
[activeWsId]: (prev[activeWsId] ?? []).filter((s) => s.id !== trimmed),
}));

// If we're currently routed to the deleted session, navigate away immediately.
// (Otherwise the route effect can try to re-select a session that no longer exists.)
Expand Down Expand Up @@ -1875,6 +1876,27 @@ export default function App() {
refreshSidebarWorkspaceSessions(id).catch(() => undefined);
});

createEffect(() => {
const allSessions = sessions(); // reactive dependency on session store
const wsId = workspaceStore.activeWorkspaceId();
const status = sidebarSessionStatusByWorkspaceId()[wsId];

// Only sync if sidebar is already in 'ready' state (not during initial load)
if (status === "ready") {
const sorted = sortSessionsByActivity(allSessions);
setSidebarSessionsByWorkspaceId((prev) => ({
...prev,
[wsId]: sorted.map((s) => ({
id: s.id,
title: s.title,
slug: s.slug,
time: s.time,
directory: s.directory,
})),
}));
}
});

const sidebarWorkspaceGroups = createMemo<WorkspaceSessionGroup[]>(() => {
const workspaces = workspaceStore.workspaces();
const sessionsById = sidebarSessionsByWorkspaceId();
Expand Down Expand Up @@ -3442,49 +3464,59 @@ export default function App() {
mark("raw result received");
const session = unwrap(rawResult);
mark("session unwrapped");
// Set selectedSessionId BEFORE switching view to avoid "No session selected" flash
// Immediately select and show the new session before background list refresh.
setBusyLabel("status.loading_session");
await withTimeout(
loadSessionsWithReady(workspaceStore.activeWorkspaceRoot().trim()),
12_000,
"session.list"
);
mark("sessions loaded");
await selectSession(session.id);
mark("selectSession (immediate)");
mark("session selected");

// Keep the dashboard/sidebar session lists in sync with the active workspace.
// (Sidebar sessions are fetched per-workspace and won't automatically update when
// we create a new session through the active client.)
try {
const activeWorkspaceId = workspaceStore.activeWorkspaceId().trim();
if (activeWorkspaceId) {
const list = sessions();
setSidebarSessionsByWorkspaceId((prev) => ({
...prev,
[activeWorkspaceId]: sortSessionsByActivity(list),
}));
setSidebarSessionStatusByWorkspaceId((prev) => ({
...prev,
[activeWorkspaceId]: "ready",
}));
setSidebarSessionErrorByWorkspaceId((prev) => ({
...prev,
[activeWorkspaceId]: null,
}));
}
} catch {
// ignore sidebar sync failures
// Inject the new session into the reactive sessions() store so
// the createEffect bridge (sessions → sidebar) will always include it,
// even if the background loadSessionsWithReady hasn't returned yet.
const currentStoreSessions = sessions();
if (!currentStoreSessions.some((s) => s.id === session.id)) {
setSessions([session, ...currentStoreSessions]);
}
mark("session injected into store");

const newItem: SidebarSessionItem = {
id: session.id,
title: session.title,
slug: session.slug,
time: session.time,
directory: session.directory,
};
const wsId = workspaceStore.activeWorkspaceId().trim();
if (wsId) {
const currentSessions = sidebarSessionsByWorkspaceId()[wsId] || [];
setSidebarSessionsByWorkspaceId((prev) => ({
...prev,
[wsId]: [newItem, ...currentSessions],
}));
setSidebarSessionStatusByWorkspaceId((prev) => ({
...prev,
[wsId]: "ready",
}));
}
mark("sidebar injected");

await selectSession(session.id);
mark("session selected");
// Now switch view AFTER session is selected
mark("view set to session");
// setSessionViewLockUntil(Date.now() + 1200);
goToSession(session.id);

// The new session is already in the sessions() store (injected above)
// and in the sidebar signal. SSE session.created events will handle
// any further syncing. Calling loadSessionsWithReady() here would
// race with the store injection — the server may not have indexed the
// session yet, so reconcile() would wipe it from the store, causing
// the sidebar to flash and the route guard to bounce back.
mark("done (SSE will sync)");
return session.id;
} catch (e) {
mark("error caught", e);
const message = e instanceof Error ? e.message : t("app.unknown_error", currentLocale());
setError(addOpencodeCacheHint(message));
return undefined;
} finally {
setCreatingSession(false);
setBusy(false);
Expand Down
12 changes: 8 additions & 4 deletions packages/app/src/app/pages/session.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export type SessionViewProps = {
updateEnv: { supported?: boolean; reason?: string | null } | null;
anyActiveRuns: boolean;
installUpdateAndRestart: () => void;
createSessionAndOpen: () => void;
createSessionAndOpen: () => Promise<string | undefined>;
sendPromptAsync: (draft: ComposerDraft) => Promise<void>;
abortSession: (sessionId?: string) => Promise<void>;
sessionRevertMessageId: string | null;
Expand Down Expand Up @@ -1255,9 +1255,13 @@ export default function SessionView(props: SessionViewProps) {
}
};

const applySessionAgent = (agent: string | null) => {
const sessionId = requireSessionId();
if (!sessionId) return;
const applySessionAgent = async (agent: string | null) => {
let sessionId = props.selectedSessionId;
if (!sessionId) {
// Auto-create a session when none is selected (same pattern as sendPrompt)
sessionId = await props.createSessionAndOpen();
if (!sessionId) return;
}
props.setSessionAgent(sessionId, agent);
};

Expand Down
5 changes: 4 additions & 1 deletion packages/desktop/scripts/prepare-sidecar.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,7 @@ if (shouldBuildOpenworkServer) {
const buildResult = spawnSync("bun", openworkServerArgs, {
cwd: openworkServerDir,
stdio: "inherit",
shell: true,
});

if (buildResult.status !== 0) {
Expand Down Expand Up @@ -542,7 +543,7 @@ if (shouldBuildOwpenbot) {
if (bunTarget) {
owpenbotArgs.push("--target", bunTarget);
}
const result = spawnSync("bun", owpenbotArgs, { cwd: owpenbotDir, stdio: "inherit" });
const result = spawnSync("bun", owpenbotArgs, { cwd: owpenbotDir, stdio: "inherit", shell: true });
if (result.status !== 0) {
process.exit(result.status ?? 1);
}
Expand Down Expand Up @@ -604,6 +605,7 @@ if (shouldBuildOpenwrk) {
const result = spawnSync("bun", openwrkArgs, {
cwd: openwrkDir,
stdio: "inherit",
shell: true,
env: {
...process.env,
NODE_ENV: "production",
Expand Down Expand Up @@ -674,6 +676,7 @@ if (shouldBuildChromeDevtools) {
const result = spawnSync("bun", chromeDevtoolsArgs, {
cwd: __dirname,
stdio: "inherit",
shell: true,
env: {
...process.env,
NODE_ENV: "production",
Expand Down