diff --git a/packages/app/src/app/app.tsx b/packages/app/src/app/app.tsx index 25710534..b2edf780 100644 --- a/packages/app/src/app/app.tsx +++ b/packages/app/src/app/app.tsx @@ -891,9 +891,6 @@ export default function App() { }), ); - await loadSessionsWithReady(workspaceStore.activeWorkspaceRoot().trim()).catch( - () => undefined - ); } else { const result = await c.session.promptAsync({ sessionID, @@ -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); @@ -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.) @@ -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(() => { const workspaces = workspaceStore.workspaces(); const sessionsById = sidebarSessionsByWorkspaceId(); @@ -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); diff --git a/packages/app/src/app/pages/session.tsx b/packages/app/src/app/pages/session.tsx index 913e3416..86b4f7fa 100644 --- a/packages/app/src/app/pages/session.tsx +++ b/packages/app/src/app/pages/session.tsx @@ -114,7 +114,7 @@ export type SessionViewProps = { updateEnv: { supported?: boolean; reason?: string | null } | null; anyActiveRuns: boolean; installUpdateAndRestart: () => void; - createSessionAndOpen: () => void; + createSessionAndOpen: () => Promise; sendPromptAsync: (draft: ComposerDraft) => Promise; abortSession: (sessionId?: string) => Promise; sessionRevertMessageId: string | null; @@ -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); }; diff --git a/packages/desktop/scripts/prepare-sidecar.mjs b/packages/desktop/scripts/prepare-sidecar.mjs index 96cfa3cb..ab6eb541 100644 --- a/packages/desktop/scripts/prepare-sidecar.mjs +++ b/packages/desktop/scripts/prepare-sidecar.mjs @@ -325,6 +325,7 @@ if (shouldBuildOpenworkServer) { const buildResult = spawnSync("bun", openworkServerArgs, { cwd: openworkServerDir, stdio: "inherit", + shell: true, }); if (buildResult.status !== 0) { @@ -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); } @@ -604,6 +605,7 @@ if (shouldBuildOpenwrk) { const result = spawnSync("bun", openwrkArgs, { cwd: openwrkDir, stdio: "inherit", + shell: true, env: { ...process.env, NODE_ENV: "production", @@ -674,6 +676,7 @@ if (shouldBuildChromeDevtools) { const result = spawnSync("bun", chromeDevtoolsArgs, { cwd: __dirname, stdio: "inherit", + shell: true, env: { ...process.env, NODE_ENV: "production",