mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 17:30:31 +00:00
306 lines
11 KiB
TypeScript
306 lines
11 KiB
TypeScript
import { normalizeReasoningEffort } from "../../domain/catalog/metadata";
|
|
import { type RuntimePermissionState, runtimePermissionStateOrDefault } from "../../domain/runtime/permissions";
|
|
import type { ApprovalsReviewer, ServiceTier } from "../../domain/runtime/policy";
|
|
import { parseServiceTier } from "../../domain/runtime/policy";
|
|
import type { RuntimeServiceTierRequest, RuntimeSettingsPatch } from "../../domain/runtime/thread-settings";
|
|
import type { ThreadActivationSnapshot } from "../../domain/threads/activation";
|
|
import type { ArchiveThreadInput } from "../../domain/threads/archive-markdown";
|
|
import type { ThreadGoal, ThreadGoalUpdate } from "../../domain/threads/goal";
|
|
import type { HistoricalTurn } from "../../domain/threads/history";
|
|
import type { Thread } from "../../domain/threads/model";
|
|
import { REFERENCED_THREAD_TURN_LIMIT } from "../../domain/threads/reference";
|
|
import type { TurnTranscriptSummary } from "../../domain/threads/transcript";
|
|
import type { ClientResponseByMethod } from "../connection/client";
|
|
import type { ClientRequestParams } from "../connection/rpc-messages";
|
|
import { type ThreadRecord, threadFromThreadRecord, threadsFromThreadRecords } from "../protocol/thread";
|
|
import { appServerThreadGoalUpdate, appServerThreadGoalUserHistoryItem, threadGoalFromAppServerGoal } from "../protocol/thread-goal";
|
|
import { appServerRuntimeSettingsPatch } from "../protocol/thread-settings";
|
|
import {
|
|
chronologicalTurnTranscriptSummariesFromTurnRecords,
|
|
completedTurnTranscriptSummariesFromTurnRecords,
|
|
transcriptEntriesFromTurnRecords,
|
|
} from "../protocol/turn";
|
|
import type { AppServerRequestClient } from "./request-client";
|
|
|
|
const THREAD_LIST_PAGE_LIMIT = 100;
|
|
|
|
export type ThreadTurnSortDirection = "asc" | "desc";
|
|
export type TurnTranscriptSummaryClient = AppServerRequestClient;
|
|
export type ThreadForkClient = AppServerRequestClient;
|
|
export type ThreadRollbackClient = AppServerRequestClient;
|
|
export type ThreadCompactionClient = AppServerRequestClient;
|
|
|
|
interface TurnTranscriptSummaryPage {
|
|
summaries: TurnTranscriptSummary[];
|
|
nextCursor: string | null;
|
|
}
|
|
|
|
interface ThreadActivationResponse extends Partial<RuntimePermissionState> {
|
|
thread: ThreadRecord;
|
|
cwd: string;
|
|
model: string | null;
|
|
serviceTier: ServiceTier | null;
|
|
approvalsReviewer: ApprovalsReviewer | null;
|
|
reasoningEffort: string | null;
|
|
sandbox?: RuntimePermissionState["sandboxPolicy"];
|
|
}
|
|
|
|
export interface AppServerStartThreadOptions {
|
|
cwd: string;
|
|
serviceTier?: RuntimeServiceTierRequest;
|
|
permissions?: RuntimeSettingsPatch["permissions"];
|
|
}
|
|
|
|
export interface AppServerStartEphemeralThreadOptions {
|
|
cwd: string;
|
|
serviceName: string;
|
|
developerInstructions: string;
|
|
}
|
|
|
|
interface AppServerThreadListOptions {
|
|
archived?: boolean;
|
|
cursor?: string | null;
|
|
limit?: number | null;
|
|
}
|
|
|
|
export function startThread(
|
|
client: AppServerRequestClient,
|
|
options: AppServerStartThreadOptions,
|
|
): Promise<ClientResponseByMethod["thread/start"]> {
|
|
const { cwd, serviceTier, permissions } = options;
|
|
return client.request("thread/start", {
|
|
cwd,
|
|
serviceName: "codex-panel",
|
|
...(serviceTier !== undefined ? { serviceTier } : {}),
|
|
...(permissions !== undefined ? { permissions } : {}),
|
|
});
|
|
}
|
|
|
|
export function startEphemeralThread(
|
|
client: AppServerRequestClient,
|
|
options: AppServerStartEphemeralThreadOptions,
|
|
): Promise<ClientResponseByMethod["thread/start"]> {
|
|
const { cwd, serviceName, developerInstructions } = options;
|
|
return client.request("thread/start", {
|
|
cwd,
|
|
serviceName,
|
|
developerInstructions,
|
|
ephemeral: true,
|
|
sandbox: "read-only",
|
|
approvalPolicy: "never",
|
|
environments: [],
|
|
});
|
|
}
|
|
|
|
export function resumeThread(
|
|
client: AppServerRequestClient,
|
|
threadId: string,
|
|
cwd: string,
|
|
): Promise<ClientResponseByMethod["thread/resume"]> {
|
|
return client.request("thread/resume", {
|
|
threadId,
|
|
cwd,
|
|
excludeTurns: true,
|
|
initialTurnsPage: { limit: 20, sortDirection: "desc", itemsView: "full" },
|
|
});
|
|
}
|
|
|
|
export async function listThreads(client: AppServerRequestClient, cwd: string, options: { archived?: boolean } = {}): Promise<Thread[]> {
|
|
const archived = options.archived ?? false;
|
|
const records: ThreadRecord[] = [];
|
|
const seenCursors = new Set<string>();
|
|
let cursor: string | null = null;
|
|
|
|
for (;;) {
|
|
const response = await listThreadPage(client, cwd, {
|
|
archived,
|
|
cursor,
|
|
limit: THREAD_LIST_PAGE_LIMIT,
|
|
});
|
|
records.push(...response.data);
|
|
|
|
cursor = response.nextCursor ?? null;
|
|
if (!cursor) break;
|
|
if (seenCursors.has(cursor)) {
|
|
throw new Error("Codex app-server returned a repeated thread list cursor.");
|
|
}
|
|
seenCursors.add(cursor);
|
|
}
|
|
|
|
return threadsFromThreadRecords(records, { archived });
|
|
}
|
|
|
|
export function threadFromAppServerRecord(thread: ThreadRecord, options: { archived?: boolean } = {}): Thread {
|
|
return threadFromThreadRecord(thread, options);
|
|
}
|
|
|
|
export async function readThreadForArchiveExport(client: AppServerRequestClient, threadId: string): Promise<ArchiveThreadInput> {
|
|
const response = await client.request("thread/read", { threadId, includeTurns: true });
|
|
return {
|
|
...threadFromThreadRecord(response.thread, { archived: true }),
|
|
transcriptEntries: transcriptEntriesFromTurnRecords(response.thread.turns),
|
|
};
|
|
}
|
|
|
|
export async function readCompletedTurnTranscriptSummariesPage(
|
|
client: TurnTranscriptSummaryClient,
|
|
threadId: string,
|
|
cursor: string | null,
|
|
limit: number,
|
|
sortDirection: ThreadTurnSortDirection = "asc",
|
|
): Promise<TurnTranscriptSummaryPage> {
|
|
const response = await listThreadTurns(client, threadId, cursor, limit, sortDirection);
|
|
return {
|
|
summaries: completedTurnTranscriptSummariesFromTurnRecords(response.data),
|
|
nextCursor: response.nextCursor,
|
|
};
|
|
}
|
|
|
|
export async function readReferencedThreadTurnTranscriptSummaries(
|
|
client: TurnTranscriptSummaryClient,
|
|
threadId: string,
|
|
limit = REFERENCED_THREAD_TURN_LIMIT,
|
|
): Promise<TurnTranscriptSummary[]> {
|
|
const response = await listThreadTurns(client, threadId, null, limit);
|
|
return chronologicalTurnTranscriptSummariesFromTurnRecords(response.data);
|
|
}
|
|
|
|
export interface ThreadRollbackSnapshot {
|
|
thread: Thread;
|
|
cwd: string;
|
|
turns: readonly HistoricalTurn[];
|
|
}
|
|
|
|
interface ThreadRollbackResult {
|
|
readonly thread: ThreadRecord & {
|
|
readonly cwd: string;
|
|
readonly turns: readonly HistoricalTurn[];
|
|
};
|
|
}
|
|
|
|
function threadRollbackSnapshotFromAppServerResponse(response: ThreadRollbackResult): ThreadRollbackSnapshot {
|
|
return {
|
|
thread: threadFromThreadRecord(response.thread),
|
|
cwd: response.thread.cwd,
|
|
turns: response.thread.turns,
|
|
};
|
|
}
|
|
|
|
export async function rollbackThread(client: ThreadRollbackClient, threadId: string, numTurns?: number): Promise<ThreadRollbackSnapshot> {
|
|
const response = await client.request("thread/rollback", { threadId, numTurns: numTurns ?? 1 });
|
|
return threadRollbackSnapshotFromAppServerResponse(response);
|
|
}
|
|
|
|
export async function forkThread(
|
|
client: ThreadForkClient,
|
|
threadId: string,
|
|
cwd: string,
|
|
lastTurnId: string | null = null,
|
|
): Promise<Thread> {
|
|
const response = await client.request("thread/fork", {
|
|
threadId,
|
|
cwd,
|
|
excludeTurns: true,
|
|
...(lastTurnId ? { lastTurnId } : {}),
|
|
});
|
|
return threadFromThreadRecord(response.thread);
|
|
}
|
|
|
|
export async function compactThread(client: ThreadCompactionClient, threadId: string): Promise<void> {
|
|
await client.request("thread/compact/start", { threadId });
|
|
}
|
|
|
|
export async function archiveThread(client: AppServerRequestClient, threadId: string): Promise<void> {
|
|
await client.request("thread/archive", { threadId });
|
|
}
|
|
|
|
export async function deleteThread(client: AppServerRequestClient, threadId: string, options: { timeoutMs?: number } = {}): Promise<void> {
|
|
await client.request("thread/delete", { threadId }, options);
|
|
}
|
|
|
|
export async function restoreArchivedThread(client: AppServerRequestClient, threadId: string): Promise<Thread> {
|
|
const response = await client.request("thread/unarchive", { threadId });
|
|
return threadFromThreadRecord(response.thread);
|
|
}
|
|
|
|
export function threadActivationSnapshotFromAppServerResponse(response: ThreadActivationResponse): ThreadActivationSnapshot {
|
|
const permissions = runtimePermissionStateOrDefault({
|
|
approvalPolicy: response.approvalPolicy ?? null,
|
|
sandboxPolicy: response.sandbox ?? response.sandboxPolicy ?? null,
|
|
activePermissionProfile: response.activePermissionProfile ?? null,
|
|
});
|
|
return {
|
|
thread: threadFromThreadRecord(response.thread),
|
|
cwd: response.cwd,
|
|
approvalPolicyKnown: "approvalPolicy" in response,
|
|
sandboxPolicyKnown: "sandbox" in response || "sandboxPolicy" in response,
|
|
permissionProfileKnown: "activePermissionProfile" in response,
|
|
model: response.model,
|
|
reasoningEffort: normalizeReasoningEffort(response.reasoningEffort),
|
|
serviceTier: parseServiceTier(response.serviceTier),
|
|
approvalsReviewer: response.approvalsReviewer,
|
|
...permissions,
|
|
};
|
|
}
|
|
|
|
export async function readThreadGoal(client: AppServerRequestClient, threadId: string): Promise<ThreadGoal | null> {
|
|
const response = await client.request("thread/goal/get", { threadId });
|
|
return threadGoalFromAppServerGoal(response.goal);
|
|
}
|
|
|
|
export async function setThreadGoal(
|
|
client: AppServerRequestClient,
|
|
threadId: string,
|
|
params: ThreadGoalUpdate,
|
|
): Promise<ThreadGoal | null> {
|
|
const response = await client.request("thread/goal/set", { threadId, ...appServerThreadGoalUpdate(params) });
|
|
return threadGoalFromAppServerGoal(response.goal);
|
|
}
|
|
|
|
export async function clearThreadGoal(client: AppServerRequestClient, threadId: string): Promise<void> {
|
|
await client.request("thread/goal/clear", { threadId });
|
|
}
|
|
|
|
export async function recordThreadGoalUserMessage(client: AppServerRequestClient, threadId: string, objective: string): Promise<void> {
|
|
await client.request("thread/inject_items", { threadId, items: [appServerThreadGoalUserHistoryItem(objective)] });
|
|
}
|
|
|
|
export async function renameThread(client: AppServerRequestClient, threadId: string, name: string): Promise<void> {
|
|
await client.request("thread/name/set", { threadId, name });
|
|
}
|
|
|
|
export async function updateThreadSettings(
|
|
client: AppServerRequestClient,
|
|
threadId: string,
|
|
settings: RuntimeSettingsPatch,
|
|
): Promise<void> {
|
|
await client.request("thread/settings/update", { threadId, ...appServerRuntimeSettingsPatch(settings) });
|
|
}
|
|
|
|
export function listThreadTurns(
|
|
client: AppServerRequestClient,
|
|
threadId: string,
|
|
cursor: string | null = null,
|
|
limit = 20,
|
|
sortDirection: ClientRequestParams<"thread/turns/list">["sortDirection"] = "desc",
|
|
itemsView: ClientRequestParams<"thread/turns/list">["itemsView"] = "full",
|
|
) {
|
|
return client.request("thread/turns/list", {
|
|
threadId,
|
|
cursor,
|
|
limit,
|
|
sortDirection,
|
|
itemsView,
|
|
});
|
|
}
|
|
|
|
function listThreadPage(client: AppServerRequestClient, cwd: string, options: AppServerThreadListOptions) {
|
|
return client.request("thread/list", {
|
|
cwd,
|
|
...(options.cursor ? { cursor: options.cursor } : {}),
|
|
...(options.limit === undefined ? {} : { limit: options.limit }),
|
|
archived: options.archived ?? false,
|
|
sortKey: "recency_at",
|
|
sortDirection: "desc",
|
|
});
|
|
}
|