export type Attachment = { id: string; kind: 'file' | 'lines' | 'paste'; filename: string; language: string | null; content: string; range?: [number, number]; source: '@' | 'line-select' | 'drop' | 'paste'; }; // v1.7: caps shared between drag-drop and paste-as-attachment so both paths // reject the same way. Match the existing 10-attachment cap in // ChatInput.addAttachment. export const MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024; // 5 MB export const PASTE_INLINE_MAX_LINES = 8; // First-8KB null-byte scan. Returns true if the content looks binary. // Accepts a string (post-decode), an ArrayBuffer (pre-decode), or a Uint8Array. // For binary files like PNG, scanning bytes is more reliable than scanning // post-UTF-8-decode strings because invalid sequences may be replaced rather // than preserved. export function looksBinary(content: string | ArrayBuffer | Uint8Array): boolean { const SCAN_BYTES = 8192; if (typeof content === 'string') { const max = Math.min(content.length, SCAN_BYTES); for (let i = 0; i < max; i++) { if (content.charCodeAt(i) === 0) return true; } return false; } const bytes = content instanceof Uint8Array ? content : new Uint8Array(content); const max = Math.min(bytes.length, SCAN_BYTES); for (let i = 0; i < max; i++) { if (bytes[i] === 0) return true; } return false; } export const LANG_MAP: Record = { ts: 'typescript', tsx: 'tsx', js: 'javascript', jsx: 'jsx', mjs: 'javascript', cjs: 'javascript', py: 'python', go: 'go', rs: 'rust', rb: 'ruby', java: 'java', c: 'c', h: 'c', cpp: 'cpp', cc: 'cpp', hpp: 'cpp', cs: 'csharp', php: 'php', sh: 'bash', bash: 'bash', zsh: 'bash', yml: 'yaml', yaml: 'yaml', json: 'json', toml: 'toml', md: 'markdown', markdown: 'markdown', sql: 'sql', dockerfile: 'dockerfile', html: 'html', htm: 'html', css: 'css', scss: 'scss', }; export function inferLanguage(filename: string): string | null { const base = filename.split('/').pop() ?? filename; if (base.toLowerCase() === 'dockerfile') return 'dockerfile'; const m = base.match(/\.([^.]+)$/); return m ? (LANG_MAP[m[1]!.toLowerCase()] ?? null) : null; } export function flattenToMessage(attachments: Attachment[], text: string): string { if (attachments.length === 0) return text; // Pasted text is raw context, not code from a file — insert it verbatim with no // ``` fence or provenance header. It trails the typed text with a leading space // so a leading slash command / prompt stays first and the paste reads as its // continuation. File/line chips stay fenced provenance blocks, appended after. const pasteBlocks: string[] = []; const fencedBlocks: string[] = []; for (const a of attachments) { if (a.kind === 'paste') { pasteBlocks.push(a.content); continue; } const fence = '```' + (a.language ?? ''); const header = a.kind === 'lines' ? `// from: ${a.filename}:${a.range?.[0] ?? '?'}-${a.range?.[1] ?? '?'}` : `// from: ${a.filename}`; fencedBlocks.push(`${fence}\n${header}\n${a.content}\n\`\`\``); } // Typed text + pasted content on the same logical line (space-joined), then // any fenced file blocks as separate paragraphs. const lead = [text, ...pasteBlocks].filter(Boolean).join(' '); return [lead, ...fencedBlocks].filter(Boolean).join('\n\n'); }