@things-factory/ai-assistant 10.1.109 → 10.1.111

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,28 @@
1
+ /** The part of the chat's height the small box may take before it scrolls. */
2
+ export declare const COMPOSER_GROWTH = 0.4;
3
+ /** The box's height for its content, both border-box pixels. */
4
+ export declare function composerHeight(content: number, chat: number, resting: number): {
5
+ height: number;
6
+ scrolls: boolean;
7
+ };
8
+ export type ComposerKey = {
9
+ key: string;
10
+ shiftKey?: boolean;
11
+ ctrlKey?: boolean;
12
+ metaKey?: boolean;
13
+ altKey?: boolean;
14
+ };
15
+ /** What a key does in the box: send, close the wide box, or nothing of ours (the textarea keeps it). */
16
+ export declare function composerKeyAction(e: ComposerKey, wide: boolean): 'send' | 'narrow' | null;
17
+ /** The shortcut the wide box's send button names, in the keys of this machine. */
18
+ export declare function sendShortcut(platform: string | undefined): string;
19
+ /**
20
+ * A pasted table given back as words: what the clipboard held as text, or - when it held only HTML - the rows with
21
+ * a tab between cells, as a spreadsheet copies them.
22
+ */
23
+ export declare function tableAsWords(table: {
24
+ rows: string[][];
25
+ text?: string | null;
26
+ }): string;
27
+ /** The words appended to what is already written - on a line of their own. */
28
+ export declare function appendWords(written: string, words: string): string;
@@ -0,0 +1,46 @@
1
+ /*
2
+ * The composer's frame (operato-application/design/ai/shared-dock.md §10) - how tall it stands, what Enter does,
3
+ * and what a pasted table gives back as words. Pure: the chat hands in its measures and the key.
4
+ *
5
+ * - The box grows with its text up to 40% of the chat's height, then scrolls. It never stands under its resting
6
+ * height, and on a chat too short for 40% to hold that height the resting height wins.
7
+ * - The wide box (넓게 쓰기) covers the conversation inside the dock; the page beside it stays where it was.
8
+ * - In the small box Enter sends and Shift+Enter breaks the line, as in every chat. In the wide box Enter breaks
9
+ * the line - long writing is made of lines - and Ctrl/Cmd+Enter sends. Ctrl/Cmd+Enter sends in both.
10
+ */
11
+ /** The part of the chat's height the small box may take before it scrolls. */
12
+ export const COMPOSER_GROWTH = 0.4;
13
+ /** The box's height for its content, both border-box pixels. */
14
+ export function composerHeight(content, chat, resting) {
15
+ const cap = Math.max(resting, Math.floor(chat * COMPOSER_GROWTH));
16
+ return { height: Math.min(Math.max(content, resting), cap), scrolls: content > cap };
17
+ }
18
+ /** What a key does in the box: send, close the wide box, or nothing of ours (the textarea keeps it). */
19
+ export function composerKeyAction(e, wide) {
20
+ if (e.key === 'Escape')
21
+ return wide ? 'narrow' : null;
22
+ if (e.key !== 'Enter' || e.altKey)
23
+ return null;
24
+ if (e.ctrlKey || e.metaKey)
25
+ return 'send';
26
+ if (wide || e.shiftKey)
27
+ return null;
28
+ return 'send';
29
+ }
30
+ /** The shortcut the wide box's send button names, in the keys of this machine. */
31
+ export function sendShortcut(platform) {
32
+ return /mac|iphone|ipad/i.test(platform ?? '') ? '⌘ Enter' : 'Ctrl+Enter';
33
+ }
34
+ /**
35
+ * A pasted table given back as words: what the clipboard held as text, or - when it held only HTML - the rows with
36
+ * a tab between cells, as a spreadsheet copies them.
37
+ */
38
+ export function tableAsWords(table) {
39
+ const text = (table.text ?? '').replace(/\r\n?/g, '\n').replace(/\n+$/, '');
40
+ return text.trim() ? text : table.rows.map(row => row.join('\t')).join('\n');
41
+ }
42
+ /** The words appended to what is already written - on a line of their own. */
43
+ export function appendWords(written, words) {
44
+ return written.trim() ? `${written.replace(/\s+$/, '')}\n${words}` : words;
45
+ }
46
+ //# sourceMappingURL=composer-frame.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"composer-frame.js","sourceRoot":"","sources":["../../client/utils/composer-frame.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,8EAA8E;AAC9E,MAAM,CAAC,MAAM,eAAe,GAAG,GAAG,CAAA;AAElC,gEAAgE;AAChE,MAAM,UAAU,cAAc,CAAC,OAAe,EAAE,IAAY,EAAE,OAAe;IAC3E,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,eAAe,CAAC,CAAC,CAAA;IACjE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,GAAG,CAAC,EAAE,OAAO,EAAE,OAAO,GAAG,GAAG,EAAE,CAAA;AACtF,CAAC;AAID,wGAAwG;AACxG,MAAM,UAAU,iBAAiB,CAAC,CAAc,EAAE,IAAa;IAC7D,IAAI,CAAC,CAAC,GAAG,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAA;IACrD,IAAI,CAAC,CAAC,GAAG,KAAK,OAAO,IAAI,CAAC,CAAC,MAAM;QAAE,OAAO,IAAI,CAAA;IAC9C,IAAI,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,OAAO;QAAE,OAAO,MAAM,CAAA;IACzC,IAAI,IAAI,IAAI,CAAC,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAA;IACnC,OAAO,MAAM,CAAA;AACf,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,YAAY,CAAC,QAA4B;IACvD,OAAO,kBAAkB,CAAC,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,YAAY,CAAA;AAC3E,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,KAAiD;IAC5E,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;IAC3E,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AAC9E,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,WAAW,CAAC,OAAe,EAAE,KAAa;IACxD,OAAO,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAA;AAC5E,CAAC","sourcesContent":["/*\n * The composer's frame (operato-application/design/ai/shared-dock.md §10) - how tall it stands, what Enter does,\n * and what a pasted table gives back as words. Pure: the chat hands in its measures and the key.\n *\n * - The box grows with its text up to 40% of the chat's height, then scrolls. It never stands under its resting\n * height, and on a chat too short for 40% to hold that height the resting height wins.\n * - The wide box (넓게 쓰기) covers the conversation inside the dock; the page beside it stays where it was.\n * - In the small box Enter sends and Shift+Enter breaks the line, as in every chat. In the wide box Enter breaks\n * the line - long writing is made of lines - and Ctrl/Cmd+Enter sends. Ctrl/Cmd+Enter sends in both.\n */\n\n/** The part of the chat's height the small box may take before it scrolls. */\nexport const COMPOSER_GROWTH = 0.4\n\n/** The box's height for its content, both border-box pixels. */\nexport function composerHeight(content: number, chat: number, resting: number): { height: number; scrolls: boolean } {\n const cap = Math.max(resting, Math.floor(chat * COMPOSER_GROWTH))\n return { height: Math.min(Math.max(content, resting), cap), scrolls: content > cap }\n}\n\nexport type ComposerKey = { key: string; shiftKey?: boolean; ctrlKey?: boolean; metaKey?: boolean; altKey?: boolean }\n\n/** What a key does in the box: send, close the wide box, or nothing of ours (the textarea keeps it). */\nexport function composerKeyAction(e: ComposerKey, wide: boolean): 'send' | 'narrow' | null {\n if (e.key === 'Escape') return wide ? 'narrow' : null\n if (e.key !== 'Enter' || e.altKey) return null\n if (e.ctrlKey || e.metaKey) return 'send'\n if (wide || e.shiftKey) return null\n return 'send'\n}\n\n/** The shortcut the wide box's send button names, in the keys of this machine. */\nexport function sendShortcut(platform: string | undefined): string {\n return /mac|iphone|ipad/i.test(platform ?? '') ? '⌘ Enter' : 'Ctrl+Enter'\n}\n\n/**\n * A pasted table given back as words: what the clipboard held as text, or - when it held only HTML - the rows with\n * a tab between cells, as a spreadsheet copies them.\n */\nexport function tableAsWords(table: { rows: string[][]; text?: string | null }): string {\n const text = (table.text ?? '').replace(/\\r\\n?/g, '\\n').replace(/\\n+$/, '')\n return text.trim() ? text : table.rows.map(row => row.join('\\t')).join('\\n')\n}\n\n/** The words appended to what is already written - on a line of their own. */\nexport function appendWords(written: string, words: string): string {\n return written.trim() ? `${written.replace(/\\s+$/, '')}\\n${words}` : words\n}\n"]}
@@ -29,6 +29,7 @@ export declare function pastedTableFile(clipboard: {
29
29
  }, policy: AttachmentPolicy | null | undefined, nameOf: (rows: number) => string): {
30
30
  name: string;
31
31
  text: string;
32
+ table: PastedTable;
32
33
  } | {
33
34
  refusal: PasteRefusal;
34
35
  } | null;
