1export const PASTED_TEXT_ATTACHMENT_THRESHOLD_BYTES = 32 * 1024;
2
3const textEncoder = new TextEncoder();
4
5export type PastedTextDisposition = "attachment" | "inline";
6
7export function isPasteAsTextShortcut(
8 event: Pick<KeyboardEvent, "key" | "metaKey" | "ctrlKey" | "altKey" | "shiftKey">,
9 macPlatform: boolean,
10): boolean {
11 return (
12 event.key.toLowerCase() === "v" &&
13 event.shiftKey &&
14 !event.altKey &&
15 (macPlatform ? event.metaKey && !event.ctrlKey : event.ctrlKey && !event.metaKey)
16 );
17}
18
19/**
20 * Large clipboard text becomes a file so an agent can inspect it selectively.
21 * The threshold is byte-based: character counts substantially understate the
22 * context cost of some Unicode-heavy clipboard contents.
23 */
24export function pastedTextDisposition(input: {
25 readonly text: string;
26 readonly canAttach: boolean;
27 readonly bypassAutoAttachment?: boolean;
28 readonly wouldExceedInputLimit?: boolean;
29}): PastedTextDisposition {
30 if (input.bypassAutoAttachment || !input.canAttach || input.text.length === 0) {
31 return "inline";
32 }
33 return input.wouldExceedInputLimit ||
34 input.text.length >= PASTED_TEXT_ATTACHMENT_THRESHOLD_BYTES ||
35 textEncoder.encode(input.text).byteLength >= PASTED_TEXT_ATTACHMENT_THRESHOLD_BYTES
36 ? "attachment"
37 : "inline";
38}
39
40/** Stable, human-readable names when a draft contains several folded pastes. */
41export function nextPastedTextFileName(existingNames: ReadonlyArray<string>): string {
42 const names = new Set(existingNames.map((name) => name.toLowerCase()));
43 if (!names.has("pasted-text.txt")) {
44 return "pasted-text.txt";
45 }
46 for (let sequence = 2; ; sequence += 1) {
47 const candidate = `pasted-text-${sequence}.txt`;
48 if (!names.has(candidate)) {
49 return candidate;
50 }
51 }
52}
53
54export function replaceTextSelection(input: {
55 readonly value: string;
56 readonly selection: { readonly start: number; readonly end: number };
57 readonly text: string;
58}): { readonly value: string; readonly cursor: number } {
59 const start = Math.max(0, Math.min(input.value.length, input.selection.start));
60 const end = Math.max(start, Math.min(input.value.length, input.selection.end));
61 return {
62 value: `${input.value.slice(0, start)}${input.text}${input.value.slice(end)}`,
63 cursor: start + input.text.length,
64 };
65}
66
67export function wouldTextPasteExceedLimit(input: {
68 readonly valueLength: number;
69 readonly selection: { readonly start: number; readonly end: number };
70 readonly textLength: number;
71 readonly maxLength: number;
72}): boolean {
73 const start = Math.max(0, Math.min(input.valueLength, input.selection.start));
74 const end = Math.max(start, Math.min(input.valueLength, input.selection.end));
75 return input.valueLength - (end - start) + input.textLength > input.maxLength;
76}