@burdenoff/microfe-bigconsole 2026.917.3 → 2026.920.1

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.
@@ -1 +1 @@
1
- {"version":3,"file":"assistantPrompt.js","names":[],"sources":["../../../src/bigconsole/assistant/assistantPrompt.ts"],"sourcesContent":["/**\n * Soft confirm-before-mutate guardrail for the assistant's write mode, plus the\n * language and dictation instructions.\n *\n * The in-sandbox agent executes mutations autonomously — there is no hard\n * server-side confirmation — so the describe-and-confirm instruction is\n * prepended to the user's TURN, not to the session system prompt. Sending it as\n * `system` would REPLACE the whole mode prompt and silently delete the agent's\n * api-calls behaviour along with it.\n *\n * This is ADVISORY. The dependable guard is the host-side consent dialog plus\n * the admin gate.\n *\n * Ported from FluidGrids' `assistantPrompt.ts` (itself ported from\n * microfe-vibecontrols), with BigConsole nouns. The composition and the\n * delimiter discipline are unchanged on purpose — see `buildPromptForMode`.\n */\n\n/**\n * ★ The TRANSPORT's mode type, not BigConsole's narrower list. The shared panel\n * calls `buildPrompt` with whatever mode its store holds, which can be one this\n * product no longer offers (a restored conversation, or a build that dropped a\n * mode) until the panel resets it, so the parameter has to accept every mode the\n * panel can send.\n */\nimport type { AssistantMode } from '@burdenoff/fe-libs/shared/assistant/types';\n\n/**\n * Marks where our instructions stop and the user's words begin. Exactly one may\n * exist in a prompt, and it must be last — see `buildPromptForMode`.\n */\nexport const USER_REQUEST_DELIMITER = '--- User request ---';\n\nexport const API_CALLS_CONFIRM_PREAMBLE = [\n 'You are in api-calls mode for BigConsole and can run GraphQL queries and mutations on the user behalf.',\n 'Before performing ANY write (create, update, delete — any mutation):',\n '1. State exactly what you are about to change (entity, fields, and values).',\n '2. Ask the user to confirm, and WAIT for an explicit \"yes\"/\"confirm\" before executing it.',\n 'Read-only queries may run without confirmation. Confirm each write separately; never batch mutations behind a single confirmation.',\n '',\n USER_REQUEST_DELIMITER,\n].join('\\n');\n\n/**\n * Ask for the reply in the user's language.\n *\n * ★ Nothing else does. The sandbox system prompt carries no language\n * instruction and the grounding documentation is entirely in English, so every\n * force in the session pushes the answer towards English regardless of what the\n * user wrote. The model's tendency to mirror its input is the only thing\n * resisting that, and it loses.\n *\n * The second clause is the one that matters: **translate the docs, do not\n * switch to their language.** Without it a grounded answer drifts back to\n * English mid-reply simply because the source material is English.\n */\nfunction buildLanguagePreamble(languageName: string, isExplicitChoice: boolean): string {\n return [\n isExplicitChoice\n ? `Reply in ${languageName}. The user has explicitly chosen this language, so use it even if they write to you in another one.`\n : `Reply in ${languageName}, matching the language the user wrote in.`,\n `Translate any English documentation you ground your answer in into ${languageName} rather than switching to English to quote it.`,\n 'Keep identifiers, code, CLI flags, API field names and product nouns in their original form — translating them would make them wrong.',\n ].join('\\n');\n}\n\n/**\n * Told to the model when the turn was SPOKEN rather than typed.\n *\n * ★ Speech recognition does not fail uniformly — it fails on specific words,\n * and in BigConsole the ones it fails on are predictable: data sink names,\n * dashboard ids, parser names and column headers. A recogniser has no model for\n * a dashboard id read aloud, so a sentence comes back mostly right with the\n * load-bearing words wrong.\n *\n * ★ The refusal to guess is the important half. Charitable interpretation is\n * right for a mangled verb and wrong for a mangled IDENTIFIER: acting on a\n * misheard dashboard id edits somebody else's dashboard. One extra question\n * costs a few seconds.\n */\nconst DICTATION_PREAMBLE = [\n 'This turn was DICTATED, so treat unusual words as probable speech-recognition errors rather than as what the user meant.',\n 'Repair obvious mis-hearings of technical terms, product nouns, column names and command names using the surrounding context.',\n 'Do NOT guess at a mis-heard data sink name, dashboard id, parser name, widget id or column header — ask which one the user meant before acting on it.',\n].join('\\n');\n\n/** A dictated turn is a SPOKEN conversation, so the reply is spoken too. */\nconst VOICE_REPLY_PREAMBLE = [\n 'Your reply will be read aloud. Write it to be heard, not scanned:',\n 'short sentences, no markdown syntax, no tables, and no code blocks unless the user asked for code.',\n].join('\\n');\n\n/**\n * Compose the turn actually sent to the agent.\n *\n * Ordering is deliberate. Dictation goes AFTER the language lines and before the\n * delimiter: it is an instruction about the user's text, so it belongs with the\n * instructions rather than inside them.\n *\n * ★★ `USER_REQUEST_DELIMITER` is emitted exactly once, here. The api-calls\n * preamble ends with its own copy, so it must not be added again — a second one\n * mid-prompt leaves the safety instructions sitting *after* a \"User request\"\n * marker, i.e. presented to the model as though the user had written them.\n *\n * ★ No branch for Ask (`assistant`). It is read-only — its sandbox boots with\n * `AI_ASSISTANT_READONLY=1` — so there is no write to confirm, and inventing a\n * confirmation preamble for it would be copy that lies about what the mode does.\n */\nexport function buildPromptForMode(\n mode: AssistantMode,\n content: string,\n languageName?: string,\n isExplicitChoice = false,\n dictated = false\n): string {\n const preamble = [\n languageName ? buildLanguagePreamble(languageName, isExplicitChoice) : '',\n dictated ? DICTATION_PREAMBLE : '',\n dictated ? VOICE_REPLY_PREAMBLE : '',\n ]\n .filter(Boolean)\n .join('\\n\\n');\n const language = preamble ? `${preamble}\\n\\n` : '';\n if (mode === 'api-calls') return `${language}${API_CALLS_CONFIRM_PREAMBLE}\\n${content}`;\n return language ? `${language}${USER_REQUEST_DELIMITER}\\n${content}` : content;\n}\n"],"mappings":";AA+BA,IAAa,IAAyB,wBAEzB,IAA6B;CACxC;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC,KAAK,KAAK;AAeZ,SAAS,EAAsB,GAAsB,GAAmC;AACtF,QAAO;EACL,IACI,YAAY,EAAa,uGACzB,YAAY,EAAa;EAC7B,sEAAsE,EAAa;EACnF;EACD,CAAC,KAAK,KAAK;;AAiBd,IAAM,IAAqB;CACzB;CACA;CACA;CACD,CAAC,KAAK,KAAK,EAGN,IAAuB,CAC3B,qEACA,qGACD,CAAC,KAAK,KAAK;AAkBZ,SAAgB,EACd,GACA,GACA,GACA,IAAmB,IACnB,IAAW,IACH;CACR,IAAM,IAAW;EACf,IAAe,EAAsB,GAAc,EAAiB,GAAG;EACvE,IAAW,IAAqB;EAChC,IAAW,IAAuB;EACnC,CACE,OAAO,QAAQ,CACf,KAAK,OAAO,EACT,IAAW,IAAW,GAAG,EAAS,QAAQ;AAEhD,QADI,MAAS,cAAoB,GAAG,IAAW,EAA2B,IAAI,MACvE,IAAW,GAAG,IAAW,EAAuB,IAAI,MAAY"}
1
+ {"version":3,"file":"assistantPrompt.js","names":[],"sources":["../../../src/bigconsole/assistant/assistantPrompt.ts"],"sourcesContent":["/**\n * Soft confirm-before-mutate guardrail for the assistant's write mode, plus the\n * language and dictation instructions.\n *\n * The in-sandbox agent executes mutations autonomously — there is no hard\n * server-side confirmation — so the describe-and-confirm instruction is\n * prepended to the user's TURN, not to the session system prompt. Sending it as\n * `system` would REPLACE the whole mode prompt and silently delete the agent's\n * api-calls behaviour along with it.\n *\n * This is ADVISORY. The dependable guard is the host-side consent dialog plus\n * the admin gate.\n *\n * Ported from FluidGrids' `assistantPrompt.ts` (itself ported from\n * microfe-vibecontrols), with BigConsole nouns. The composition and the\n * delimiter discipline are unchanged on purpose — see `buildPromptForMode`.\n */\n\n/**\n * ★ The TRANSPORT's mode type, not BigConsole's narrower list. The shared panel\n * calls `buildPrompt` with whatever mode its store holds, which can be one this\n * product no longer offers (a restored conversation, or a build that dropped a\n * mode) until the panel resets it, so the parameter has to accept every mode the\n * panel can send.\n */\nimport type { AssistantMode } from '@burdenoff/fe-libs/shared/assistant/types';\n\n/**\n * Marks where our instructions stop and the user's words begin. Exactly one may\n * exist in a prompt, and it must be last — see `buildPromptForMode`.\n */\nexport const USER_REQUEST_DELIMITER = '--- User request ---';\n\nexport const API_CALLS_CONFIRM_PREAMBLE = [\n 'You are in api-calls mode for BigConsole and can run GraphQL queries and mutations on the user behalf.',\n 'Before performing ANY write (create, update, delete — any mutation):',\n '1. State exactly what you are about to change (entity, fields, and values).',\n '2. Ask the user to confirm, and WAIT for an explicit \"yes\"/\"confirm\" before executing it.',\n 'Read-only queries may run without confirmation. Confirm each write separately; never batch mutations behind a single confirmation.',\n '',\n USER_REQUEST_DELIMITER,\n].join('\\n');\n\n/**\n * Ask for the reply in the user's language.\n *\n * ★ Nothing else does. The sandbox system prompt carries no language\n * instruction and the grounding documentation is entirely in English, so every\n * force in the session pushes the answer towards English regardless of what the\n * user wrote. The model's tendency to mirror its input is the only thing\n * resisting that, and it loses.\n *\n * The second clause is the one that matters: **translate the docs, do not\n * switch to their language.** Without it a grounded answer drifts back to\n * English mid-reply simply because the source material is English.\n */\nfunction buildLanguagePreamble(languageName: string, isExplicitChoice: boolean): string {\n return [\n isExplicitChoice\n ? `Reply in ${languageName}. The user has explicitly chosen this language, so use it even if they write to you in another one.`\n : `Reply in ${languageName}, matching the language the user wrote in.`,\n `Translate any English documentation you ground your answer in into ${languageName} rather than switching to English to quote it.`,\n 'Keep identifiers, code, CLI flags, API field names and product nouns in their original form — translating them would make them wrong.',\n ].join('\\n');\n}\n\n/**\n * Told to the model when the turn was SPOKEN rather than typed.\n *\n * ★ Speech recognition does not fail uniformly — it fails on specific words,\n * and in BigConsole the ones it fails on are predictable: data sink names,\n * dashboard ids, parser names and column headers. A recogniser has no model for\n * a dashboard id read aloud, so a sentence comes back mostly right with the\n * load-bearing words wrong.\n *\n * ★ The refusal to guess is the important half. Charitable interpretation is\n * right for a mangled verb and wrong for a mangled IDENTIFIER: acting on a\n * misheard dashboard id edits somebody else's dashboard. One extra question\n * costs a few seconds.\n */\nconst DICTATION_PREAMBLE = [\n 'This turn was DICTATED, so treat unusual words as probable speech-recognition errors rather than as what the user meant.',\n 'Repair obvious mis-hearings of technical terms, product nouns, column names and command names using the surrounding context.',\n 'Do NOT guess at a mis-heard data sink name, dashboard id, parser name, widget id or column header — ask which one the user meant before acting on it.',\n].join('\\n');\n\n/**\n * A dictated turn is a SPOKEN conversation, so the reply is spoken too.\n *\n * ★★ REACHABLE FROM ASK ONLY, from fe-libs 2026.920.x. `writeModeSendRefusal`\n * refuses a dictated send in any `writeCapable` mode before a prompt is\n * composed (\"Voice can't be used in a mode that can change your data\"), and\n * Build is write-capable — so `dictated` and `api-calls` no longer occur\n * together. Both preambles still apply to Ask, which is where dictation lives.\n */\nconst VOICE_REPLY_PREAMBLE = [\n 'Your reply will be read aloud. Write it to be heard, not scanned:',\n 'short sentences, no markdown syntax, no tables, and no code blocks unless the user asked for code.',\n].join('\\n');\n\n/**\n * Compose the turn actually sent to the agent.\n *\n * Ordering is deliberate. Dictation goes AFTER the language lines and before the\n * delimiter: it is an instruction about the user's text, so it belongs with the\n * instructions rather than inside them.\n *\n * ★★ `USER_REQUEST_DELIMITER` is emitted exactly once, here. The api-calls\n * preamble ends with its own copy, so it must not be added again — a second one\n * mid-prompt leaves the safety instructions sitting *after* a \"User request\"\n * marker, i.e. presented to the model as though the user had written them.\n *\n * ★ No branch for Ask (`assistant`). It is read-only — its sandbox boots with\n * `AI_ASSISTANT_READONLY=1` — so there is no write to confirm, and inventing a\n * confirmation preamble for it would be copy that lies about what the mode does.\n */\nexport function buildPromptForMode(\n mode: AssistantMode,\n content: string,\n languageName?: string,\n isExplicitChoice = false,\n dictated = false\n): string {\n const preamble = [\n languageName ? buildLanguagePreamble(languageName, isExplicitChoice) : '',\n dictated ? DICTATION_PREAMBLE : '',\n dictated ? VOICE_REPLY_PREAMBLE : '',\n ]\n .filter(Boolean)\n .join('\\n\\n');\n const language = preamble ? `${preamble}\\n\\n` : '';\n if (mode === 'api-calls') return `${language}${API_CALLS_CONFIRM_PREAMBLE}\\n${content}`;\n return language ? `${language}${USER_REQUEST_DELIMITER}\\n${content}` : content;\n}\n"],"mappings":";AA+BA,IAAa,IAAyB,wBAEzB,IAA6B;CACxC;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC,KAAK,KAAK;AAeZ,SAAS,EAAsB,GAAsB,GAAmC;AACtF,QAAO;EACL,IACI,YAAY,EAAa,uGACzB,YAAY,EAAa;EAC7B,sEAAsE,EAAa;EACnF;EACD,CAAC,KAAK,KAAK;;AAiBd,IAAM,IAAqB;CACzB;CACA;CACA;CACD,CAAC,KAAK,KAAK,EAWN,IAAuB,CAC3B,qEACA,qGACD,CAAC,KAAK,KAAK;AAkBZ,SAAgB,EACd,GACA,GACA,GACA,IAAmB,IACnB,IAAW,IACH;CACR,IAAM,IAAW;EACf,IAAe,EAAsB,GAAc,EAAiB,GAAG;EACvE,IAAW,IAAqB;EAChC,IAAW,IAAuB;EACnC,CACE,OAAO,QAAQ,CACf,KAAK,OAAO,EACT,IAAW,IAAW,GAAG,EAAS,QAAQ;AAEhD,QADI,MAAS,cAAoB,GAAG,IAAW,EAA2B,IAAI,MACvE,IAAW,GAAG,IAAW,EAAuB,IAAI,MAAY"}
@@ -1,17 +1,17 @@
1
1
  //#region src/bigconsole/assistant/attachmentExtract.ts
