Files
openchamber/packages/ui/src/stores/globalSessions.ts
T
Bohdan Triapitsyn dcd68baa09 fix: cross-client settings sync and sidebar session pagination
- Paginate /experimental/session via time.updated fallback so sidebar
  loads past the 200-item first page when server omits x-next-cursor.
- VSCode extension persists all settings in shared
  ~/.config/openchamber/settings.json (not only opencodeBinary),
  matching Desktop and Web. Canonical read from disk with globalState
  fallback for eager migration of pre-existing users.
- Desktop settings sync: guard the contextBridge read-only
  __OPENCHAMBER_HOME__ assignment that was silently throwing a TypeError
  and skipping the whole applySettings chain — leaving server-synced
  values (autoDeleteAfterDays, autoDeleteEnabled, sessionRetentionAction)
  stuck at local defaults. Also wait for Zustand persist hydration before
  applying server settings to avoid overwrite races.
2026-04-22 15:43:15 +03:00

120 lines
3.8 KiB
TypeScript

import type { OpencodeClient, Session } from "@opencode-ai/sdk/v2";
import { retry } from "@/sync/retry";
export type GlobalSessionRecord = Session & {
project?: {
id: string;
name?: string;
worktree?: string;
} | null;
};
const toNumber = (value: string | null): number | null => {
if (!value) {
return null;
}
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
};
const readResponseHeader = (response: unknown, header: string): string | null => {
if (!response || typeof response !== "object") {
return null;
}
const container = response as { headers?: unknown };
const headers = container.headers;
if (!headers || typeof headers !== "object") {
return null;
}
const maybeGet = headers as { get?: (name: string) => string | null };
if (typeof maybeGet.get === "function") {
return maybeGet.get(header);
}
const maybeRecord = headers as Record<string, unknown>;
const direct = maybeRecord[header] ?? maybeRecord[header.toLowerCase()];
return typeof direct === "string" ? direct : null;
};
export const readNextCursor = (response: unknown): number | null => {
return toNumber(readResponseHeader(response, "x-next-cursor"));
};
export const isMissingGlobalSessionsEndpointError = (error: unknown): boolean => {
if (!error || typeof error !== "object") {
return false;
}
const value = error as {
status?: number;
response?: { status?: number };
cause?: { status?: number; response?: { status?: number } };
};
const status = value.status
?? value.response?.status
?? value.cause?.status
?? value.cause?.response?.status;
return status === 404;
};
export async function listGlobalSessionPages(
apiClient: OpencodeClient,
options: {
archived: boolean;
pageSize: number;
onPage?: (sessions: GlobalSessionRecord[]) => void;
},
): Promise<GlobalSessionRecord[]> {
const all: GlobalSessionRecord[] = [];
const seenIds = new Set<string>();
let cursor: number | undefined;
while (true) {
const response = await retry(
() => apiClient.experimental.session.list({
archived: options.archived,
limit: options.pageSize,
...(cursor !== undefined ? { cursor } : {}),
}),
{ attempts: 3, delay: 500, retryIf: () => true },
);
const payload = Array.isArray(response.data) ? (response.data as GlobalSessionRecord[]) : [];
if (payload.length === 0) break;
let appended = 0;
for (const session of payload) {
if (!session?.id || seenIds.has(session.id)) continue;
seenIds.add(session.id);
all.push(session);
appended += 1;
}
if (appended > 0) {
options.onPage?.(payload);
}
// Stop on partial page — nothing more to fetch.
if (payload.length < options.pageSize) break;
// Prefer server header; fall back to last session's `time.updated`
// (cursor semantics on server = "updated strictly before this timestamp").
const headerCursor = toNumber(readResponseHeader(response, "x-next-cursor"));
const lastUpdated = payload[payload.length - 1]?.time?.updated;
const nextCursor = headerCursor
?? (typeof lastUpdated === "number" && Number.isFinite(lastUpdated) ? lastUpdated : undefined);
if (nextCursor === undefined) break;
// Loop guard: cursor must move backwards in time.
if (cursor !== undefined && nextCursor >= cursor) break;
// Every id in this page already seen — stop to avoid spinning.
if (appended === 0) break;
cursor = nextCursor;
}
return all;
}