feat(vscode): startup parity + workspace-grouped session list (#1658)

* perf(vscode): gate API readiness and coalesce duplicate startup reads

Bring the VS Code bridge runtime to parity with the web startup
optimizations (PR #1650), which were web/desktop-only.

waitForApiUrl now hands out the OpenCode API URL only once the manager
reports 'connected', instead of as soon as getApiUrl() exposes
server.url. The URL is available the moment the process is spawned —
before waitForReady confirms it can serve and during a workspace-switch
restart (stale port) — so URL-presence alone let the bridge forward to a
not-yet-ready OpenCode and surface 502s. Gating on connected status
mirrors the web proxy's isOpenCodeReady hold. Also fail fast on 'error'
status so a missing CLI doesn't burn the full 30s timeout.

Coalesce concurrent identical GET reads (config/path/agents/agent/
project/command) at the bridge proxy so the single OpenCode process
serves them once. On cold start the webview's sync bootstrap and config
store fire these reads in parallel with no shared dedup; this is the
extension-host analog of the runtimeFetch coalescer. Shared reads carry
no AbortController so one caller's abort can't strand the others, and the
entry clears as soon as it settles (never serves stale).

* perf(vscode): fade the startup splash once mounted + connected, not on live fetch

The webview's initial-loading overlay held until a successful live
/api/config/providers AND /api/agent fetch completed. After the cache
hydration work those live reads are the slowest cold-start tail — the UI
underneath already paints pickers and the sidebar from cache and refreshes
in the background — so gating the splash on them kept it spinning long
after the app was usable.

Fade the overlay as soon as the UI is mounted and OpenCode is connected.
Per-widget loaders convey any remaining background refresh, matching how
web/desktop (which have no such splash) already behave. Removes the now
-obsolete bootstrapProvidersReady/AgentsReady/Failed tracking and
recordBootstrapFetch. Connection error/disconnected splash messages are
unchanged.

* fix(vscode): include captured OpenCode output in spawn-timeout error

When the managed OpenCode server fails to emit its 'listening' line within
the start timeout, the error discarded everything the process printed to
stdout/stderr — so the status report showed a bare 'Timeout waiting for
server to start' with no clue whether the process hung, crashed silently,
or printed a config/auth error. The exit path already includes the output;
the timeout path now does too (or notes that nothing was printed).

* feat(vscode): workspace-grouped session list with working folders, pinning, and archived toggle

Replace the flat multi-workspace session list with the grouped project view,
using each open VS Code workspace folder as a header (no per-worktree
subgroups). This restores native folder and pin support, which the flat list
silently dropped, and fixes the clipped left padding on session rows.

- Group sessions strictly by open workspace; funnel all non-archived sessions
  into the workspace's group so they no longer fall into the archived bucket.
- Keep the project/group/folder + buttons but make them open a draft in the
  correct workspace and navigate to chat; hide the project actions (...) menu,
  which isn't relevant in VS Code.
- Force the minimal single-line row layout (the second metadata row is
  redundant under workspace headers) and drop the per-row tooltip.
- Add a show/hide archived toggle next to the archive-all control, since the
  VS Code header has no display-mode menu.
- Size the hover action reveal so the timestamp clears the row buttons.
This commit is contained in:
Bohdan Triapitsyn
2026-06-15 13:02:26 +03:00
committed by GitHub
parent 8919d33636
commit c73ab9cbd5
14 changed files with 364 additions and 135 deletions
@@ -60,7 +60,6 @@ import { useSessionDisplayStore } from '@/stores/useSessionDisplayStore';
import { type SessionGroup, type SessionNode } from './sidebar/types';
import {
deriveRecentSessions,
getSessionUpdatedAtMs,
} from './sidebar/activitySections';
import { useSessionPinnedStore } from '@/stores/useSessionPinnedStore';
import {
@@ -83,8 +82,6 @@ const PROJECT_COLLAPSE_STORAGE_KEY = 'oc.sessions.projectCollapse';
const GROUP_ORDER_STORAGE_KEY = 'oc.sessions.groupOrder';
const GROUP_COLLAPSE_STORAGE_KEY = 'oc.sessions.groupCollapse';
const PROJECT_ACTIVE_SESSION_STORAGE_KEY = 'oc.sessions.activeSessionByProject';
const VSCODE_RECENT_INITIAL_SESSION_COUNT = 20;
const VSCODE_RECENT_SESSION_BATCH_SIZE = 20;
// v2 key holds composite "${renderContext}:${active|archived}:${sessionId}"
// entries so the same session in different render contexts (e.g. "Recent"
// and a project's root) has independent expand state. v1 held bare session
@@ -325,6 +322,16 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
const worktreeMetadata = useSessionUIStore((state) => state.worktreeMetadata);
const availableWorktreesByProject = useSessionUIStore((state) => state.availableWorktreesByProject);
const openNewSessionDraft = useSessionUIStore((state) => state.openNewSessionDraft);
// The sidebar tree's +-buttons (project / group / folder) open a draft but,
// unlike selecting an existing session, don't navigate. VS Code's compact view
// is driven by the openchamber:navigate event, so switch to chat explicitly
// (a no-op in the expanded side-by-side layout, which is always showing chat).
const openNewSessionDraftFromTree = React.useCallback<typeof openNewSessionDraft>((options) => {
openNewSessionDraft(options);
if (isVSCode) {
window.dispatchEvent(new CustomEvent('openchamber:navigate', { detail: { view: 'chat' } }));
}
}, [isVSCode, openNewSessionDraft]);
const updateStore = useUpdateStore(useShallow((s) => ({
checkForUpdates: s.checkForUpdates,
available: s.available,
@@ -1024,29 +1031,17 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
.sort((a, b) => compareSessionsByPinnedAndTime(a, b, pinnedSessionIds));
}, [isVSCode, pinnedSessionIds, sessions, showRecentSection]);
const vscodeSharedSessions = React.useMemo(() => {
if (!isVSCode) {
return [];
}
return sessions
.filter((session) => !session.time?.archived)
.filter((session) => !(session as Session & { parentID?: string | null }).parentID)
.sort((left, right) => {
const timeDelta = getSessionUpdatedAtMs(right) - getSessionUpdatedAtMs(left);
if (timeDelta !== 0) return timeDelta;
return right.id.localeCompare(left.id);
});
}, [isVSCode, sessions]);
// Prefetch is wired below, after recentSessionIds is computed.
const activitySections = React.useMemo(() => {
if (!isVSCode && !showRecentSection) {
// VS Code renders the full grouped project view (one group per open
// workspace, folders + pinned native); the flat "recent" activity list is
// web/desktop-only.
if (isVSCode || !showRecentSection) {
return [];
}
const recentSessions = isVSCode ? vscodeSharedSessions : activeNowSessions;
const recentSessions = activeNowSessions;
const toItem = (session: Session) => {
const existing = sessionSidebarMetaById.get(session.id);
@@ -1080,7 +1075,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
return [
{ key: 'active-now' as const, title: t('sessions.sidebar.activity.recentTitle'), items },
];
}, [activeNowSessions, filterSessionNodesForSearch, hasSessionSearchQuery, isVSCode, normalizedSessionSearchQuery, sessionSidebarMetaById, showRecentSection, t, vscodeSharedSessions]);
}, [activeNowSessions, filterSessionNodesForSearch, hasSessionSearchQuery, isVSCode, normalizedSessionSearchQuery, sessionSidebarMetaById, showRecentSection, t]);
const hasActivitySectionItems = React.useMemo(
() => activitySections.some((section) => section.items.length > 0),
@@ -1376,7 +1371,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
setActiveProjectIdOnly={setActiveProjectIdOnly}
setActiveMainTab={setActiveMainTab}
setSessionSwitcherOpen={setSessionSwitcherOpen}
openNewSessionDraft={openNewSessionDraft}
openNewSessionDraft={openNewSessionDraftFromTree}
addSessionToFolder={addSessionToFolder}
createFolderAndStartRename={createFolderAndStartRename}
renamingFolderId={renamingFolderId}
@@ -1413,7 +1408,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
setActiveProjectIdOnly,
setActiveMainTab,
setSessionSwitcherOpen,
openNewSessionDraft,
openNewSessionDraftFromTree,
addSessionToFolder,
createFolderAndStartRename,
renamingFolderId,
@@ -1425,13 +1420,11 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
],
);
const topContent = (isVSCode || (showRecentSection && !hasSessionSearchQuery)) ? (
const topContent = (!isVSCode && showRecentSection && !hasSessionSearchQuery) ? (
<SidebarActivitySections
sections={activitySections}
renderSessionNode={renderSessionNode}
variant={isVSCode ? 'flat' : 'section'}
initialVisibleCount={isVSCode ? VSCODE_RECENT_INITIAL_SESSION_COUNT : undefined}
batchSize={isVSCode ? VSCODE_RECENT_SESSION_BATCH_SIZE : undefined}
variant="section"
/>
) : null;
const isInlineEditing = Boolean(renamingFolderId || editingId || editingProjectDialogId);
@@ -1648,7 +1641,6 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
<SidebarProjectsList
topContent={topContent}
sharedSessionsOnly={isVSCode}
hasSharedSessions={hasActivitySectionItems}
sectionsForRender={sectionsForSidebarRender}
projectSections={projectSections}
@@ -1670,7 +1662,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
setActiveProjectIdOnly={setActiveProjectIdOnly}
setActiveMainTab={setActiveMainTab}
setSessionSwitcherOpen={setSessionSwitcherOpen}
openNewSessionDraft={openNewSessionDraft}
openNewSessionDraft={openNewSessionDraftFromTree}
openNewWorktreeDialog={openNewWorktreeDialog}
openProjectEditDialog={setEditingProjectDialogId}
removeProject={removeProject}