@@ -145,6 +145,6 @@ export function pastedTableFile(clipboard, policy, nameOf) {
145
145
  const maxChars = textCharsOf(policy);
146
146
  if (text.length > maxChars)
147
147
  return { refusal: { code: 'attachment-paste-too-long', rows: table.rows.length, maxChars } };
148
- return { name: `${name.replace(/[\\/:*?"<>|]/g, '_')}.csv`, text };
148
+ return { name: `${name.replace(/[\\/:*?"<>|]/g, '_')}.csv`, text, table };
149
149
  }
150
150
  //# sourceMappingURL=pasted-table.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"pasted-table.js","sourceRoot":"","sources":["../../client/utils/pasted-table.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * A table pasted into the composer, carried as a file (design/ai/data-entry.md §4-1, design/ai/shared-dock.md §9-5).\n *\n * Rows copied from a spreadsheet or a web page are what an AI 입력 is made from most often. Pasted as words they\n * reach the model as something the person said, and the entry card cannot check a row against them nor read a\n * printed total. Carried as a CSV file they are a source like an attached sheet: each row is found on its line.\n *\n * - The HTML of the clipboard is read first when it holds a table - a spreadsheet and a browser both put one there,\n * and it keeps a cell that holds a line break or a tab whole. A web page whose words lie mostly outside the table\n * is not a table paste; it is left as words.\n * - Otherwise the plain text is a table when its lines are tab-separated (a spreadsheet's own text).\n * - The file has the shape of a read sheet: a line naming it, then every row led by its own number.\n * - The limit is on the text, as for a sheet - a table over it is refused, never cut.\n *\n * Pure: the screen hands in the clipboard's strings.\n */\nimport { textCharsOf, type AttachmentPolicy } from './attachment-policy.js'\nimport { csvField } from './spreadsheet-text.js'\n\nexport interface PastedTable {\n rows: string[][]\n columns: number\n}\n\n/** Words outside the table beyond this are a page that holds a table, not a table. */\nconst WORDS_OUTSIDE_A_TABLE = 20\n\nconst ENTITIES: Record<string, string> = { nbsp: ' ', amp: '&', lt: '<', gt: '>', quot: '\"', apos: \"'\", '#39': \"'\" }\n\nfunction decodeEntities(text: string): string {\n return text.replace(/&(#x[0-9a-f]+|#\\d+|[a-z]+\\d*|#39);/gi, (whole, name: string) => {\n const lower = name.toLowerCase()\n if (lower in ENTITIES) return ENTITIES[lower]\n if (lower.startsWith('#x')) return String.fromCodePoint(parseInt(lower.slice(2), 16))\n if (lower.startsWith('#')) return String.fromCodePoint(parseInt(lower.slice(1), 10))\n return whole\n })\n}\n\n/** What a piece of HTML shows as words - a line break and a cell's own breaks become one space. */\nfunction wordsOf(html: string): string {\n return decodeEntities(html.replace(/<br\\s*\\/?>/gi, ' ').replace(/<[^>]*>/g, ''))\n .replace(/\\s+/g, ' ')\n .trim()\n}\n\n/** What the clipboard's HTML shows, without what is never shown - comments, styles, scripts, the head. */\nfunction shownHtml(html: string): string {\n return html\n .replace(/<!--[\\s\\S]*?-->/g, '')\n .replace(/<(style|script|head)\\b[\\s\\S]*?<\\/\\1\\s*>/gi, '')\n}\n\n/** The rows of the tables in the clipboard's HTML, or null when it holds none or is mostly not a table. */\nexport function tableOfHtml(html: string | null | undefined): string[][] | null {\n if (!html || !/<table\\b/i.test(html)) return null\n const shown = shownHtml(html)\n const outside = wordsOf(shown.replace(/<table\\b[\\s\\S]*?<\\/table\\s*>/gi, ' '))\n if (outside.replace(/\\s/g, '').length > WORDS_OUTSIDE_A_TABLE) return null\n\n const rows: string[][] = []\n for (const part of shown.split(/<tr\\b[^>]*>/i).slice(1)) {\n const body = part.split(/<\\/table\\s*>/i)[0]\n const row: string[] = []\n for (const cell of body.matchAll(/<(td|th)\\b([^>]*)>([\\s\\S]*?)(?=<\\/?(?:td|th|tr)\\b|$)/gi)) {\n row.push(wordsOf(cell[3]))\n const span = Number(/\\bcolspan\\s*=\\s*[\"']?(\\d+)/i.exec(cell[2])?.[1] ?? 1)\n for (let more = 1; more < Math.min(span, 100); more++) row.push('')\n }\n if (row.some(text => text !== '')) rows.push(row)\n }\n return rows.length ? rows : null\n}\n\n/** The rows of a spreadsheet's plain text - tab-separated lines, a cell with a break or a tab in quotes. */\nexport function tableOfText(text: string | null | undefined): string[][] | null {\n if (!text || !text.includes('\\t')) return null\n const rows: string[][] = []\n let row: string[] = []\n let cell = ''\n let quoted = false\n for (let at = 0; at < text.length; at++) {\n const c = text[at]\n if (quoted) {\n if (c === '\"' && text[at + 1] === '\"') (cell += '\"'), at++\n else if (c === '\"') quoted = false\n else cell += c\n } else if (c === '\"' && cell === '') quoted = true\n else if (c === '\\t') row.push(cell), (cell = '')\n else if (c === '\\n' || c === '\\r') {\n if (c === '\\r' && text[at + 1] === '\\n') at++\n row.push(cell), rows.push(row), (row = []), (cell = '')\n } else cell += c\n }\n if (cell !== '' || row.length) row.push(cell), rows.push(row)\n const kept = rows.map(one => one.map(text => text.replace(/\\s+/g, ' ').trim())).filter(one => one.some(text => text !== ''))\n /* One tab in a sentence is not a table - most lines must be split into columns. */\n if (kept.filter(one => one.length > 1).length < Math.max(2, Math.ceil(kept.length / 2))) return null\n return kept\n}\n\n/** The table a paste holds, or null for words - at least two rows of at least two columns. */\nexport function pastedTable(clipboard: { html?: string | null; text?: string | null }): PastedTable | null {\n const fromHtml = tableOfHtml(clipboard.html)\n /* A page the HTML shows to be mostly words stays words - its plain text would read the table out of it anyway. */\n if (!fromHtml && clipboard.html && /<table\\b/i.test(clipboard.html)) return null\n for (const rows of [fromHtml, tableOfText(clipboard.text)]) {\n if (!rows) continue\n const trimmed = rows.map(row => {\n let cells = row\n while (cells.length && cells[cells.length - 1] === '') cells = cells.slice(0, -1)\n return cells\n })\n const columns = Math.max(...trimmed.map(row => row.length))\n if (trimmed.length >= 2 && columns >= 2) return { rows: trimmed, columns }\n }\n return null\n}\n\n/** The table as the text of its file: a line naming it, then every row led by its own number. */\nexport function pastedTableText(table: PastedTable, name: string): string {\n const lines = table.rows.map((row, i) => [String(i + 1), ...row].map(csvField).join(','))\n return [`# ${name} · a table the person pasted · the first column is the row number in the pasted table`, ...lines].join('\\n') + '\\n'\n}\n\nexport type PasteRefusal = { code: 'attachment-paste-too-long'; rows: number; maxChars: number }\n\n/**\n * What a paste becomes in a conversation under this policy: a table file, a refusal, or null - the paste stays words\n * (no table in it, or a conversation that takes no text files).\n */\nexport function pastedTableFile(\n clipboard: { html?: string | null; text?: string | null },\n policy: AttachmentPolicy | null | undefined,\n nameOf: (rows: number) => string\n): { name: string; text: string } | { refusal: PasteRefusal } | null {\n if (!policy?.kinds?.some(kind => kind === 'text' || kind === 'spreadsheet')) return null\n const table = pastedTable(clipboard)\n if (!table) return null\n const name = nameOf(table.rows.length)\n const text = pastedTableText(table, name)\n const maxChars = textCharsOf(policy)\n if (text.length > maxChars) return { refusal: { code: 'attachment-paste-too-long', rows: table.rows.length, maxChars } }\n return { name: `${name.replace(/[\\\\/:*?\"<>|]/g, '_')}.csv`, text }\n}\n"]}
