mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 17:30:31 +00:00
145 lines
6.1 KiB
TypeScript
145 lines
6.1 KiB
TypeScript
import { fileReferencesFromManifest, referencedThreadFromManifest, userMessageContextProjection } from "../../domain/chat/context-manifest";
|
|
import type { VaultFileReference } from "../../domain/chat/input";
|
|
import { type ReferencedThreadMetadata, referencedThreadMetadataFromPrompt } from "../../domain/threads/reference";
|
|
import {
|
|
nonEmptyTurnTranscriptSummaries,
|
|
type ThreadTranscriptEntry,
|
|
type TurnTranscriptSummary,
|
|
turnTranscriptSummaryFromTranscriptEntries,
|
|
} from "../../domain/threads/transcript";
|
|
import type { ThreadItem as GeneratedThreadItem } from "../../generated/app-server/v2/ThreadItem";
|
|
import type { Turn as GeneratedTurn } from "../../generated/app-server/v2/Turn";
|
|
|
|
export type TurnItem = GeneratedThreadItem;
|
|
export type TurnRecord = GeneratedTurn;
|
|
|
|
function transcriptEntriesFromTurnRecord(turn: TurnRecord): ThreadTranscriptEntry[] {
|
|
return turn.items.flatMap((item) => transcriptEntriesFromTurnItem(item, turn));
|
|
}
|
|
|
|
export function transcriptEntriesFromTurnRecords(turns: readonly TurnRecord[]): ThreadTranscriptEntry[] {
|
|
return turns.flatMap(transcriptEntriesFromTurnRecord);
|
|
}
|
|
|
|
function turnTranscriptSummaryFromTurnRecord(turn: TurnRecord): TurnTranscriptSummary {
|
|
return turnTranscriptSummaryFromTranscriptEntries(transcriptEntriesFromTurnRecord(turn));
|
|
}
|
|
|
|
export function turnTranscriptAssistantTextFromTurnRecord(turn: TurnRecord): string | null {
|
|
return turnTranscriptSummaryFromTurnRecord(turn).assistantText;
|
|
}
|
|
|
|
export function completedTurnTranscriptSummaryFromTurnRecord(turn: TurnRecord): TurnTranscriptSummary | null {
|
|
if (turn.status !== "completed") return null;
|
|
const summary = turnTranscriptSummaryFromTurnRecord(turn);
|
|
return summary.userText && summary.assistantText ? summary : null;
|
|
}
|
|
|
|
export function completedTurnTranscriptSummariesFromTurnRecords(turns: readonly TurnRecord[]): TurnTranscriptSummary[] {
|
|
return turns.flatMap((turn) => {
|
|
const summary = completedTurnTranscriptSummaryFromTurnRecord(turn);
|
|
return summary ? [summary] : [];
|
|
});
|
|
}
|
|
|
|
export function chronologicalTurnTranscriptSummariesFromTurnRecords(turns: readonly TurnRecord[]): TurnTranscriptSummary[] {
|
|
return nonEmptyTurnTranscriptSummaries(
|
|
[...turns].sort((a, b) => (a.startedAt ?? 0) - (b.startedAt ?? 0)).map(turnTranscriptSummaryFromTurnRecord),
|
|
);
|
|
}
|
|
|
|
export interface TurnUserItemProjection {
|
|
text: string;
|
|
referencedThread: ReferencedThreadMetadata | null;
|
|
manifest: ReturnType<typeof userMessageContextProjection>["manifest"];
|
|
}
|
|
|
|
export function turnUserItemProjection(item: Extract<TurnItem, { type: "userMessage" }>): TurnUserItemProjection {
|
|
const projected = userMessageContextProjection(item.content, item.clientId);
|
|
const supplementalText = nonTextUserInputText(item.content, projected.text);
|
|
const text = [projected.text, supplementalText].filter(Boolean).join("\n");
|
|
const legacyReference = referencedThreadMetadataFromPrompt(text);
|
|
return {
|
|
text: legacyReference?.text ?? text,
|
|
referencedThread: referencedThreadFromManifest(projected.manifest) ?? legacyReference?.reference ?? null,
|
|
manifest: projected.manifest,
|
|
};
|
|
}
|
|
|
|
export function turnUserFileReferences(
|
|
item: Extract<TurnItem, { type: "userMessage" }>,
|
|
manifest: ReturnType<typeof userMessageContextProjection>["manifest"],
|
|
): VaultFileReference[] {
|
|
const legacyReferences = item.content.flatMap((input) => {
|
|
if (input.type !== "mention") return [];
|
|
const reference = legacyPanelFileReference(input);
|
|
return reference ? [reference] : [];
|
|
});
|
|
return [...fileReferencesFromManifest(manifest), ...legacyReferences];
|
|
}
|
|
|
|
export function lastAgentMessageTextFromTurnRecord(turn: TurnRecord): string | null {
|
|
for (let index = turn.items.length - 1; index >= 0; index -= 1) {
|
|
const item = turn.items[index];
|
|
if (item === undefined) continue;
|
|
if (item.type !== "agentMessage") continue;
|
|
const text = item.text.trim();
|
|
if (text) return text;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function transcriptEntriesFromTurnItem(item: TurnItem, turn: TurnRecord): ThreadTranscriptEntry[] {
|
|
if (item.type === "userMessage") {
|
|
const projection = turnUserItemProjection(item);
|
|
const text = projection.text.trim();
|
|
const contexts =
|
|
projection.manifest?.contexts
|
|
.filter((context) => context.kind === "web" || context.kind === "obsidian")
|
|
.map((context) => ({ kind: context.kind as "web" | "obsidian", truncated: context.truncated })) ?? [];
|
|
return text
|
|
? [
|
|
{
|
|
kind: "user",
|
|
text,
|
|
timestamp: turn.startedAt,
|
|
...(projection.referencedThread ? { referencedThread: projection.referencedThread } : {}),
|
|
...(contexts.length > 0 ? { contexts } : {}),
|
|
},
|
|
]
|
|
: [];
|
|
}
|
|
if (item.type === "agentMessage") {
|
|
const text = item.text.trim();
|
|
return text ? [{ kind: "assistant", text, timestamp: turn.completedAt ?? turn.startedAt }] : [];
|
|
}
|
|
if (item.type === "plan") {
|
|
const text = item.text.trim();
|
|
return text ? [{ kind: "plan", text, timestamp: turn.completedAt ?? turn.startedAt }] : [];
|
|
}
|
|
return [];
|
|
}
|
|
|
|
function nonTextUserInputText(content: Extract<TurnItem, { type: "userMessage" }>["content"], visibleText: string): string {
|
|
const hasText = visibleText.length > 0;
|
|
const textIncludes = (value: string) => value.length > 0 && visibleText.includes(value);
|
|
return content
|
|
.map((item) => {
|
|
if (item.type === "localImage") return hasText && textIncludes(item.path) ? "" : `[local image] ${item.path}`;
|
|
if (item.type === "image") return hasText && textIncludes(item.url) ? "" : `[image] ${item.url}`;
|
|
if (item.type === "mention") {
|
|
if (hasText) return "";
|
|
const fileReference = legacyPanelFileReference(item);
|
|
return fileReference ? `[file] ${fileReference.path}` : `[@${item.name}] ${item.path}`;
|
|
}
|
|
if (item.type === "skill") return hasText ? "" : `[$${item.name}] ${item.path}`;
|
|
return "";
|
|
})
|
|
.filter(Boolean)
|
|
.join("\n");
|
|
}
|
|
|
|
function legacyPanelFileReference(input: { name: string; path: string }): VaultFileReference | null {
|
|
if (!input.path || /^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(input.path)) return null;
|
|
return { name: input.name, path: input.path };
|
|
}
|