Files
Netcatty/application/state/terminalSelectionAttachment.ts
T
2026-06-06 18:59:48 +08:00

102 lines
3.1 KiB
TypeScript

import type { ChatMessageAttachment, UploadedFile } from "../../infrastructure/ai/types";
export const TERMINAL_SELECTION_ATTACHMENT_MEDIA_TYPE = "text/plain";
const MAX_PREVIEW_CHARS = 80;
function bytesToBase64(bytes: Uint8Array): string {
let binary = "";
const chunkSize = 0x8000;
for (let i = 0; i < bytes.length; i += chunkSize) {
const chunk = bytes.subarray(i, i + chunkSize);
binary += String.fromCharCode(...chunk);
}
return btoa(binary);
}
function base64ToText(base64Data: string): string {
const binary = atob(base64Data);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i += 1) {
bytes[i] = binary.charCodeAt(i);
}
return new TextDecoder().decode(bytes);
}
function buildTimestamp(date: Date): string {
const pad = (value: number) => String(value).padStart(2, "0");
return [
date.getFullYear(),
pad(date.getMonth() + 1),
pad(date.getDate()),
pad(date.getHours()),
pad(date.getMinutes()),
pad(date.getSeconds()),
].join("-");
}
function getPreviewText(text: string): string {
const firstLine = text.split(/\r?\n/).find((line) => line.trim().length > 0) ?? "";
return firstLine.length > MAX_PREVIEW_CHARS
? `${firstLine.slice(0, MAX_PREVIEW_CHARS - 1)}...`
: firstLine;
}
export function createTerminalSelectionAttachment(
selection: string,
now: Date = new Date(),
): UploadedFile | null {
const text = selection.trim();
if (!text) return null;
const base64Data = bytesToBase64(new TextEncoder().encode(text));
const filename = `terminal-selection-${buildTimestamp(now)}.log`;
return {
id: crypto.randomUUID(),
filename,
dataUrl: `data:${TERMINAL_SELECTION_ATTACHMENT_MEDIA_TYPE};base64,${base64Data}`,
base64Data,
mediaType: TERMINAL_SELECTION_ATTACHMENT_MEDIA_TYPE,
terminalSelection: true,
previewText: getPreviewText(text),
lineCount: text.split(/\r?\n/).length,
};
}
export function decodeTerminalSelectionAttachment(
attachment: Pick<UploadedFile | ChatMessageAttachment, "base64Data" | "terminalSelection">,
): string | null {
if (!attachment.terminalSelection) return null;
return base64ToText(attachment.base64Data);
}
export function isTerminalSelectionAttachment(
attachment: Pick<UploadedFile | ChatMessageAttachment, "terminalSelection">,
): boolean {
return attachment.terminalSelection === true;
}
export function buildPromptWithTerminalSelectionAttachments(
prompt: string,
attachments: Array<ChatMessageAttachment | UploadedFile>,
): string {
const terminalBlocks = attachments
.filter(isTerminalSelectionAttachment)
.map((attachment, index) => {
const text = decodeTerminalSelectionAttachment(attachment);
if (!text) return null;
const label = attachment.filename || `terminal-selection-${index + 1}.log`;
return `\n\n[Terminal selection: ${label}]\n${text}`;
})
.filter((block): block is string => block !== null);
if (terminalBlocks.length === 0) return prompt;
if (!prompt.trim()) return terminalBlocks.join("").trimStart();
return `${prompt}${terminalBlocks.join("")}`;
}