1import type { ThreadId } from "@t3tools/contracts";
2import { formatComposerContextReference } from "@t3tools/shared/composerContextReferences";
3import { toKindScopedComposerContextId } from "./composerContextReferences";
4
5export interface TerminalContextSelection {
6 terminalId: string;
7 terminalLabel: string;
8 lineStart: number;
9 lineEnd: number;
10 text: string;
11}
12
13export interface TerminalContextDraft extends TerminalContextSelection {
14 id: string;
15 threadId: ThreadId;
16 createdAt: string;
17}
18
19/** Legacy ordinal placeholder from drafts saved before context references. Migration only. */
20export const INLINE_TERMINAL_CONTEXT_PLACEHOLDER = "\uFFFC";
21
22export interface TerminalContextReferenceSource {
23 id: string;
24 terminalLabel: string;
25 lineStart: number;
26 lineEnd: number;
27}
28
29/** The canonical inline link that stands for this context in the prompt. */
30export function formatTerminalContextReference(context: TerminalContextReferenceSource): string {
31 return formatComposerContextReference({
32 kind: "terminal",
33 contextId: toKindScopedComposerContextId("terminal", context.id),
34 label: formatTerminalContextLabel(context),
35 });
36}
37
38export function normalizeTerminalContextText(text: string): string {
39 return text.replace(/\r\n/g, "\n").replace(/^\n+|\n+$/g, "");
40}
41
42export function hasTerminalContextText(context: { text: string }): boolean {
43 return normalizeTerminalContextText(context.text).length > 0;
44}
45
46export function isTerminalContextExpired(context: { text: string }): boolean {
47 return !hasTerminalContextText(context);
48}
49
50export function filterTerminalContextsWithText<T extends { text: string }>(
51 contexts: ReadonlyArray<T>,
52): T[] {
53 return contexts.filter((context) => hasTerminalContextText(context));
54}
55
56function normalizeTerminalContextSelection(
57 selection: TerminalContextSelection,
58): TerminalContextSelection | null {
59 const text = normalizeTerminalContextText(selection.text);
60 const terminalId = selection.terminalId.trim();
61 const terminalLabel = selection.terminalLabel.trim();
62 if (text.length === 0 || terminalId.length === 0 || terminalLabel.length === 0) {
63 return null;
64 }
65 const lineStart = Math.max(1, Math.floor(selection.lineStart));
66 const lineEnd = Math.max(lineStart, Math.floor(selection.lineEnd));
67 return {
68 terminalId,
69 terminalLabel,
70 lineStart,
71 lineEnd,
72 text,
73 };
74}
75
76function formatTerminalContextRange(selection: { lineStart: number; lineEnd: number }): string {
77 return selection.lineStart === selection.lineEnd
78 ? `line ${selection.lineStart}`
79 : `lines ${selection.lineStart}-${selection.lineEnd}`;
80}
81
82export function formatTerminalContextLabel(selection: {
83 terminalLabel: string;
84 lineStart: number;
85 lineEnd: number;
86}): string {
87 return `${selection.terminalLabel} ${formatTerminalContextRange(selection)}`;
88}
89
90/** Binds legacy U+FFFC placeholders to contexts in array order; leftover placeholders vanish. */
91export function migrateLegacyTerminalContextPlaceholders(
92 prompt: string,
93 contexts: ReadonlyArray<TerminalContextReferenceSource>,
94): string {
95 if (!prompt.includes(INLINE_TERMINAL_CONTEXT_PLACEHOLDER)) return prompt;
96 let index = 0;
97 return prompt.replaceAll(INLINE_TERMINAL_CONTEXT_PLACEHOLDER, () => {
98 const context = contexts[index];
99 index += 1;
100 return context ? formatTerminalContextReference(context) : "";
101 });
102}