Files
NetMesh/application/state/terminalSelectionAttachment.ts
zhaolei 3c72efcb7f
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

120 lines
3.7 KiB
TypeScript

import type { ChatMessageAttachment, UploadedFile } from "../../infrastructure/ai/types";
import {
formatVaultNoteReferences,
isVaultNoteAttachment,
} from "./vaultNoteAttachment";
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;
}
/**
* Attachments whose text is inlined into the prompt instead of being sent as
* file parts: terminal selections and Vault note mentions.
*/
export function isInlineTextAttachment(
attachment: Pick<ChatMessageAttachment | UploadedFile, "terminalSelection" | "vaultNoteId">,
): boolean {
return isTerminalSelectionAttachment(attachment) || isVaultNoteAttachment(attachment);
}
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);
const notes = attachments.filter(isVaultNoteAttachment);
const noteBlocks = notes.length ? [`\n\n${formatVaultNoteReferences(notes)}`] : [];
const blocks = [...terminalBlocks, ...noteBlocks];
if (blocks.length === 0) return prompt;
if (!prompt.trim()) return blocks.join("").trimStart();
return `${prompt}${blocks.join("")}`;
}