1
+ {"version":3,"file":"pasted-table.js","sourceRoot":"","sources":["../../client/utils/pasted-table.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * A table pasted into the composer, carried as a file (design/ai/data-entry.md §4-1, design/ai/shared-dock.md §9-5).\n *\n * Rows copied from a spreadsheet or a web page are what an AI 입력 is made from most often. Pasted as words they\n * reach the model as something the person said, and the entry card cannot check a row against them nor read a\n * printed total. Carried as a CSV file they are a source like an attached sheet: each row is found on its line.\n *\n * - The HTML of the clipboard is read first when it holds a table - a spreadsheet and a browser both put one there,\n * and it keeps a cell that holds a line break or a tab whole. A web page whose words lie mostly outside the table\n * is not a table paste; it is left as words.\n * - Otherwise the plain text is a table when its lines are tab-separated (a spreadsheet's own text).\n * - The file has the shape of a read sheet: a line naming it, then every row led by its own number.\n * - The limit is on the text, as for a sheet - a table over it is refused, never cut.\n *\n * Pure: the screen hands in the clipboard's strings.\n */\nimport { textCharsOf, type AttachmentPolicy } from './attachment-policy.js'\nimport { csvField } from './spreadsheet-text.js'\n\nexport interface PastedTable {\n rows: string[][]\n columns: number\n}\n\n/** Words outside the table beyond this are a page that holds a table, not a table. */\nconst WORDS_OUTSIDE_A_TABLE = 20\n\nconst ENTITIES: Record<string, string> = { nbsp: ' ', amp: '&', lt: '<', gt: '>', quot: '\"', apos: \"'\", '#39': \"'\" }\n\nfunction decodeEntities(text: string): string {\n return text.replace(/&(#x[0-9a-f]+|#\\d+|[a-z]+\\d*|#39);/gi, (whole, name: string) => {\n const lower = name.toLowerCase()\n if (lower in ENTITIES) return ENTITIES[lower]\n if (lower.startsWith('#x')) return String.fromCodePoint(parseInt(lower.slice(2), 16))\n if (lower.startsWith('#')) return String.fromCodePoint(parseInt(lower.slice(1), 10))\n return whole\n })\n}\n\n/** What a piece of HTML shows as words - a line break and a cell's own breaks become one space. */\nfunction wordsOf(html: string): string {\n return decodeEntities(html.replace(/<br\\s*\\/?>/gi, ' ').replace(/<[^>]*>/g, ''))\n .replace(/\\s+/g, ' ')\n .trim()\n}\n\n/** What the clipboard's HTML shows, without what is never shown - comments, styles, scripts, the head. */\nfunction shownHtml(html: string): string {\n return html\n .replace(/<!--[\\s\\S]*?-->/g, '')\n .replace(/<(style|script|head)\\b[\\s\\S]*?<\\/\\1\\s*>/gi, '')\n}\n\n/** The rows of the tables in the clipboard's HTML, or null when it holds none or is mostly not a table. */\nexport function tableOfHtml(html: string | null | undefined): string[][] | null {\n if (!html || !/<table\\b/i.test(html)) return null\n const shown = shownHtml(html)\n const outside = wordsOf(shown.replace(/<table\\b[\\s\\S]*?<\\/table\\s*>/gi, ' '))\n if (outside.replace(/\\s/g, '').length > WORDS_OUTSIDE_A_TABLE) return null\n\n const rows: string[][] = []\n for (const part of shown.split(/<tr\\b[^>]*>/i).slice(1)) {\n const body = part.split(/<\\/table\\s*>/i)[0]\n const row: string[] = []\n for (const cell of body.matchAll(/<(td|th)\\b([^>]*)>([\\s\\S]*?)(?=<\\/?(?:td|th|tr)\\b|$)/gi)) {\n row.push(wordsOf(cell[3]))\n const span = Number(/\\bcolspan\\s*=\\s*[\"']?(\\d+)/i.exec(cell[2])?.[1] ?? 1)\n for (let more = 1; more < Math.min(span, 100); more++) row.push('')\n }\n if (row.some(text => text !== '')) rows.push(row)\n }\n return rows.length ? rows : null\n}\n\n/** The rows of a spreadsheet's plain text - tab-separated lines, a cell with a break or a tab in quotes. */\nexport function tableOfText(text: string | null | undefined): string[][] | null {\n if (!text || !text.includes('\\t')) return null\n const rows: string[][] = []\n let row: string[] = []\n let cell = ''\n let quoted = false\n for (let at = 0; at < text.length; at++) {\n const c = text[at]\n if (quoted) {\n if (c === '\"' && text[at + 1] === '\"') (cell += '\"'), at++\n else if (c === '\"') quoted = false\n else cell += c\n } else if (c === '\"' && cell === '') quoted = true\n else if (c === '\\t') row.push(cell), (cell = '')\n else if (c === '\\n' || c === '\\r') {\n if (c === '\\r' && text[at + 1] === '\\n') at++\n row.push(cell), rows.push(row), (row = []), (cell = '')\n } else cell += c\n }\n if (cell !== '' || row.length) row.push(cell), rows.push(row)\n const kept = rows.map(one => one.map(text => text.replace(/\\s+/g, ' ').trim())).filter(one => one.some(text => text !== ''))\n /* One tab in a sentence is not a table - most lines must be split into columns. */\n if (kept.filter(one => one.length > 1).length < Math.max(2, Math.ceil(kept.length / 2))) return null\n return kept\n}\n\n/** The table a paste holds, or null for words - at least two rows of at least two columns. */\nexport function pastedTable(clipboard: { html?: string | null; text?: string | null }): PastedTable | null {\n const fromHtml = tableOfHtml(clipboard.html)\n /* A page the HTML shows to be mostly words stays words - its plain text would read the table out of it anyway. */\n if (!fromHtml && clipboard.html && /<table\\b/i.test(clipboard.html)) return null\n for (const rows of [fromHtml, tableOfText(clipboard.text)]) {\n if (!rows) continue\n const trimmed = rows.map(row => {\n let cells = row\n while (cells.length && cells[cells.length - 1] === '') cells = cells.slice(0, -1)\n return cells\n })\n const columns = Math.max(...trimmed.map(row => row.length))\n if (trimmed.length >= 2 && columns >= 2) return { rows: trimmed, columns }\n }\n return null\n}\n\n/** The table as the text of its file: a line naming it, then every row led by its own number. */\nexport function pastedTableText(table: PastedTable, name: string): string {\n const lines = table.rows.map((row, i) => [String(i + 1), ...row].map(csvField).join(','))\n return [`# ${name} · a table the person pasted · the first column is the row number in the pasted table`, ...lines].join('\\n') + '\\n'\n}\n\nexport type PasteRefusal = { code: 'attachment-paste-too-long'; rows: number; maxChars: number }\n\n/**\n * What a paste becomes in a conversation under this policy: a table file, a refusal, or null - the paste stays words\n * (no table in it, or a conversation that takes no text files).\n */\nexport function pastedTableFile(\n clipboard: { html?: string | null; text?: string | null },\n policy: AttachmentPolicy | null | undefined,\n nameOf: (rows: number) => string\n): { name: string; text: string; table: PastedTable } | { refusal: PasteRefusal } | null {\n if (!policy?.kinds?.some(kind => kind === 'text' || kind === 'spreadsheet')) return null\n const table = pastedTable(clipboard)\n if (!table) return null\n const name = nameOf(table.rows.length)\n const text = pastedTableText(table, name)\n const maxChars = textCharsOf(policy)\n if (text.length > maxChars) return { refusal: { code: 'attachment-paste-too-long', rows: table.rows.length, maxChars } }\n return { name: `${name.replace(/[\\\\/:*?\"<>|]/g, '_')}.csv`, text, table }\n}\n"]}