108 lines
3.1 KiB
TypeScript
108 lines
3.1 KiB
TypeScript
import type { Session } from '@opencode-ai/sdk/v2';
|
|||
|
|
|
||
|
|
export const ACTIVE_NOW_STORAGE_KEY = 'oc.sessions.activeNow';
|
||
|
|
export const ACTIVE_NOW_MAX_AGE_MS = 36 * 60 * 60 * 1000;
|
||
|
|
|
||
|
|
export type ActiveNowEntry = {
|
||
|
|
sessionId: string;
|
||
|
|
};
|
||
|
|
|
||
|
|
const isSubtaskSession = (session: Session): boolean => {
|
||
|
|
return Boolean((session as Session & { parentID?: string | null }).parentID);
|
||
|
|
};
|
||
|
|
|
||
|
|
const isArchivedSession = (session: Session): boolean => {
|
||
|
|
return Boolean(session.time?.archived);
|
||
|
|
};
|
||
|
|
|
||
|
|
const getSessionUpdatedAt = (session: Session): number => {
|
||
|
|
const updated = session.time?.updated;
|
||
|
|
const created = session.time?.created;
|
||
|
|
if (typeof updated === 'number' && Number.isFinite(updated)) {
|
||
|
|
return updated;
|
||
|
|
}
|
||
|
|
if (typeof created === 'number' && Number.isFinite(created)) {
|
||
|
|
return created;
|
||
|
|
}
|
||
|
|
return 0;
|
||
|
|
};
|
||
|
|
|
||
|
|
export const readActiveNowEntries = (storage: Storage): ActiveNowEntry[] => {
|
||
|
|
try {
|
||
|
|
const raw = storage.getItem(ACTIVE_NOW_STORAGE_KEY);
|
||
|
|
if (!raw) {
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
const parsed = JSON.parse(raw) as unknown;
|
||
|
|
if (!Array.isArray(parsed)) {
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
const seen = new Set<string>();
|
||
|
|
const next: ActiveNowEntry[] = [];
|
||
|
|
parsed.forEach((item) => {
|
||
|
|
const sessionId = typeof item === 'string'
|
||
|
|
? item
|
||
|
|
: (item && typeof item === 'object' && 'sessionId' in item && typeof item.sessionId === 'string' ? item.sessionId : null);
|
||
|
|
if (!sessionId || seen.has(sessionId)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
seen.add(sessionId);
|
||
|
|
next.push({ sessionId });
|
||
|
|
});
|
||
|
|
return next;
|
||
|
|
} catch {
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
export const persistActiveNowEntries = (storage: Storage, entries: ActiveNowEntry[]): void => {
|
||
|
|
try {
|
||
|
|
storage.setItem(ACTIVE_NOW_STORAGE_KEY, JSON.stringify(entries));
|
||
|
|
} catch {
|
||
|
|
// ignored
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
export const pruneActiveNowEntries = (
|
||
|
|
entries: ActiveNowEntry[],
|
||
|
|
sessionsById: Map<string, Session>,
|
||
|
|
now = Date.now(),
|
||
|
|
): ActiveNowEntry[] => {
|
||
|
|
const minUpdatedAt = now - ACTIVE_NOW_MAX_AGE_MS;
|
||
|
|
return entries.filter((entry) => {
|
||
|
|
const session = sessionsById.get(entry.sessionId);
|
||
|
|
if (!session) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
if (isArchivedSession(session)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
return getSessionUpdatedAt(session) >= minUpdatedAt;
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
export const addActiveNowSession = (entries: ActiveNowEntry[], sessionId: string): ActiveNowEntry[] => {
|
||
|
|
if (!sessionId || entries.some((entry) => entry.sessionId === sessionId)) {
|
||
|
|
return entries;
|
||
|
|
}
|
||
|
|
return [{ sessionId }, ...entries];
|
||
|
|
};
|
||
|
|
|
||
|
|
export const sortSessionsByUpdated = (sessions: Session[]): Session[] => {
|
||
|
|
return [...sessions].sort((a, b) => getSessionUpdatedAt(b) - getSessionUpdatedAt(a));
|
||
|
|
};
|
||
|
|
|
||
|
|
export const deriveActiveNowSessions = (
|
||
|
|
entries: ActiveNowEntry[],
|
||
|
|
sessionsById: Map<string, Session>,
|
||
|
|
): Session[] => {
|
||
|
|
const sessions = entries
|
||
|
|
.map((entry) => sessionsById.get(entry.sessionId) ?? null)
|
||
|
|
.filter((session): session is Session => Boolean(session))
|
||
|
|
.filter((session) => !isArchivedSession(session))
|
||
|
|
.filter((session) => !isSubtaskSession(session));
|
||
|
|
return sortSessionsByUpdated(sessions);
|
||
|
|
};
|
||
|
|
|
||
|
|
export const getSessionUpdatedAtMs = getSessionUpdatedAt;
|