2
- var e = 1e5, t = 200;
3
- function n(e) {
2
+ var e = 1e5;
3
+ function t(e) {
4
4
  let t = e.name.toLowerCase();
5
5
  return t.endsWith(".json") || t.endsWith(".csv") || t.endsWith(".txt") || e.type === "application/json" || e.type === "text/csv" || e.type.startsWith("text/");
6
6
  }
7
- async function r(e) {
8
- let t = e.name.toLowerCase();
9
- if (n(e)) return await e.text();
10
- if (t.endsWith(".xlsx") || t.endsWith(".xls")) {
7
+ async function n(e) {
8
+ let n = e.name.toLowerCase();
9
+ if (t(e)) return await e.text();
10
+ if (n.endsWith(".xlsx") || n.endsWith(".xls")) {
11
11
  let t = await import("../../node_modules/@e965/xlsx/xlsx.js"), n = await e.arrayBuffer(), r = t.read(n, { type: "array" }), i = r.SheetNames[0];
12
12
  return i ? t.utils.sheet_to_csv(r.Sheets[i]) : "";
13
13
  }
14
- if (t.endsWith(".pdf") || e.type === "application/pdf") {
14
+ if (n.endsWith(".pdf") || e.type === "application/pdf") {
15
15
  let t = await import("../../node_modules/pdfjs-dist/build/pdf.js"), n = await e.arrayBuffer(), r = await t.getDocument({ data: new Uint8Array(n) }).promise, i = [], a = Math.min(r.numPages, 20);
16
16
  for (let e = 1; e <= a; e += 1) {
17
17
  let t = await (await r.getPage(e)).getTextContent();
@@ -21,30 +21,30 @@ async function r(e) {
21
21
  }
22
22
  return await e.text();
23
23
  }
24
- function i(n) {
25
- let r = n, i = !1, a = r.split(/\r?\n/);
26
- return a.length > t && (r = a.slice(0, t).join("\n"), i = !0), r.length > e && (r = r.slice(0, e), i = !0), {
27
- text: r,
28
- truncated: i
24
+ function r(t) {
25
+ let n = t, r = !1, i = n.split(/\r?\n/);
26
+ return i.length > 200 && (n = i.slice(0, 200).join("\n"), r = !0), n.length > 1e5 && (n = n.slice(0, e), r = !0), {
27
+ text: n,
28
+ truncated: r
29
29
  };
30
30
  }
31
- async function a(n) {
32
- let a = [];
33
- for (let o of n) {
34
- let n;
31
+ async function i(t) {
32
+ let i = [];
33
+ for (let a of t) {
34
+ let t;
35
35
  try {
36
- n = await r(o);
36
+ t = await n(a);
37
37
  } catch (e) {
38
38
  let t = e instanceof Error ? e.message : "unknown error";
39
- a.push(`--- Attached file "${o.name}" could not be auto-read (${t}). Please paste its rows here instead. ---`);
39
+ i.push(`--- Attached file "${a.name}" could not be auto-read (${t}). Please paste its rows here instead. ---`);
40
40
  continue;
41
41
  }
42
- let { text: s, truncated: c } = i(n.trim()), l = c ? ` (sample — first ${t} rows / ~${Math.round(e / 1e3)}KB)` : "";
43
- a.push(`--- Attached data source: ${o.name}${l} ---\n${s}\n--- end ${o.name} ---`);
42
+ let { text: o, truncated: s } = r(t.trim()), c = s ? ` (sample — first 200 rows / ~${Math.round(e / 1e3)}KB)` : "";
43
+ i.push(`--- Attached data source: ${a.name}${c} ---\n${o}\n--- end ${a.name} ---`);
44
44
  }
45
- return a.join("\n\n");
45
+ return i.join("\n\n");
46
46
  }
47
47
  //#endregion
48
- export { a as buildAttachmentBlock };
48
+ export { i as buildAttachmentBlock };
49
49
 
50
50
  //# sourceMappingURL=attachmentExtract.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"attachmentExtract.js","names":[],"sources":["../../../src/bigconsole/assistant/attachmentExtract.ts"],"sourcesContent":["/**\n * Client-side extraction of an uploaded data file into a capped text/rows\n * preview that gets folded into the assistant prompt.\n *\n * Lives in the BigConsole product layer (not fe-libs) because it pulls in the\n * xlsx / pdfjs parsers — fe-libs stays parsing-free and only carries the raw\n * `File`s through `AssistantSendArgs.attachments`. Heavy parsers are loaded via\n * dynamic `import()` so they code-split and only download when a file is\n * actually attached.\n *\n * The agent receives a CAPPED preview — enough to design a DataSink schema from,\n * not the whole file — because the payload rides the `proxySandboxRequest`\n * gateway call (bounded execution budget) and the model has a context limit.\n */\n\nconst MAX_CHARS = 100_000; // ~100 KB of text per file\nconst MAX_ROWS = 200;\n\nfunction isTextLike(file: File): boolean {\n const name = file.name.toLowerCase();\n return (\n name.endsWith('.json') ||\n name.endsWith('.csv') ||\n name.endsWith('.txt') ||\n file.type === 'application/json' ||\n file.type === 'text/csv' ||\n file.type.startsWith('text/')\n );\n}\n\nasync function extractOne(file: File): Promise<string> {\n const name = file.name.toLowerCase();\n\n // JSON / CSV / plain text — native, no deps.\n if (isTextLike(file)) {\n return await file.text();\n }\n\n // Excel — SheetJS, dynamically imported. First sheet → CSV (compact + agent-friendly).\n if (name.endsWith('.xlsx') || name.endsWith('.xls')) {\n const XLSX = await import('@e965/xlsx');\n const buf = await file.arrayBuffer();\n const wb = XLSX.read(buf, { type: 'array' });\n const first = wb.SheetNames[0];\n if (!first) return '';\n return XLSX.utils.sheet_to_csv(wb.Sheets[first]);\n }\n\n // PDF — best-effort text-layer extraction via pdfjs (dynamically imported).\n // Unreliable for tabular data and useless for scanned PDFs; the caller shows a\n // graceful note if this throws. NOTE: pdfjs worker setup is Vite-specific and\n // must be verified against a running build.\n if (name.endsWith('.pdf') || file.type === 'application/pdf') {\n const pdfjs = await import('pdfjs-dist');\n const buf = await file.arrayBuffer();\n const doc = await pdfjs.getDocument({ data: new Uint8Array(buf) }).promise;\n const parts: string[] = [];\n const pages = Math.min(doc.numPages, 20);\n for (let page = 1; page <= pages; page += 1) {\n const content = await (await doc.getPage(page)).getTextContent();\n parts.push(content.items.map((item) => ('str' in item ? item.str : '')).join(' '));\n }\n return parts.join('\\n');\n }\n\n // Unknown extension → best-effort text.\n return await file.text();\n}\n\n/** Keep the first MAX_ROWS lines / MAX_CHARS chars so a huge file can't blow the\n * prompt. Returns whether anything was dropped so the agent knows it's a sample. */\nfunction limit(text: string): { text: string; truncated: boolean } {\n let out = text;\n let truncated = false;\n const lines = out.split(/\\r?\\n/);\n if (lines.length > MAX_ROWS) {\n out = lines.slice(0, MAX_ROWS).join('\\n');\n truncated = true;\n }\n if (out.length > MAX_CHARS) {\n out = out.slice(0, MAX_CHARS);\n truncated = true;\n }\n return { text: out, truncated };\n}\n\n/**\n * Extract every attachment and format them as fenced \"Attached data source\"\n * blocks the agent can read and turn into a DataSink. Files that fail to parse\n * (e.g. a scanned PDF) produce a short note rather than aborting the send.\n */\nexport async function buildAttachmentBlock(files: File[]): Promise<string> {\n const blocks: string[] = [];\n for (const file of files) {\n let raw: string;\n try {\n raw = await extractOne(file);\n } catch (err) {\n const reason = err instanceof Error ? err.message : 'unknown error';\n blocks.push(\n `--- Attached file \"${file.name}\" could not be auto-read (${reason}). ` +\n `Please paste its rows here instead. ---`\n );\n continue;\n }\n const { text, truncated } = limit(raw.trim());\n const note = truncated ? ` (sample — first ${MAX_ROWS} rows / ~${Math.round(MAX_CHARS / 1000)}KB)` : '';\n blocks.push(`--- Attached data source: ${file.name}${note} ---\\n${text}\\n--- end ${file.name} ---`);\n }\n return blocks.join('\\n\\n');\n}\n"],"mappings":";AAeA,IAAM,IAAY,KACZ,IAAW;AAEjB,SAAS,EAAW,GAAqB;CACvC,IAAM,IAAO,EAAK,KAAK,aAAa;AACpC,QACE,EAAK,SAAS,QAAQ,IACtB,EAAK,SAAS,OAAO,IACrB,EAAK,SAAS,OAAO,IACrB,EAAK,SAAS,sBACd,EAAK,SAAS,cACd,EAAK,KAAK,WAAW,QAAQ;;AAIjC,eAAe,EAAW,GAA6B;CACrD,IAAM,IAAO,EAAK,KAAK,aAAa;AAGpC,KAAI,EAAW,EAAK,CAClB,QAAO,MAAM,EAAK,MAAM;AAI1B,KAAI,EAAK,SAAS,QAAQ,IAAI,EAAK,SAAS,OAAO,EAAE;EACnD,IAAM,IAAO,MAAM,OAAO,0CACpB,IAAM,MAAM,EAAK,aAAa,EAC9B,IAAK,EAAK,KAAK,GAAK,EAAE,MAAM,SAAS,CAAC,EACtC,IAAQ,EAAG,WAAW;AAE5B,SADK,IACE,EAAK,MAAM,aAAa,EAAG,OAAO,GAAO,GAD7B;;AAQrB,KAAI,EAAK,SAAS,OAAO,IAAI,EAAK,SAAS,mBAAmB;EAC5D,IAAM,IAAQ,MAAM,OAAO,+CACrB,IAAM,MAAM,EAAK,aAAa,EAC9B,IAAM,MAAM,EAAM,YAAY,EAAE,MAAM,IAAI,WAAW,EAAI,EAAE,CAAC,CAAC,SAC7D,IAAkB,EAAE,EACpB,IAAQ,KAAK,IAAI,EAAI,UAAU,GAAG;AACxC,OAAK,IAAI,IAAO,GAAG,KAAQ,GAAO,KAAQ,GAAG;GAC3C,IAAM,IAAU,OAAO,MAAM,EAAI,QAAQ,EAAK,EAAE,gBAAgB;AAChE,KAAM,KAAK,EAAQ,MAAM,KAAK,MAAU,SAAS,IAAO,EAAK,MAAM,GAAI,CAAC,KAAK,IAAI,CAAC;;AAEpF,SAAO,EAAM,KAAK,KAAK;;AAIzB,QAAO,MAAM,EAAK,MAAM;;AAK1B,SAAS,EAAM,GAAoD;CACjE,IAAI,IAAM,GACN,IAAY,IACV,IAAQ,EAAI,MAAM,QAAQ;AAShC,QARI,EAAM,SAAS,MACjB,IAAM,EAAM,MAAM,GAAG,EAAS,CAAC,KAAK,KAAK,EACzC,IAAY,KAEV,EAAI,SAAS,MACf,IAAM,EAAI,MAAM,GAAG,EAAU,EAC7B,IAAY,KAEP;EAAE,MAAM;EAAK;EAAW;;AAQjC,eAAsB,EAAqB,GAAgC;CACzE,IAAM,IAAmB,EAAE;AAC3B,MAAK,IAAM,KAAQ,GAAO;EACxB,IAAI;AACJ,MAAI;AACF,OAAM,MAAM,EAAW,EAAK;WACrB,GAAK;GACZ,IAAM,IAAS,aAAe,QAAQ,EAAI,UAAU;AACpD,KAAO,KACL,sBAAsB,EAAK,KAAK,4BAA4B,EAAO,4CAEpE;AACD;;EAEF,IAAM,EAAE,SAAM,iBAAc,EAAM,EAAI,MAAM,CAAC,EACvC,IAAO,IAAY,oBAAoB,EAAS,WAAW,KAAK,MAAM,IAAY,IAAK,CAAC,OAAO;AACrG,IAAO,KAAK,6BAA6B,EAAK,OAAO,EAAK,QAAQ,EAAK,YAAY,EAAK,KAAK,MAAM;;AAErG,QAAO,EAAO,KAAK,OAAO"}
1
+ {"version":3,"file":"attachmentExtract.js","names":[],"sources":["../../../src/bigconsole/assistant/attachmentExtract.ts"],"sourcesContent":["/**\n * Client-side extraction of an uploaded data file into a capped text/rows\n * preview that gets folded into the assistant prompt.\n *\n * Lives in the BigConsole product layer (not fe-libs) because it pulls in the\n * xlsx / pdfjs parsers — fe-libs stays parsing-free and only carries the raw\n * `File`s through `AssistantSendArgs.attachments`. Heavy parsers are loaded via\n * dynamic `import()` so they code-split and only download when a file is\n * actually attached.\n *\n * The agent receives a CAPPED preview — enough to design a DataSink schema from,\n * not the whole file — because the payload rides the `proxySandboxRequest`\n * gateway call (bounded execution budget) and the model has a context limit.\n */\n\n/**\n * Characters of ONE file's text this widget folds into the prompt: ~100 KB.\n *\n * ★★ A CEILING THAT ALMOST NEVER BINDS, and exported so the seam can say so\n * out loud. `MAX_ROWS` is applied FIRST and wins for anything row-oriented: a\n * 200-row slice of a CSV is a few KB, so what this widget has actually been\n * sending is nowhere near 100,000 characters. Only a file with very long lines\n * — one-line JSON, a wide log — ever reaches this number.\n *\n * ★★ So `modes.ts` DELIBERATELY DOES NOT MATCH IT. The shared panel has no row\n * cap, and fe-libs spends a turn's inline budget greedily in attachment order,\n * so asking for 100,000 a file there would let two files eat the whole turn and\n * drop the third (BOFF-7532). `Build` asks for 15,155 instead — derived from\n * the BYTE envelope fe-libs enforces, not from this ceiling — which still\n * delivers MORE rows of a CSV than `MAX_ROWS` below.\n * `tests/assistantSeam.test.ts` guards that direction in rows actually served,\n * measured against the installed fe-libs; when this legacy transport goes,\n * delete the guard with it.\n */\nexport const MAX_INLINE_CHARS_PER_FILE = 100_000;\n\n/**\n * Rows of ONE row-oriented file this widget folds into the prompt.\n *\n * ★★ THE CAP THAT ACTUALLY BOUND, which is why it is exported: it, not\n * `MAX_INLINE_CHARS_PER_FILE`, is what the seam's parity guard compares\n * against. 200 rows of a CSV is a few KB, nowhere near 100,000 characters.\n */\nexport const MAX_ROWS = 200;\n\nfunction isTextLike(file: File): boolean {\n const name = file.name.toLowerCase();\n return (\n name.endsWith('.json') ||\n name.endsWith('.csv') ||\n name.endsWith('.txt') ||\n file.type === 'application/json' ||\n file.type === 'text/csv' ||\n file.type.startsWith('text/')\n );\n}\n\nasync function extractOne(file: File): Promise<string> {\n const name = file.name.toLowerCase();\n\n // JSON / CSV / plain text — native, no deps.\n if (isTextLike(file)) {\n return await file.text();\n }\n\n // Excel — SheetJS, dynamically imported. First sheet → CSV (compact + agent-friendly).\n if (name.endsWith('.xlsx') || name.endsWith('.xls')) {\n const XLSX = await import('@e965/xlsx');\n const buf = await file.arrayBuffer();\n const wb = XLSX.read(buf, { type: 'array' });\n const first = wb.SheetNames[0];\n if (!first) return '';\n return XLSX.utils.sheet_to_csv(wb.Sheets[first]);\n }\n\n // PDF — best-effort text-layer extraction via pdfjs (dynamically imported).\n // Unreliable for tabular data and useless for scanned PDFs; the caller shows a\n // graceful note if this throws. NOTE: pdfjs worker setup is Vite-specific and\n // must be verified against a running build.\n if (name.endsWith('.pdf') || file.type === 'application/pdf') {\n const pdfjs = await import('pdfjs-dist');\n const buf = await file.arrayBuffer();\n const doc = await pdfjs.getDocument({ data: new Uint8Array(buf) }).promise;\n const parts: string[] = [];\n const pages = Math.min(doc.numPages, 20);\n for (let page = 1; page <= pages; page += 1) {\n const content = await (await doc.getPage(page)).getTextContent();\n parts.push(content.items.map((item) => ('str' in item ? item.str : '')).join(' '));\n }\n return parts.join('\\n');\n }\n\n // Unknown extension → best-effort text.\n return await file.text();\n}\n\n/** Keep the first MAX_ROWS lines / MAX_INLINE_CHARS_PER_FILE chars so a huge\n * file can't blow the prompt. Returns whether anything was dropped so the\n * agent knows it's a sample. */\nfunction limit(text: string): { text: string; truncated: boolean } {\n let out = text;\n let truncated = false;\n const lines = out.split(/\\r?\\n/);\n if (lines.length > MAX_ROWS) {\n out = lines.slice(0, MAX_ROWS).join('\\n');\n truncated = true;\n }\n if (out.length > MAX_INLINE_CHARS_PER_FILE) {\n out = out.slice(0, MAX_INLINE_CHARS_PER_FILE);\n truncated = true;\n }\n return { text: out, truncated };\n}\n\n/**\n * Extract every attachment and format them as fenced \"Attached data source\"\n * blocks the agent can read and turn into a DataSink. Files that fail to parse\n * (e.g. a scanned PDF) produce a short note rather than aborting the send.\n */\nexport async function buildAttachmentBlock(files: File[]): Promise<string> {\n const blocks: string[] = [];\n for (const file of files) {\n let raw: string;\n try {\n raw = await extractOne(file);\n } catch (err) {\n const reason = err instanceof Error ? err.message : 'unknown error';\n blocks.push(\n `--- Attached file \"${file.name}\" could not be auto-read (${reason}). ` +\n `Please paste its rows here instead. ---`\n );\n continue;\n }\n const { text, truncated } = limit(raw.trim());\n const note = truncated\n ? ` (sample — first ${MAX_ROWS} rows / ~${Math.round(MAX_INLINE_CHARS_PER_FILE / 1000)}KB)`\n : '';\n blocks.push(`--- Attached data source: ${file.name}${note} ---\\n${text}\\n--- end ${file.name} ---`);\n }\n return blocks.join('\\n\\n');\n}\n"],"mappings":";AAkCA,IAAa,IAA4B;AAWzC,SAAS,EAAW,GAAqB;CACvC,IAAM,IAAO,EAAK,KAAK,aAAa;AACpC,QACE,EAAK,SAAS,QAAQ,IACtB,EAAK,SAAS,OAAO,IACrB,EAAK,SAAS,OAAO,IACrB,EAAK,SAAS,sBACd,EAAK,SAAS,cACd,EAAK,KAAK,WAAW,QAAQ;;AAIjC,eAAe,EAAW,GAA6B;CACrD,IAAM,IAAO,EAAK,KAAK,aAAa;AAGpC,KAAI,EAAW,EAAK,CAClB,QAAO,MAAM,EAAK,MAAM;AAI1B,KAAI,EAAK,SAAS,QAAQ,IAAI,EAAK,SAAS,OAAO,EAAE;EACnD,IAAM,IAAO,MAAM,OAAO,0CACpB,IAAM,MAAM,EAAK,aAAa,EAC9B,IAAK,EAAK,KAAK,GAAK,EAAE,MAAM,SAAS,CAAC,EACtC,IAAQ,EAAG,WAAW;AAE5B,SADK,IACE,EAAK,MAAM,aAAa,EAAG,OAAO,GAAO,GAD7B;;AAQrB,KAAI,EAAK,SAAS,OAAO,IAAI,EAAK,SAAS,mBAAmB;EAC5D,IAAM,IAAQ,MAAM,OAAO,+CACrB,IAAM,MAAM,EAAK,aAAa,EAC9B,IAAM,MAAM,EAAM,YAAY,EAAE,MAAM,IAAI,WAAW,EAAI,EAAE,CAAC,CAAC,SAC7D,IAAkB,EAAE,EACpB,IAAQ,KAAK,IAAI,EAAI,UAAU,GAAG;AACxC,OAAK,IAAI,IAAO,GAAG,KAAQ,GAAO,KAAQ,GAAG;GAC3C,IAAM,IAAU,OAAO,MAAM,EAAI,QAAQ,EAAK,EAAE,gBAAgB;AAChE,KAAM,KAAK,EAAQ,MAAM,KAAK,MAAU,SAAS,IAAO,EAAK,MAAM,GAAI,CAAC,KAAK,IAAI,CAAC;;AAEpF,SAAO,EAAM,KAAK,KAAK;;AAIzB,QAAO,MAAM,EAAK,MAAM;;AAM1B,SAAS,EAAM,GAAoD;CACjE,IAAI,IAAM,GACN,IAAY,IACV,IAAQ,EAAI,MAAM,QAAQ;AAShC,QARI,EAAM,SAAA,QACR,IAAM,EAAM,MAAM,GAAA,IAAY,CAAC,KAAK,KAAK,EACzC,IAAY,KAEV,EAAI,SAAA,QACN,IAAM,EAAI,MAAM,GAAG,EAA0B,EAC7C,IAAY,KAEP;EAAE,MAAM;EAAK;EAAW;;AAQjC,eAAsB,EAAqB,GAAgC;CACzE,IAAM,IAAmB,EAAE;AAC3B,MAAK,IAAM,KAAQ,GAAO;EACxB,IAAI;AACJ,MAAI;AACF,OAAM,MAAM,EAAW,EAAK;WACrB,GAAK;GACZ,IAAM,IAAS,aAAe,QAAQ,EAAI,UAAU;AACpD,KAAO,KACL,sBAAsB,EAAK,KAAK,4BAA4B,EAAO,4CAEpE;AACD;;EAEF,IAAM,EAAE,SAAM,iBAAc,EAAM,EAAI,MAAM,CAAC,EACvC,IAAO,IACT,gCAAwC,KAAK,MAAM,IAA4B,IAAK,CAAC,OACrF;AACJ,IAAO,KAAK,6BAA6B,EAAK,OAAO,EAAK,QAAQ,EAAK,YAAY,EAAK,KAAK,MAAM;;AAErG,QAAO,EAAO,KAAK,OAAO"}
@@ -21,7 +21,10 @@ var e = {
21
21
  ],
22
22
  requiresAdmin: !0,
23
23
  writeCapable: !0,
24
- streamBudgetMs: 42e4
24
+ streamBudgetMs: 42e4,
25
+ accumulatesNarration: !0,
26
+ maxInlineCharsPerFile: 15155,
27
+ maxInlineCharsTotal: 2e5
25
28
  }, n = [e, t];
26
29
  //#endregion
27
30
  export { e as ASK, n as BIGCONSOLE_ASSISTANT_MODES, t as BUILD };
@@ -1 +1 @@
1
- {"version":3,"file":"modes.js","names":[],"sources":["../../../src/bigconsole/assistant/modes.ts"],"sourcesContent":["/**\n * The modes BigConsole offers the shared assistant panel.\n *\n * ★★ Mode IDS are transport values, not labels. The id picks the sandbox image,\n * the env that image boots with, and the agent persona; `label` is the only part\n * a user sees. Renaming an id silently changes which agent answers.\n *\n * ★★★ WHY `assistant` (Ask) IS SAFE HERE, despite the note beside `MODE` in\n * `createSandboxAssistantTransport.ts`.\n *\n * That comment says BigConsole \"still boots the manually-tagged ACA image\n * `alpha-delegated-auth-v11`\", and that the combined `assistant` mode \"depends\n * on newer image contracts that are not yet guaranteed on this tag\". Both\n * halves are now stale:\n *\n * - ACA — Azure Container Apps — was DECOMMISSIONED and deleted on\n * 2026-08-15. The sandbox runs on the AWS/Cloudflare path now; there is no\n * ACA image line left to be behind.\n * - fe-libs' `getImageTagForMode` returns the SAME tag for `assistant` and\n * `api-calls` (`alpha-delegated-auth-v11` off-prod, `prod` in prod), and\n * `getImageForMode` maps `assistant` onto the api-calls IMAGE outright —\n * \"one image, mode chosen at runtime\". So Ask runs on exactly the image\n * BigConsole already boots today, with `AI_ASSISTANT_MODE` selecting the\n * persona.\n *\n * HealthyBowl, VibeControls and FluidGrids all ship this same `assistant` mode\n * in production, on that same image. Ask is not a new image contract for\n * BigConsole; it is the persona switch the image already supports.\n */\nimport type { AssistantModeConfig } from '@burdenoff/fe-libs/shared/assistant/conversation';\n\n/**\n * Read-only. The ONLY mode fe-libs ever forwards the user's tokens to, and the\n * only one whose sandbox boots with `AI_ASSISTANT_READONLY=1`.\n *\n * ★ This is the user-visible point of the migration. Today BigConsole's\n * assistant is Owner/Admin-only in its entirety, because the legacy\n * `AssistantWidget` has exactly one mode and that mode writes. A member who\n * previously saw NO assistant at all gets read-only Ask, while `Build` stays\n * hidden from them — fe-libs does that filtering itself\n * (`AssistantChatArea` keeps `!mode.requiresAdmin || canUseAdminModes`).\n */\nconst ASK = {\n id: 'assistant',\n label: 'Ask',\n description:\n 'Ask about BigConsole and your workspace. It reads your dashboards, data sinks, parsers and widgets, ' +\n 'explains what you are looking at and points you to the right page. Read-only.',\n suggestions: [\n 'What can I do on this screen?',\n 'What is in this data sink?',\n 'Which widgets are on this dashboard?',\n 'Why is my parser returning no rows?',\n ],\n // No flags: resolves to the chat defaults — no artifacts, 180s budget, not\n // continued in the background, provisionable.\n} as const satisfies AssistantModeConfig;\n\n/**\n * The write mode. `api-calls` is the only id whose sandbox boots with\n * `AI_ASSISTANT_CONFIRM_WRITES=1`, which is why the label may change and the id\n * may not.\n */\nconst BUILD = {\n id: 'api-calls',\n label: 'Build',\n description:\n 'Let the assistant build for you: create a data sink, a dashboard, a parser and the widgets on it. ' +\n 'It shows you each change and waits for your approval before anything is saved.',\n suggestions: [\n 'Build a school attendance dashboard from this CSV',\n 'Add a bar chart of monthly totals to this dashboard',\n 'Create a parser that splits the address column',\n 'Show me what this data sink contains',\n ],\n // `requiresAdmin` is what makes fe-libs issue the myRoles query at all.\n requiresAdmin: true,\n // `writeCapable` routes the first switch through the consent dialog and turns\n // on the composer's write-access warning.\n writeCapable: true,\n /**\n * 420s, matching the legacy transport's `STREAM_BUDGET_MS` exactly, so this\n * introduces no behavioural change.\n *\n * ★★ NOT cosmetic, and not a round number picked for comfort. The default is\n * 180s, which was SHORTER THAN THE WORK: a full build — data sink → dashboard\n * → parser → widget, each a separate gateway call preceded by a model\n * round-trip — measured 229s in prod. The agent finished, the dashboard\n * genuinely existed, and the user was told the assistant timed out. People\n * then retried and ended up with duplicate dashboards.\n *\n * Finite, positive and under `MAX_ASSISTANT_STREAM_BUDGET_MS`, so fe-libs\n * honours it (`resolveAssistantModeCapabilities`) instead of silently falling\n * back to 180_000 — which is why this seam needs fe-libs >= 2026.916.1.\n */\n streamBudgetMs: 420_000,\n} as const satisfies AssistantModeConfig;\n\n/**\n * The list handed to `AssistantProductProvider`, in display order.\n *\n * ★ Exported as ONE array and consumed by identity. The seam guard asserts\n * `toBe`, not deep equality, because any `.map()` or rebuild on the way to the\n * provider is exactly how `requiresAdmin` and `writeCapable` get dropped.\n */\nexport const BIGCONSOLE_ASSISTANT_MODES: readonly AssistantModeConfig[] = [ASK, BUILD];\n\nexport { ASK, BUILD };\n"],"mappings":";AA0CA,IAAM,IAAM;CACV,IAAI;CACJ,OAAO;CACP,aACE;CAEF,aAAa;EACX;EACA;EACA;EACA;EACD;CAGF,EAOK,IAAQ;CACZ,IAAI;CACJ,OAAO;CACP,aACE;CAEF,aAAa;EACX;EACA;EACA;EACA;EACD;CAED,eAAe;CAGf,cAAc;CAgBd,gBAAgB;CACjB,EASY,IAA6D,CAAC,GAAK,EAAM"}
1
+ {"version":3,"file":"modes.js","names":[],"sources":["../../../src/bigconsole/assistant/modes.ts"],"sourcesContent":["/**\n * The modes BigConsole offers the shared assistant panel.\n *\n * ★★ Mode IDS are transport values, not labels. The id picks the sandbox image,\n * the env that image boots with, and the agent persona; `label` is the only part\n * a user sees. Renaming an id silently changes which agent answers.\n *\n * ★★★ WHY `assistant` (Ask) IS SAFE HERE, despite the note beside `MODE` in\n * `createSandboxAssistantTransport.ts`.\n *\n * That comment says BigConsole \"still boots the manually-tagged ACA image\n * `alpha-delegated-auth-v11`\", and that the combined `assistant` mode \"depends\n * on newer image contracts that are not yet guaranteed on this tag\". Both\n * halves are now stale:\n *\n * - ACA — Azure Container Apps — was DECOMMISSIONED and deleted on\n * 2026-08-15. The sandbox runs on the AWS/Cloudflare path now; there is no\n * ACA image line left to be behind.\n * - fe-libs' `getImageTagForMode` returns the SAME tag for `assistant` and\n * `api-calls` (`alpha-delegated-auth-v11` off-prod, `prod` in prod), and\n * `getImageForMode` maps `assistant` onto the api-calls IMAGE outright —\n * \"one image, mode chosen at runtime\". So Ask runs on exactly the image\n * BigConsole already boots today, with `AI_ASSISTANT_MODE` selecting the\n * persona.\n *\n * HealthyBowl, VibeControls and FluidGrids all ship this same `assistant` mode\n * in production, on that same image. Ask is not a new image contract for\n * BigConsole; it is the persona switch the image already supports.\n */\nimport type { AssistantModeConfig } from '@burdenoff/fe-libs/shared/assistant/conversation';\n\n/**\n * Read-only. The ONLY mode fe-libs ever forwards the user's tokens to, and the\n * only one whose sandbox boots with `AI_ASSISTANT_READONLY=1`.\n *\n * ★ This is the user-visible point of the migration. Today BigConsole's\n * assistant is Owner/Admin-only in its entirety, because the legacy\n * `AssistantWidget` has exactly one mode and that mode writes. A member who\n * previously saw NO assistant at all gets read-only Ask, while `Build` stays\n * hidden from them — fe-libs does that filtering itself\n * (`AssistantChatArea` keeps `!mode.requiresAdmin || canUseAdminModes`).\n */\nconst ASK = {\n id: 'assistant',\n label: 'Ask',\n description:\n 'Ask about BigConsole and your workspace. It reads your dashboards, data sinks, parsers and widgets, ' +\n 'explains what you are looking at and points you to the right page. Read-only.',\n suggestions: [\n 'What can I do on this screen?',\n 'What is in this data sink?',\n 'Which widgets are on this dashboard?',\n 'Why is my parser returning no rows?',\n ],\n // No flags: resolves to the chat defaults — no artifacts, 180s budget, not\n // continued in the background, provisionable.\n} as const satisfies AssistantModeConfig;\n\n/**\n * The write mode. `api-calls` is the only id whose sandbox boots with\n * `AI_ASSISTANT_CONFIRM_WRITES=1`, which is why the label may change and the id\n * may not.\n */\nconst BUILD = {\n id: 'api-calls',\n label: 'Build',\n description:\n 'Let the assistant build for you: create a data sink, a dashboard, a parser and the widgets on it. ' +\n 'It shows you each change and waits for your approval before anything is saved.',\n suggestions: [\n 'Build a school attendance dashboard from this CSV',\n 'Add a bar chart of monthly totals to this dashboard',\n 'Create a parser that splits the address column',\n 'Show me what this data sink contains',\n ],\n // `requiresAdmin` is what makes fe-libs issue the myRoles query at all.\n requiresAdmin: true,\n // `writeCapable` routes the first switch through the consent dialog and turns\n // on the composer's write-access warning.\n writeCapable: true,\n /**\n * 420s, matching the legacy transport's `STREAM_BUDGET_MS` exactly, so this\n * introduces no behavioural change.\n *\n * ★★ NOT cosmetic, and not a round number picked for comfort. The default is\n * 180s, which was SHORTER THAN THE WORK: a full build — data sink → dashboard\n * → parser → widget, each a separate gateway call preceded by a model\n * round-trip — measured 229s in prod. The agent finished, the dashboard\n * genuinely existed, and the user was told the assistant timed out. People\n * then retried and ended up with duplicate dashboards.\n *\n * Finite, positive and under `MAX_ASSISTANT_STREAM_BUDGET_MS`, so fe-libs\n * honours it (`resolveAssistantModeCapabilities`) instead of silently falling\n * back to 180_000 — which is why this seam needs fe-libs >= 2026.916.1.\n */\n streamBudgetMs: 420_000,\n /**\n * Report the WHOLE run's narration, joined oldest first, instead of only the\n * newest sandbox message (BC1, BOFF-7334).\n *\n * ★★ Legacy PARITY, not a new behaviour. `createSandboxAssistantTransport`'s\n * `buildProgress` already joins every assistant message of the run — its own\n * comment reads \"ACCUMULATE the run, don't just show its last line\" — and it\n * does so for a reason that has not changed: the agent opens a NEW message per\n * step and prints a link the moment a create lands (\"✅ Data sink created —\n * [Open …](/…)\"). Rendering only the newest message threw all of that away on\n * the following poll; the user saw a lone \"Thinking…\" and none of the links\n * they had been promised. Without this flag the shared panel would reintroduce\n * the bug BigConsole already fixed.\n *\n * ★★ It changes what is STORED, not only what is displayed: the completed turn\n * persists the joined account as its answer, so Copy, Export and read-aloud\n * carry it too. That is correct HERE — the build log IS the answer, and it is\n * where the links to the new data sink and dashboard live — and wrong for Ask,\n * which is why Ask does not set it.\n *\n * Honoured by `resolveAssistantModeCapabilities`, which defaults it to `false`\n * for every mode including the built-ins, so this seam needs fe-libs\n * >= 2026.917.2 (fe-libs #233).\n */\n accumulatesNarration: true,\n /**\n * 15,155 characters of ONE attached file's text, against fe-libs' 8,000\n * default (BOFF-7334; fe-libs #242, so this seam needs >= 2026.920.2).\n *\n * ★★★ DERIVED FROM THE BYTE ENVELOPE, BECAUSE THAT IS THE ONE THAT BINDS:\n *\n * floor((MAX_INLINE_TURN_BODY_BYTES - PROMPT_ENVELOPE_RESERVE_BYTES)\n * / MAX_UTF8_BYTES_PER_JS_CHAR / MAX_ATTACHMENTS)\n * = floor(227,328 / 3 / 5) = 15,155\n *\n * and 5 x 15,155 x 3 = 227,325 <= 227,328, so this is the LARGEST value that\n * derivation allows — three bytes of slack. Two properties follow, and\n * `tests/assistantSeam.test.ts` guards both rather than the literal:\n * 1. the attachment TEXT can never exhaust the CHARACTER total\n * (5 x 15,155 = 75,775 <= MAX_CONFIGURABLE_INLINE_CHARS), and\n * 2. its RAW UTF-8 BYTES can never exhaust the envelope `capTextToBody`\n * cuts to (5 x 15,155 x 3 = 227,325 <= 227,328), whatever script the\n * files are written in.\n *\n * ★★★ (2) BOUNDS THE PAYLOAD, NOT THE RENDERED TURN. An earlier revision of\n * this docblock claimed the stronger thing — \"it can never exhaust the BYTE\n * envelope, whatever script the files are written in\" — and that is FALSE.\n * `capTextToBody` weighs `JSON.stringify({prompt, parts})` over the RENDERED\n * block, and that block indents every line by four spaces, writes every\n * newline as a two-byte escape, and adds an `[n] file — mime, size` header, a\n * `Contents (...)` line and a pair of fences per file plus one\n * `--- Attachments (N) ---` frame per turn. Where the lines are short and\n * multibyte that is over 4 bytes per attachment character, not 3, so the\n * envelope CAN still bind and fe-libs then cuts the turn total until it\n * fits. Measured through `buildPromptWithAttachments` on the installed\n * 2026.920.4, over the fixture the test carries — five 120 K-character\n * all-CJK files of 1-3 characters a line — characters of each file that\n * reached the agent:\n *\n * 15,155/200,000 15,151 15,151 15,151 11,179 3\n * 8k/20k (today) 7,996 7,996 4,006 2 omitted\n *\n * 56,635 characters served for 227,326 of the 227,328 bytes, i.e. 4.01 bytes\n * a character where the derivation budgets 3, and the fifth file arrives as\n * a THREE-CHARACTER sliver announced as a sample.\n * That is NOT a reason to lower this number, and the alternative is worse:\n * on that same turn it still serves at least as much of every file as the\n * shared default it raises (the second row IS the comparison); on the\n * ordinary Build turn — three CJK fact tables and two Devanagari lookups —\n * it hands BOTH lookups over WHOLE where the default cuts them to 77 and 3\n * characters; and no per-file value can repair a slice fe-libs spends IN\n * ATTACHMENT ORDER out of a total it has already cut. That last part is fe-libs' to fix — BOFF-7532 — and the\n * residual is pinned by the test named \"the byte envelope STILL binds\".\n *\n * ★★★ WHY 3 IS A BOUND, NOT A GUESS. fe-libs budgets in JS string LENGTH and\n * enforces in UTF-8 BYTES. Every UTF-16 code unit encodes to at most 3 bytes:\n * <= U+007F is 1, <= U+07FF is 2, the rest of the BMP is 3, and an astral\n * character is 4 bytes across TWO units — 2 bytes a unit. So\n * `utf8Bytes(s) <= 3 * s.length` holds for every string, with equality from\n * U+0800 (CJK, Devanagari, Thai). The test proves that over the whole BMP\n * instead of asserting it.\n *\n * ★★★ THIS SEAM'S FIRST TWO ANSWERS WERE BOTH WRONG, and the second was wrong\n * in a way ASCII tests could not see. 100,000 let two files eat the turn.\n * 40,000 — `MAX_CONFIGURABLE_INLINE_CHARS / MAX_ATTACHMENTS` — was defended\n * here as \"the largest per-file limit that cannot starve a later attachment\";\n * that claim was FALSE for any non-ASCII file, because the character total is\n * not what binds. `capTextToBody` lowers the turn total until the prompt fits\n * 227,328 BYTES, and `applyInlineBudget` then still spends\n * `min(remainingTotal, perFileChars)` IN ATTACHMENT ORDER. Measured against\n * the installed 2026.920.4 dist with the fixtures the tests now carry — a\n * 2,000-row all-CJK fact CSV (186 K characters, 506 KB, 2.78 bytes a\n * character) and a 40-row Devanagari dimension CSV — rows of each file that\n * reached the agent:\n *\n * 8k/20k (today) 40k/200k 15,155/200,000\n * 2 big + 1 small 85, 85, 40w 429, 429, 0 162, 162, 40w\n * 3 big + 2 small 85, 85, 43, 1, 0 429, 427, 0, 0, 0 162 x3, 40w, 40w\n * 5 big 85, 85, 43, 1, — 429, 427, 0, 0, — 162 x5\n *\n * (\"w\" is a table that arrived WHOLE; \"—\" is the honest `contents omitted`\n * note; every other zero is a header fragment announced as a truncation.) At\n * 40,000 the dimension tables arrive as SLIVERS ANNOUNCED AS SAMPLES —\n * \"Contents (first 50 characters of 5.7 KB; truncated)\" — which is the\n * failure this mode's docblocks keep naming: the agent believes it sampled\n * the dimension, joins against values it never saw, and the dashboard it then\n * builds looks right. Worse, on the ordinary two-fact-tables-and-a-lookup\n * turn 40,000 was BEATEN BY THE SHARED DEFAULT it replaces, which hands that\n * lookup table over WHOLE.\n *\n * ★★ WHAT THE DERIVATION COSTS, in the ASCII case where 40,000 was safe —\n * same measurement, a 2,200-row / 144 KB fact CSV and a 40-row dimension CSV:\n *\n * 8k/20k (today) 40k/200k 15,155/200,000\n * 1 big 118 596 225\n * 3 big + 2 small 118, 118, 59, 0, 0 596 x3, 40w, 40w 225 x3, 40w, 40w\n *\n * 225 rows of a fact table instead of 596. Still 1.9x fe-libs' default, and\n * still more than the legacy widget ever sent: `attachmentExtract.ts` cuts at\n * `MAX_ROWS` = 200 rows *or* `MAX_INLINE_CHARS_PER_FILE` = 100,000, whichever\n * comes first, and for a row-oriented CSV the row cap almost always wins — so\n * its 100,000 was a ceiling that never bound. Build stays a strict\n * improvement on both paths it replaces, in every script, which matching a\n * ceiling was never worth breaking.\n *\n * ★ What this does NOT fix, because it cannot be fixed from a mode config:\n * the user's own message shares that same 227,328-byte envelope, and opting\n * above fe-libs' defaults is what turns `capTextToBody` on at all. See\n * `maxInlineCharsTotal` below for the measured window. The real fix is a\n * per-attachment floor or a smallest-first spending pass IN FE-LIBS —\n * BOFF-7532. Raise this number only once that lands.\n */\n maxInlineCharsPerFile: 15_155,\n /**\n * 200,000 characters of attached text across the whole turn — fe-libs'\n * `MAX_CONFIGURABLE_INLINE_CHARS`, i.e. \"as much as a 256 KB body can carry\".\n *\n * ★★★ SET EXPLICITLY BECAUSE LEAVING IT OUT IS NOT NEUTRAL. fe-libs raises\n * the turn total to match the per-file limit only when that is LARGER, so\n * omitting this resolves `{perFileChars: 15_155, totalChars: 20_000}` — the\n * 8,000/20,000 default's total, one and a third files' worth of budget for a\n * five-file turn, spent GREEDILY IN ATTACHMENT ORDER. At 200,000 the\n * character total never binds before the per-file limit does\n * (5 x 15,155 = 75,775), which is the point: the per-file limit is the only\n * thing rationing the turn, and it is derived from the envelope.\n *\n * ★★ RAISING EITHER FIELD ABOVE fe-libs' DEFAULTS ARMS A SECOND, TIGHTER\n * CEILING, and that is a measured cost rather than a theoretical one.\n * `buildInlineBudget` runs `capTextToBody` only when\n * `perFileChars > 8,000 || totalChars > 20,000`, and `capTextToBody` fits the\n * prompt into `MAX_INLINE_TURN_BODY_BYTES - PROMPT_ENVELOPE_RESERVE_BYTES` =\n * 227,328 bytes. The un-opted path has no such pass — its only ceiling is\n * `buildBoundedAttachmentTurn`'s `MAX_TURN_BODY_BYTES` = 262,144, roughly\n * 35 KB more room — so for a very large USER MESSAGE this mode delivers less\n * than it would have unconfigured. Measured, 3 big + 2 small ASCII CSVs, rows\n * that reached the agent:\n *\n * user message chars this mode fe-libs defaults\n * <= 189,000 225, 225, 225, 40, 40 118, 118, 59, 0, 0\n * 190,000 225, 225, 53, 1, 0 118, 118, 59, 0, 0\n * 210,000 222, 0, 0, 0, 0 118, 118, 59, 0, 0\n * >= 226,000 nothing at all 118, 118, 59, 0, 0\n *\n * The threshold is 190,000 characters TYPED OR PASTED INTO THE COMPOSER, so\n * the window is narrow — but it is reachable (a pasted CSV under \"build me a\n * dashboard from this\"), it is pinned by a test, and it is one more reason\n * the per-file limit is not raised: at 40,000 the same crossover sat at\n * 137,000 characters, and by 150,000 it had already lost three of the five\n * files this value still serves whole. BOFF-7532 asks fe-libs to measure\n * `capTextToBody` against the envelope the send path actually enforces, and\n * to spend the remainder fairly instead of in attachment order.\n *\n * ★★ AND IT IS NOT \"cut, never rejected\", which an earlier draft of this\n * docblock claimed. `buildBoundedAttachmentTurn` cuts the attachment TEXT,\n * then drops the attachment block entirely with an explicit disclosure (\"N\n * attachment(s) omitted from this turn to fit the request-size limit\"), and\n * if even that text-only turn still exceeds `MAX_TURN_BODY_BYTES` it ends in\n * `throw new Error(REQUEST_TOO_LARGE_REFUSAL)`. The path is narrow — the\n * user's own message has to overflow a 256 KB body on its own — but it is\n * real, and it is the one case where nothing is sent at all.\n *\n * ★ Read at PREPARE time as well as at send time: fe-libs keeps\n * `largestInlineCharsPerFile(modes)` characters of a file when it is attached,\n * so a CSV attached while in Ask is not already cut to 8,000 by the time the\n * user switches to Build. The send then cuts to the SENDING mode's limits.\n *\n * ★★★ NONE OF THIS RUNS UNTIL THE APP MOVES ITS OWN fe-libs PIN. fe-libs is\n * `external` in this package's Vite build, so the HOST APP's copy is what\n * executes here, and `bigconsole-app` pins it in `dependencies` AND in\n * `overrides` — an `overrides` entry is not satisfied by this package's\n * `peerDependencies`. At the app's `2026.917.2`, `resolveAssistantInlineLimits`\n * does not exist at all, so both fields above are read by nobody, with no\n * error and no warning, and the panel keeps the fixed 8,000/20,000 path.\n * `accumulatesNarration` DOES exist there, so half of BOFF-7334 would ship\n * and half would not. Bump the app's fe-libs to >= 2026.920.2 in both places\n * in the same PR as the microfe bump — BOFF-7533.\n */\n maxInlineCharsTotal: 200_000,\n} as const satisfies AssistantModeConfig;\n\n/**\n * The list handed to `AssistantProductProvider`, in display order.\n *\n * ★ Exported as ONE array and consumed by identity. The seam guard asserts\n * `toBe`, not deep equality, because any `.map()` or rebuild on the way to the\n * provider is exactly how `requiresAdmin` and `writeCapable` get dropped.\n */\nexport const BIGCONSOLE_ASSISTANT_MODES: readonly AssistantModeConfig[] = [ASK, BUILD];\n\nexport { ASK, BUILD };\n"],"mappings":";AA0CA,IAAM,IAAM;CACV,IAAI;CACJ,OAAO;CACP,aACE;CAEF,aAAa;EACX;EACA;EACA;EACA;EACD;CAGF,EAOK,IAAQ;CACZ,IAAI;CACJ,OAAO;CACP,aACE;CAEF,aAAa;EACX;EACA;EACA;EACA;EACD;CAED,eAAe;CAGf,cAAc;CAgBd,gBAAgB;CAyBhB,sBAAsB;CA4GtB,uBAAuB;CAiEvB,qBAAqB;CACtB,EASY,IAA6D,CAAC,GAAK,EAAM"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-bigconsole",
3
- "version": "2026.917.3",
3
+ "version": "2026.920.1",
4
4
  "description": "BigConsole - AI-powered analytics and dashboard platform",
5
5
  "type": "module",
6
6
  "files": [
@@ -49,7 +49,7 @@
49
49
  },
50
50
  "peerDependencies": {
51
51
  "@apollo/client": "^4.0.11",
52
- "@burdenoff/fe-libs": "2026.916.1",
52
+ "@burdenoff/fe-libs": "2026.920.4",
53
53
  "@burdenoff/fluidgrids-fe-sdk": "2026.731.4",
54
54
  "@iconify/react": "^6.0.2",
55
55
  "@monaco-editor/react": "^4.7.0",
@@ -84,7 +84,7 @@
84
84
  "zustand": "^5.0.3"
85
85
  },
86
86
  "devDependencies": {
87
- "@burdenoff/fe-libs": "2026.916.1",
87
+ "@burdenoff/fe-libs": "2026.920.4",
88
88
  "@burdenoff/fluidgrids-fe-sdk": "2026.731.4",
89
89
  "@eslint/js": "^10.0.1",
90
90
  "@graphql-codegen/add": "^5.0.0",