import { INLINE_TERMINAL_CONTEXT_PLACEHOLDER, type TerminalContextDraft,} from "./lib/terminalContext";import { collectComposerInlineTokens, type ComposerInlineToken,} from "@t3tools/shared/composerInlineTokens"; export type ComposerPromptSegment = | { type: "text"; text: string; } | { type: "mention"; path: string; source: string; } | { type: "skill"; name: string; } | { type: "terminal-context"; context: TerminalContextDraft | null; }; function rangeIncludesIndex(start: number, end: number, index: number): boolean { return start <= index && index < end;} function pushTextSegment(segments: ComposerPromptSegment[], text: string): void { if (!text) return; const last = segments[segments.length - 1]; if (last && last.type === "text") { last.text += text; return; } segments.push({ type: "text", text });} function forEachPromptSegmentSlice( prompt: string, visitor: ( slice: | { type: "text"; text: string; promptOffset: number; } | { type: "terminal-context"; promptOffset: number; }, ) => boolean | void,): boolean { let textCursor = 0; for (let index = 0; index < prompt.length; index += 1) { if (prompt[index] !== INLINE_TERMINAL_CONTEXT_PLACEHOLDER) { continue; } if ( index > textCursor && visitor({ type: "text", text: prompt.slice(textCursor, index), promptOffset: textCursor, }) === true ) { return true; } if (visitor({ type: "terminal-context", promptOffset: index }) === true) { return true; } textCursor = index + 1; } if ( textCursor < prompt.length && visitor({ type: "text", text: prompt.slice(textCursor), promptOffset: textCursor, }) === true ) { return true; } return false;} function forEachPromptTextSlice( prompt: string, visitor: (text: string, promptOffset: number) => boolean | void,): boolean { return forEachPromptSegmentSlice(prompt, (slice) => { if (slice.type !== "text") { return false; } return visitor(slice.text, slice.promptOffset); });} function forEachMentionMatch( prompt: string, visitor: ( match: Extract<ComposerInlineToken, { type: "mention" }>, promptOffset: number, ) => boolean | void,): boolean { return forEachPromptTextSlice(prompt, (text, promptOffset) => { for (const match of collectComposerInlineTokens(text)) { if (match.type !== "mention") { continue; } if (visitor(match, promptOffset) === true) { return true; } } return false; });} function splitPromptTextIntoComposerSegments(text: string): ComposerPromptSegment[] { const segments: ComposerPromptSegment[] = []; if (!text) { return segments; } const tokenMatches = collectComposerInlineTokens(text); let cursor = 0; for (const match of tokenMatches) { if (match.start < cursor) { continue; } if (match.start > cursor) { pushTextSegment(segments, text.slice(cursor, match.start)); } if (match.type === "mention") { segments.push({ type: "mention", path: match.value, source: match.source, }); } else { segments.push({ type: "skill", name: match.value }); } cursor = match.end; } if (cursor < text.length) { pushTextSegment(segments, text.slice(cursor)); } return segments;} export function selectionTouchesMentionBoundary( prompt: string, start: number, end: number,): boolean { if (!prompt || start >= end) { return false; } return forEachMentionMatch(prompt, (match, promptOffset) => { const mentionStart = promptOffset + match.start; const mentionEnd = promptOffset + match.end; const beforeMentionIndex = mentionStart - 1; const afterMentionIndex = mentionEnd; if ( beforeMentionIndex >= 0 && /\s/.test(prompt[beforeMentionIndex] ?? "") && rangeIncludesIndex(start, end, beforeMentionIndex) ) { return true; } if ( afterMentionIndex < prompt.length && /\s/.test(prompt[afterMentionIndex] ?? "") && rangeIncludesIndex(start, end, afterMentionIndex) ) { return true; } return false; });} export function splitPromptIntoComposerSegments( prompt: string, terminalContexts: ReadonlyArray<TerminalContextDraft> = [],): ComposerPromptSegment[] { if (!prompt) { return []; } const segments: ComposerPromptSegment[] = []; let terminalContextIndex = 0; forEachPromptSegmentSlice(prompt, (slice) => { if (slice.type === "text") { segments.push(...splitPromptTextIntoComposerSegments(slice.text)); return false; } segments.push({ type: "terminal-context", context: terminalContexts[terminalContextIndex] ?? null, }); terminalContextIndex += 1; return false; }); return segments;}
import {
INLINE_TERMINAL_CONTEXT_PLACEHOLDER,
type TerminalContextDraft,
} from "./lib/terminalContext";
import {
collectComposerInlineTokens,
type ComposerInlineToken,
} from "@t3tools/shared/composerInlineTokens";
export type ComposerPromptSegment =
| {
type: "text";
text: string;
}
| {
type: "mention";
path: string;
source: string;
}
| {
type: "skill";
name: string;
}
| {
type: "terminal-context";
context: TerminalContextDraft | null;
};
function rangeIncludesIndex(start: number, end: number, index: number): boolean {
return start <= index && index < end;
}
function pushTextSegment(segments: ComposerPromptSegment[], text: string): void {
if (!text) return;
const last = segments[segments.length - 1];
if (last && last.type === "text") {
last.text += text;
return;
}
segments.push({ type: "text", text });
}
function forEachPromptSegmentSlice(
prompt: string,
visitor: (
slice:
| {
type: "text";
text: string;
promptOffset: number;
}
| {
type: "terminal-context";
promptOffset: number;
},
) => boolean | void,
): boolean {
let textCursor = 0;
for (let index = 0; index < prompt.length; index += 1) {
if (prompt[index] !== INLINE_TERMINAL_CONTEXT_PLACEHOLDER) {
continue;
}
if (
index > textCursor &&
visitor({
type: "text",
text: prompt.slice(textCursor, index),
promptOffset: textCursor,
}) === true
) {
return true;
}
if (visitor({ type: "terminal-context", promptOffset: index }) === true) {
return true;
}
textCursor = index + 1;
}
if (
textCursor < prompt.length &&
visitor({
type: "text",
text: prompt.slice(textCursor),
promptOffset: textCursor,
}) === true
) {
return true;
}
return false;
}
function forEachPromptTextSlice(
prompt: string,
visitor: (text: string, promptOffset: number) => boolean | void,
): boolean {
return forEachPromptSegmentSlice(prompt, (slice) => {
if (slice.type !== "text") {
return false;
}
return visitor(slice.text, slice.promptOffset);
});
}
function forEachMentionMatch(
prompt: string,
visitor: (
match: Extract<ComposerInlineToken, { type: "mention" }>,
promptOffset: number,
) => boolean | void,
): boolean {
return forEachPromptTextSlice(prompt, (text, promptOffset) => {
for (const match of collectComposerInlineTokens(text)) {
if (match.type !== "mention") {
continue;
}
if (visitor(match, promptOffset) === true) {
return true;
}
}
return false;
});
}
function splitPromptTextIntoComposerSegments(text: string): ComposerPromptSegment[] {
const segments: ComposerPromptSegment[] = [];
if (!text) {
return segments;
}
const tokenMatches = collectComposerInlineTokens(text);
let cursor = 0;
for (const match of tokenMatches) {
if (match.start < cursor) {
continue;
}
if (match.start > cursor) {
pushTextSegment(segments, text.slice(cursor, match.start));
}
if (match.type === "mention") {
segments.push({
type: "mention",
path: match.value,
source: match.source,
});
} else {
segments.push({ type: "skill", name: match.value });
}
cursor = match.end;
}
if (cursor < text.length) {
pushTextSegment(segments, text.slice(cursor));
}
return segments;
}
export function selectionTouchesMentionBoundary(
prompt: string,
start: number,
end: number,
): boolean {
if (!prompt || start >= end) {
return false;
}
return forEachMentionMatch(prompt, (match, promptOffset) => {
const mentionStart = promptOffset + match.start;
const mentionEnd = promptOffset + match.end;
const beforeMentionIndex = mentionStart - 1;
const afterMentionIndex = mentionEnd;
if (
beforeMentionIndex >= 0 &&
/\s/.test(prompt[beforeMentionIndex] ?? "") &&
rangeIncludesIndex(start, end, beforeMentionIndex)
) {
return true;
}
if (
afterMentionIndex < prompt.length &&
/\s/.test(prompt[afterMentionIndex] ?? "") &&
rangeIncludesIndex(start, end, afterMentionIndex)
) {
return true;
}
return false;
});
}
export function splitPromptIntoComposerSegments(
prompt: string,
terminalContexts: ReadonlyArray<TerminalContextDraft> = [],
): ComposerPromptSegment[] {
if (!prompt) {
return [];
}
const segments: ComposerPromptSegment[] = [];
let terminalContextIndex = 0;
forEachPromptSegmentSlice(prompt, (slice) => {
if (slice.type === "text") {
segments.push(...splitPromptTextIntoComposerSegments(slice.text));
return false;
}
segments.push({
type: "terminal-context",
context: terminalContexts[terminalContextIndex] ?? null,
});
terminalContextIndex += 1;
return false;
});
return segments;
}