Files
2026-07-13 12:52:40 +08:00

193 lines
5.1 KiB
TypeScript

import { existsSync, readdirSync, rmSync } from "node:fs";
import { join } from "node:path";
import { SqliteSessionStore } from "@cline/core";
import { readSessionManifest, sharedSessionDataDir } from "../paths";
import type { JsonRecord } from "../types";
import {
compareSessionRecordsByStartedAtDesc,
derivePromptFromMessages,
resolveSessionListTitle,
} from "./common";
import { readPersistedChatMessages } from "./messages";
/**
* Minimal context shape required by discoverChatSessions so both the host
* backend and the sidecar can call it without pulling in CLI-specific code.
*/
export type DiscoveryChatContext = {
liveSessions: Map<
string,
{
busy: boolean;
prompt?: string;
title?: string;
messages: unknown[];
status: string;
config: JsonRecord;
startedAt: number;
endedAt?: number;
}
>;
};
function trimKnownString(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
if (!trimmed) {
return undefined;
}
return trimmed.toLowerCase() === "unknown" ? undefined : trimmed;
}
export function discoverChatSessions(
ctx: DiscoveryChatContext,
limit = 300,
): unknown[] {
const out: JsonRecord[] = [];
const store = new SqliteSessionStore();
for (const [sessionId, session] of ctx.liveSessions.entries()) {
if (!session.busy && !session.prompt && session.messages.length === 0) {
continue;
}
const prompt = session.prompt ?? derivePromptFromMessages(session.messages);
const resolvedTitle = resolveSessionListTitle({
sessionId,
metadata: session.title ? { title: session.title } : undefined,
prompt,
messages: session.messages,
});
out.push({
sessionId,
status: session.status,
provider: session.config.provider ?? "",
model: session.config.model ?? "",
cwd: session.config.cwd ?? session.config.workspaceRoot ?? "",
workspaceRoot: session.config.workspaceRoot ?? "",
prompt,
startedAt: String(session.startedAt),
endedAt: session.endedAt ? String(session.endedAt) : undefined,
metadata: { title: resolvedTitle },
});
}
const base = sharedSessionDataDir();
if (existsSync(base)) {
for (const entry of readdirSync(base, { withFileTypes: true })) {
if (!entry.isDirectory()) {
continue;
}
const sessionId = entry.name.trim();
if (!sessionId || out.some((item) => item.sessionId === sessionId)) {
continue;
}
// Skip subagent / team-task child sessions — they are shown
// under their parent, not as top-level sidebar entries.
if (sessionId.includes("__teamtask__") || sessionId.includes("__sub__")) {
continue;
}
if (!store.get(sessionId)) {
try {
rmSync(join(base, sessionId), { recursive: true, force: true });
} catch {
// Ignore cleanup failures and keep orphaned artifacts hidden.
}
continue;
}
const manifest = readSessionManifest(sessionId) ?? {};
// Skip sessions explicitly marked as subagent.
if (manifest.source === "subagent") {
continue;
}
const provider = trimKnownString(manifest.provider);
const model = trimKnownString(manifest.model);
if (!provider || !model) {
continue;
}
const messages = readPersistedChatMessages(sessionId) ?? [];
if (messages.length === 0) {
continue;
}
const metadata =
manifest.metadata && typeof manifest.metadata === "object"
? { ...(manifest.metadata as JsonRecord) }
: undefined;
const prompt = derivePromptFromMessages(messages);
const resolvedTitle = resolveSessionListTitle({
sessionId,
metadata,
prompt,
messages,
});
out.push({
sessionId,
status: "completed",
provider,
model,
cwd: manifest.cwd ?? "",
workspaceRoot:
manifest.workspace_root ??
manifest.workspaceRoot ??
manifest.cwd ??
"",
prompt,
startedAt: String(
manifest.started_at ?? manifest.startedAt ?? Date.now(),
),
endedAt:
(manifest.ended_at ?? manifest.endedAt)
? String(manifest.ended_at ?? manifest.endedAt)
: undefined,
metadata: {
...(metadata ?? {}),
title: resolvedTitle,
},
});
}
}
out.sort(compareSessionRecordsByStartedAtDesc);
return out.slice(0, Math.max(1, limit));
}
export function mergeDiscoveredSessionLists(
chat: unknown[],
cli: unknown[],
limit: number,
): unknown[] {
const merged = new Map<string, unknown>();
for (const item of [...chat, ...cli]) {
if (!item || typeof item !== "object") {
continue;
}
const sessionId = String(
(item as JsonRecord).sessionId ?? (item as JsonRecord).session_id ?? "",
).trim();
if (!sessionId || merged.has(sessionId)) {
continue;
}
const normalized = item as JsonRecord;
merged.set(sessionId, {
...normalized,
sessionId,
startedAt:
normalized.startedAt ?? normalized.started_at ?? String(Date.now()),
endedAt: normalized.endedAt ?? normalized.ended_at,
workspaceRoot:
normalized.workspaceRoot ??
normalized.workspace_root ??
normalized.cwd ??
"",
});
}
return Array.from(merged.values())
.sort((left, right) =>
compareSessionRecordsByStartedAtDesc(
left as JsonRecord,
right as JsonRecord,
),
)
.slice(0, limit);
}