@burdenoff/microfe-bigconsole 2026.922.1 → 2026.924.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.
@@ -19,7 +19,6 @@ var e = {
19
19
  "Create a parser that splits the address column",
20
20
  "Show me what this data sink contains"
21
21
  ],
22
- requiresAdmin: !0,
23
22
  writeCapable: !0,
24
23
  streamBudgetMs: 42e4,
25
24
  accumulatesNarration: !0,
@@ -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 /**\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"}
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 // ★★★ NO `requiresAdmin` (BOFF-7800). Declaring it is what makes fe-libs issue\n // the myRoles query at all and hide this mode from anyone who is not an org\n // Owner/Admin — the wrong question at the wrong scope, since product writes\n // are governed by the WORKSPACE role and the server already grants\n // `sandbox:create` to Editor and above. RBAC alone decides what this mode may\n // DO; nothing decides who may see it.\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;CASD,cAAc;CAgBd,gBAAgB;CAyBhB,sBAAsB;CA4GtB,uBAAuB;CAiEvB,qBAAqB;CACtB,EASY,IAA6D,CAAC,GAAK,EAAM"}
@@ -1,78 +1,78 @@
1
1
  import e from "../../../hooks/useWidgetComments.js";
2
- import { useCallback as t, useState as n } from "react";
3
- import { Check as r, CornerDownRight as i, Loader2 as a, MessageSquare as o, RotateCcw as s, Send as c, Trash2 as l, X as u } from "lucide-react";
4
- import { useI18n as d } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
5
- import { jsx as f, jsxs as p } from "react/jsx-runtime";
2
+ import { useCallback as t, useEffect as n, useRef as r, useState as i } from "react";
3
+ import { Check as a, CornerDownRight as o, Loader2 as s, MessageSquare as c, RotateCcw as l, Send as u, Trash2 as d, X as f } from "lucide-react";
4
+ import { useI18n as p } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
5
+ import { jsx as m, jsxs as h } from "react/jsx-runtime";
6
6
  //#region src/bigconsole/components/widgets/comments/WidgetComments.tsx
7
- function m(e, t) {
7
+ function g(e, t) {
8
8
  return e("bigconsole.widget.comments.authorLabel", "User {{id}}", { id: t.slice(0, 6) });
9
9
  }
10
- function h(e) {
10
+ function _(e) {
11
11
  let t = new Date(e);
12
12
  return Number.isNaN(t.getTime()) ? "" : new Intl.DateTimeFormat(void 0, {
13
13
  dateStyle: "medium",
14
14
  timeStyle: "short"
15
15
  }).format(t);
16
16
  }
17
- var g = ({ comment: e, isReply: t, busy: n, onResolve: a, onDelete: o, onReply: c }) => {
18
- let { t: u } = d(), g = !!e.resolvedAt, _ = e.content.text ?? e.content.markdown ?? "";
19
- return /* @__PURE__ */ f("div", {
17
+ var v = ({ comment: e, isReply: t, busy: n, onResolve: r, onDelete: i, onReply: s }) => {
18
+ let { t: c } = p(), u = !!e.resolvedAt, f = e.content.text ?? e.content.markdown ?? "";
19
+ return /* @__PURE__ */ m("div", {
20
20
  className: t ? "pl-6" : "",
21
- children: /* @__PURE__ */ p("div", {
21
+ children: /* @__PURE__ */ h("div", {
22
22
  className: "rounded-md border border-[var(--color-border-default)] bg-bg-surface p-2.5",
23
23
  children: [
24
- /* @__PURE__ */ p("div", {
24
+ /* @__PURE__ */ h("div", {
25
25
  className: "flex items-center justify-between gap-2",
26
- children: [/* @__PURE__ */ f("span", {
26
+ children: [/* @__PURE__ */ m("span", {
27
27
  className: "text-xs font-medium text-text-primary",
28
- children: m(u, e.authorId)
29
- }), /* @__PURE__ */ f("span", {
28
+ children: g(c, e.authorId)
29
+ }), /* @__PURE__ */ m("span", {
30
30
  className: "text-[10px] text-text-muted tabular-nums",
31
- children: h(e.createdAt)
31
+ children: _(e.createdAt)
32
32
  })]
33
33
  }),
34
- /* @__PURE__ */ f("p", {
34
+ /* @__PURE__ */ m("p", {
35
35
  className: "mt-1 whitespace-pre-wrap break-words text-sm text-text-secondary",
36
- children: _
36
+ children: f
37
37
  }),
38
- /* @__PURE__ */ p("div", {
38
+ /* @__PURE__ */ h("div", {
39
39
  className: "mt-1.5 flex items-center gap-2",
40
40
  children: [
41
- g && /* @__PURE__ */ p("span", {
41
+ u && /* @__PURE__ */ h("span", {
42
42
  className: "inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] font-medium bg-status-success-bg text-status-success-text",
43
43
  children: [
44
- /* @__PURE__ */ f(r, { className: "h-3 w-3" }),
44
+ /* @__PURE__ */ m(a, { className: "h-3 w-3" }),
45
45
  " ",
46
- u("bigconsole.widget.comments.resolvedBadge", "Resolved")
46
+ c("bigconsole.widget.comments.resolvedBadge", "Resolved")
47
47
  ]
48
48
  }),
49
- c && /* @__PURE__ */ p("button", {
49
+ s && /* @__PURE__ */ h("button", {
50
50
  type: "button",
51
51
  disabled: n,
52
- onClick: () => c(e.id),
52
+ onClick: () => s(e.id),
53
53
  className: "inline-flex items-center gap-1 text-[11px] text-text-muted hover:text-text-primary disabled:opacity-50",
54
54
  children: [
55
- /* @__PURE__ */ f(i, { className: "h-3 w-3" }),
55
+ /* @__PURE__ */ m(o, { className: "h-3 w-3" }),
56
56
  " ",
57
- u("bigconsole.widget.comments.reply", "Reply")
57
+ c("bigconsole.widget.comments.reply", "Reply")
58
58
  ]
59
59
  }),
60
- /* @__PURE__ */ p("button", {
60
+ /* @__PURE__ */ h("button", {
61
61
  type: "button",
62
62
  disabled: n,
63
- onClick: () => a(e.id, !g),
63
+ onClick: () => r(e.id, !u),
64
64
  className: "inline-flex items-center gap-1 text-[11px] text-text-muted hover:text-text-primary disabled:opacity-50",
65
- children: [f(g ? s : r, { className: "h-3 w-3" }), g ? u("bigconsole.widget.comments.reopen", "Reopen") : u("bigconsole.widget.comments.resolve", "Resolve")]
65
+ children: [m(u ? l : a, { className: "h-3 w-3" }), u ? c("bigconsole.widget.comments.reopen", "Reopen") : c("bigconsole.widget.comments.resolve", "Resolve")]
66
66
  }),
67
- /* @__PURE__ */ p("button", {
67
+ /* @__PURE__ */ h("button", {
68
68
  type: "button",
69
69
  disabled: n,
70
- onClick: () => o(e.id),
70
+ onClick: () => i(e.id),
71
71
  className: "inline-flex items-center gap-1 text-[11px] text-status-error-text hover:opacity-80 disabled:opacity-50",
72
72
  children: [
73
- /* @__PURE__ */ f(l, { className: "h-3 w-3" }),
73
+ /* @__PURE__ */ m(d, { className: "h-3 w-3" }),
74
74
  " ",
75
- u("bigconsole.widget.comments.delete", "Delete")
75
+ c("bigconsole.widget.comments.delete", "Delete")
76
76
  ]
77
77
  })
78
78
  ]
@@ -80,138 +80,148 @@ var g = ({ comment: e, isReply: t, busy: n, onResolve: a, onDelete: o, onReply:
80
80
  ]
81
81
  })
82
82
  });
83
- }, _ = ({ placeholder: e, busy: t, autoFocus: r, onSubmit: i, onCancel: o }) => {
84
- let { t: s } = d(), [l, m] = n(""), h = async () => {
85
- let e = l.trim();
86
- e && await i(e) !== !1 && m("");
83
+ }, y = ({ placeholder: e, busy: t, autoFocus: n, onSubmit: r, onCancel: a }) => {
84
+ let { t: o } = p(), [c, l] = i(""), d = async () => {
85
+ let e = c.trim();
86
+ e && await r(e) !== !1 && l("");
87
87
  };
88
- return /* @__PURE__ */ p("div", {
88
+ return /* @__PURE__ */ h("div", {
89
89
  className: "flex items-end gap-2",
90
- children: [/* @__PURE__ */ f("textarea", {
91
- autoFocus: r,
92
- value: l,
93
- onChange: (e) => m(e.target.value),
90
+ children: [/* @__PURE__ */ m("textarea", {
91
+ autoFocus: n,
92
+ value: c,
93
+ onChange: (e) => l(e.target.value),
94
94
  onKeyDown: (e) => {
95
- e.stopPropagation(), (e.metaKey || e.ctrlKey) && e.key === "Enter" && (e.preventDefault(), h());
95
+ e.stopPropagation(), (e.metaKey || e.ctrlKey) && e.key === "Enter" && (e.preventDefault(), d());
96
96
  },
97
97
  rows: 2,
98
98
  placeholder: e,
99
99
  className: "flex-1 resize-none rounded-md border border-[var(--color-border-default)] bg-bg-surface px-2.5 py-1.5 text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-action-primary-bg"
100
- }), /* @__PURE__ */ p("div", {
100
+ }), /* @__PURE__ */ h("div", {
101
101
  className: "flex flex-col gap-1",
102
- children: [/* @__PURE__ */ f("button", {
102
+ children: [/* @__PURE__ */ m("button", {
103
103
  type: "button",
104
- disabled: t || !l.trim(),
105
- onClick: () => void h(),
106
- title: s("bigconsole.widget.comments.postTitle", "Post (⌘/Ctrl + Enter)"),
104
+ disabled: t || !c.trim(),
105
+ onClick: () => void d(),
106
+ title: o("bigconsole.widget.comments.postTitle", "Post (⌘/Ctrl + Enter)"),
107
107
  className: "inline-flex items-center justify-center rounded-md bg-action-primary-bg p-1.5 text-action-primary-text hover:opacity-90 disabled:opacity-50",
108
- children: t ? /* @__PURE__ */ f(a, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ f(c, { className: "h-4 w-4" })
109
- }), o && /* @__PURE__ */ f("button", {
108
+ children: t ? /* @__PURE__ */ m(s, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ m(u, { className: "h-4 w-4" })
109
+ }), a && /* @__PURE__ */ m("button", {
110
110
  type: "button",
111
- onClick: o,
111
+ onClick: a,
112
112
  className: "inline-flex items-center justify-center rounded-md border border-[var(--color-border-default)] p-1.5 text-text-muted hover:text-text-primary",
113
- title: s("bigconsole.widget.comments.cancelReplyTitle", "Cancel reply"),
114
- children: /* @__PURE__ */ f(u, { className: "h-4 w-4" })
113
+ title: o("bigconsole.widget.comments.cancelReplyTitle", "Cancel reply"),
114
+ children: /* @__PURE__ */ m(f, { className: "h-4 w-4" })
115
115
  })]
116
116
  })]
117
117
  });
118
- }, v = ({ widgetId: r, onClose: i }) => {
119
- let { t: s } = d(), { threads: c, count: l, loading: m, error: h, mutating: v, addComment: y, resolveComment: b, deleteComment: x } = e(r), [S, C] = n(null), w = t(async (e, t) => {
120
- let n = await y(e, t);
121
- return n && t && C(null), n;
122
- }, [y]);
123
- return /* @__PURE__ */ p("div", {
118
+ }, b = ({ widgetId: a, onClose: o }) => {
119
+ let { t: l } = p(), { threads: u, count: d, loading: g, error: _, mutating: b, addComment: x, resolveComment: S, deleteComment: C } = e(a), [w, T] = i(null), [E, D] = i(!1), O = r(null);
120
+ n(() => {
121
+ if (!E) return;
122
+ let e = O.current;
123
+ e?.scrollTo({
124
+ top: e.scrollHeight,
125
+ behavior: "smooth"
126
+ }), D(!1);
127
+ }, [E, u]);
128
+ let k = t(async (e, t) => {
129
+ let n = await x(e, t);
130
+ return n && (t ? T(null) : D(!0)), n;
131
+ }, [x]);
132
+ return /* @__PURE__ */ h("div", {
124
133
  className: "absolute inset-0 z-40 flex flex-col bg-bg-surface/98 backdrop-blur-sm",
125
134
  role: "dialog",
126
- "aria-label": s("bigconsole.widget.comments.dialogAriaLabel", "Widget comments"),
135
+ "aria-label": l("bigconsole.widget.comments.dialogAriaLabel", "Widget comments"),
127
136
  tabIndex: -1,
128
137
  onClick: (e) => e.stopPropagation(),
129
138
  onKeyDown: (e) => {
130
- e.key === "Escape" && (e.stopPropagation(), i());
139
+ e.key === "Escape" && (e.stopPropagation(), o());
131
140
  },
132
141
  children: [
133
- /* @__PURE__ */ p("div", {
142
+ /* @__PURE__ */ h("div", {
134
143
  className: "flex min-w-0 items-center justify-between gap-2 border-b border-[var(--color-border-subtle)] bg-bg-surface px-3 py-2 sm:px-4",
135
- children: [/* @__PURE__ */ p("h3", {
144
+ children: [/* @__PURE__ */ h("h3", {
136
145
  className: "flex min-w-0 items-center gap-2 text-sm font-semibold leading-tight tracking-tight text-text-primary",
137
146
  children: [
138
- /* @__PURE__ */ f(o, {
147
+ /* @__PURE__ */ m(c, {
139
148
  "aria-hidden": "true",
140
149
  className: "h-icon-sm w-icon-sm shrink-0 text-text-secondary"
141
150
  }),
142
- s("bigconsole.widget.comments.title", "Comments"),
143
- /* @__PURE__ */ f("span", {
151
+ l("bigconsole.widget.comments.title", "Comments"),
152
+ /* @__PURE__ */ m("span", {
144
153
  className: "rounded-full bg-bg-sunken px-1.5 py-0.5 text-[10px] text-text-secondary tabular-nums",
145
- children: l
154
+ children: d
146
155
  })
147
156
  ]
148
- }), /* @__PURE__ */ f("button", {
157
+ }), /* @__PURE__ */ m("button", {
149
158
  type: "button",
150
- onClick: i,
151
- "aria-label": s("bigconsole.widget.comments.closeAriaLabel", "Close comments"),
159
+ onClick: o,
160
+ "aria-label": l("bigconsole.widget.comments.closeAriaLabel", "Close comments"),
152
161
  className: "shrink-0 rounded-button p-1 text-text-muted hover:bg-bg-sunken hover:text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-bg-surface",
153
- children: /* @__PURE__ */ f(u, { className: "h-4 w-4" })
162
+ children: /* @__PURE__ */ m(f, { className: "h-4 w-4" })
154
163
  })]
155
164
  }),
156
- /* @__PURE__ */ p("div", {
165
+ /* @__PURE__ */ h("div", {
166
+ ref: O,
157
167
  className: "flex-1 space-y-3 overflow-auto p-3",
158
168
  children: [
159
- m && c.length === 0 && /* @__PURE__ */ f("div", {
169
+ g && u.length === 0 && /* @__PURE__ */ m("div", {
160
170
  className: "flex items-center justify-center py-8 text-text-muted",
161
- children: /* @__PURE__ */ f(a, { className: "h-5 w-5 animate-spin" })
171
+ children: /* @__PURE__ */ m(s, { className: "h-5 w-5 animate-spin" })
162
172
  }),
163
- h && !m && /* @__PURE__ */ f("p", {
173
+ _ && !g && /* @__PURE__ */ m("p", {
164
174
  role: "alert",
165
175
  className: "rounded-input border border-[var(--color-status-error-border)] bg-status-error-bg-subtle px-2.5 py-2 text-xs text-[var(--color-status-error-text)]",
166
- children: s("bigconsole.widget.comments.loadError", "Couldn’t load comments.")
176
+ children: l("bigconsole.widget.comments.loadError", "Couldn’t load comments.")
167
177
  }),
168
- !m && !h && c.length === 0 && /* @__PURE__ */ f("p", {
178
+ !g && !_ && u.length === 0 && /* @__PURE__ */ m("p", {
169
179
  className: "py-8 text-center text-sm text-text-muted",
170
- children: s("bigconsole.widget.comments.empty", "No comments yet. Start the conversation.")
180
+ children: l("bigconsole.widget.comments.empty", "No comments yet. Start the conversation.")
171
181
  }),
172
- c.map(({ comment: e, replies: t }) => /* @__PURE__ */ p("div", {
182
+ u.map(({ comment: e, replies: t }) => /* @__PURE__ */ h("div", {
173
183
  className: "space-y-2",
174
184
  children: [
175
- /* @__PURE__ */ f(g, {
185
+ /* @__PURE__ */ m(v, {
176
186
  comment: e,
177
- busy: v,
178
- onResolve: (e, t) => void b(e, t),
179
- onDelete: (e) => void x(e),
180
- onReply: (e) => C((t) => t === e ? null : e)
187
+ busy: b,
188
+ onResolve: (e, t) => void S(e, t),
189
+ onDelete: (e) => void C(e),
190
+ onReply: (e) => T((t) => t === e ? null : e)
181
191
  }),
182
- t.map((e) => /* @__PURE__ */ f(g, {
192
+ t.map((e) => /* @__PURE__ */ m(v, {
183
193
  comment: e,
184
194
  isReply: !0,
185
- busy: v,
186
- onResolve: (e, t) => void b(e, t),
187
- onDelete: (e) => void x(e)
195
+ busy: b,
196
+ onResolve: (e, t) => void S(e, t),
197
+ onDelete: (e) => void C(e)
188
198
  }, e.id)),
189
- S === e.id && /* @__PURE__ */ f("div", {
199
+ w === e.id && /* @__PURE__ */ m("div", {
190
200
  className: "pl-6",
191
- children: /* @__PURE__ */ f(_, {
192
- placeholder: s("bigconsole.widget.comments.replyPlaceholder", "Write a reply…"),
193
- busy: v,
201
+ children: /* @__PURE__ */ m(y, {
202
+ placeholder: l("bigconsole.widget.comments.replyPlaceholder", "Write a reply…"),
203
+ busy: b,
194
204
  autoFocus: !0,
195
- onSubmit: (t) => w(t, e.id),
196
- onCancel: () => C(null)
205
+ onSubmit: (t) => k(t, e.id),
206
+ onCancel: () => T(null)
197
207
  })
198
208
  })
199
209
  ]
200
210
  }, e.id))
201
211
  ]
202
212
  }),
203
- /* @__PURE__ */ f("div", {
213
+ /* @__PURE__ */ m("div", {
204
214
  className: "border-t border-[var(--color-border-default)] p-3",
205
- children: /* @__PURE__ */ f(_, {
206
- placeholder: s("bigconsole.widget.comments.addPlaceholder", "Add a comment…"),
207
- busy: v,
208
- onSubmit: (e) => w(e)
215
+ children: /* @__PURE__ */ m(y, {
216
+ placeholder: l("bigconsole.widget.comments.addPlaceholder", "Add a comment…"),
217
+ busy: b,
218
+ onSubmit: (e) => k(e)
209
219
  })
210
220
  })
211
221
  ]
212
222
  });
213
223
  };
214
224
  //#endregion
215
- export { v as WidgetComments };
225
+ export { b as WidgetComments };
216
226
 
217
227
  //# sourceMappingURL=WidgetComments.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"WidgetComments.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/comments/WidgetComments.tsx"],"sourcesContent":["/**\n * WidgetComments\n *\n * Collaborative comment thread for a single widget, rendered as a slide-over\n * panel inside the widget container. Supports posting top-level comments and\n * threaded replies, resolving/reopening, and (soft) deleting. Data flows through\n * useWidgetComments → the federated EntityComment API.\n */\n\nimport { type FC, useState, useCallback } from 'react';\nimport { MessageSquare, Send, Check, RotateCcw, Trash2, X, CornerDownRight, Loader2 } from 'lucide-react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useWidgetComments, type WidgetCommentThread } from '../../../hooks/useWidgetComments';\nimport type { EntityCommentFieldsFragment } from '../../../../generated/wspace-operations';\n\nexport interface WidgetCommentsProps {\n widgetId: string;\n /** Close the panel. */\n onClose: () => void;\n}\n\nfunction authorLabel(\n t: (\n key: string,\n fallbackOrParams?: string | Record<string, string | number>,\n params?: Record<string, string | number>\n ) => string,\n authorId: string\n): string {\n // We only have an opaque actor id here; show a short, stable handle.\n return t('bigconsole.widget.comments.authorLabel', 'User {{id}}', { id: authorId.slice(0, 6) });\n}\n\nfunction formatWhen(iso: string): string {\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return '';\n return new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' }).format(d);\n}\n\ninterface CommentItemProps {\n comment: EntityCommentFieldsFragment;\n isReply?: boolean;\n busy: boolean;\n onResolve: (id: string, resolved: boolean) => void;\n onDelete: (id: string) => void;\n onReply?: (id: string) => void;\n}\n\nconst CommentItem: FC<CommentItemProps> = ({ comment, isReply, busy, onResolve, onDelete, onReply }) => {\n const { t } = useI18n();\n const resolved = !!comment.resolvedAt;\n const body = comment.content.text ?? comment.content.markdown ?? '';\n return (\n <div className={isReply ? 'pl-6' : ''}>\n <div className=\"rounded-md border border-[var(--color-border-default)] bg-bg-surface p-2.5\">\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"text-xs font-medium text-text-primary\">{authorLabel(t, comment.authorId)}</span>\n <span className=\"text-[10px] text-text-muted tabular-nums\">{formatWhen(comment.createdAt)}</span>\n </div>\n <p className=\"mt-1 whitespace-pre-wrap break-words text-sm text-text-secondary\">{body}</p>\n <div className=\"mt-1.5 flex items-center gap-2\">\n {resolved && (\n <span className=\"inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] font-medium bg-status-success-bg text-status-success-text\">\n <Check className=\"h-3 w-3\" /> {t('bigconsole.widget.comments.resolvedBadge', 'Resolved')}\n </span>\n )}\n {onReply && (\n <button\n type=\"button\"\n disabled={busy}\n onClick={() => onReply(comment.id)}\n className=\"inline-flex items-center gap-1 text-[11px] text-text-muted hover:text-text-primary disabled:opacity-50\"\n >\n <CornerDownRight className=\"h-3 w-3\" /> {t('bigconsole.widget.comments.reply', 'Reply')}\n </button>\n )}\n <button\n type=\"button\"\n disabled={busy}\n onClick={() => onResolve(comment.id, !resolved)}\n className=\"inline-flex items-center gap-1 text-[11px] text-text-muted hover:text-text-primary disabled:opacity-50\"\n >\n {resolved ? <RotateCcw className=\"h-3 w-3\" /> : <Check className=\"h-3 w-3\" />}\n {resolved\n ? t('bigconsole.widget.comments.reopen', 'Reopen')\n : t('bigconsole.widget.comments.resolve', 'Resolve')}\n </button>\n <button\n type=\"button\"\n disabled={busy}\n onClick={() => onDelete(comment.id)}\n className=\"inline-flex items-center gap-1 text-[11px] text-status-error-text hover:opacity-80 disabled:opacity-50\"\n >\n <Trash2 className=\"h-3 w-3\" /> {t('bigconsole.widget.comments.delete', 'Delete')}\n </button>\n </div>\n </div>\n </div>\n );\n};\n\ninterface ComposerProps {\n placeholder: string;\n busy: boolean;\n autoFocus?: boolean;\n /** Returns false when the post failed — the draft is kept so it isn't lost. */\n onSubmit: (text: string) => boolean | void | Promise<boolean | void>;\n onCancel?: () => void;\n}\n\nconst Composer: FC<ComposerProps> = ({ placeholder, busy, autoFocus, onSubmit, onCancel }) => {\n const { t } = useI18n();\n const [text, setText] = useState('');\n const submit = async () => {\n const v = text.trim();\n if (!v) return;\n // Only clear once the post succeeds; on failure the user keeps their draft.\n const ok = await onSubmit(v);\n if (ok !== false) setText('');\n };\n return (\n <div className=\"flex items-end gap-2\">\n <textarea\n autoFocus={autoFocus}\n value={text}\n onChange={(e) => setText(e.target.value)}\n onKeyDown={(e) => {\n // The composer lives inside an edit-mode widget, whose keyboard\n // handler consumes Space to select the widget. Keep text editing\n // events within the textarea so spaces and other input are intact.\n e.stopPropagation();\n if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {\n e.preventDefault();\n void submit();\n }\n }}\n rows={2}\n placeholder={placeholder}\n className=\"flex-1 resize-none rounded-md border border-[var(--color-border-default)] bg-bg-surface px-2.5 py-1.5 text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-action-primary-bg\"\n />\n <div className=\"flex flex-col gap-1\">\n <button\n type=\"button\"\n disabled={busy || !text.trim()}\n onClick={() => void submit()}\n title={t('bigconsole.widget.comments.postTitle', 'Post (⌘/Ctrl + Enter)')}\n className=\"inline-flex items-center justify-center rounded-md bg-action-primary-bg p-1.5 text-action-primary-text hover:opacity-90 disabled:opacity-50\"\n >\n {busy ? <Loader2 className=\"h-4 w-4 animate-spin\" /> : <Send className=\"h-4 w-4\" />}\n </button>\n {onCancel && (\n <button\n type=\"button\"\n onClick={onCancel}\n className=\"inline-flex items-center justify-center rounded-md border border-[var(--color-border-default)] p-1.5 text-text-muted hover:text-text-primary\"\n title={t('bigconsole.widget.comments.cancelReplyTitle', 'Cancel reply')}\n >\n <X className=\"h-4 w-4\" />\n </button>\n )}\n </div>\n </div>\n );\n};\n\nexport const WidgetComments: FC<WidgetCommentsProps> = ({ widgetId, onClose }) => {\n const { t } = useI18n();\n const { threads, count, loading, error, mutating, addComment, resolveComment, deleteComment } =\n useWidgetComments(widgetId);\n const [replyTo, setReplyTo] = useState<string | null>(null);\n\n const handleAdd = useCallback(\n async (text: string, parentId?: string): Promise<boolean> => {\n const ok = await addComment(text, parentId);\n if (ok && parentId) setReplyTo(null);\n return ok;\n },\n [addComment]\n );\n\n return (\n <div\n className=\"absolute inset-0 z-40 flex flex-col bg-bg-surface/98 backdrop-blur-sm\"\n role=\"dialog\"\n aria-label={t('bigconsole.widget.comments.dialogAriaLabel', 'Widget comments')}\n tabIndex={-1}\n onClick={(e) => e.stopPropagation()}\n onKeyDown={(e) => {\n if (e.key === 'Escape') {\n e.stopPropagation();\n onClose();\n }\n }}\n >\n {/*\n Header. Matches the widget card's own title band exactly — same padding,\n same seam, same semibold title — so opening comments does not change the\n card's header grammar mid-flight (BOFF-5636).\n */}\n <div className=\"flex min-w-0 items-center justify-between gap-2 border-b border-[var(--color-border-subtle)] bg-bg-surface px-3 py-2 sm:px-4\">\n <h3 className=\"flex min-w-0 items-center gap-2 text-sm font-semibold leading-tight tracking-tight text-text-primary\">\n <MessageSquare aria-hidden=\"true\" className=\"h-icon-sm w-icon-sm shrink-0 text-text-secondary\" />\n {t('bigconsole.widget.comments.title', 'Comments')}\n <span className=\"rounded-full bg-bg-sunken px-1.5 py-0.5 text-[10px] text-text-secondary tabular-nums\">\n {count}\n </span>\n </h3>\n <button\n type=\"button\"\n onClick={onClose}\n aria-label={t('bigconsole.widget.comments.closeAriaLabel', 'Close comments')}\n className=\"shrink-0 rounded-button p-1 text-text-muted hover:bg-bg-sunken hover:text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-bg-surface\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n </div>\n\n {/* Thread list */}\n <div className=\"flex-1 space-y-3 overflow-auto p-3\">\n {loading && threads.length === 0 && (\n <div className=\"flex items-center justify-center py-8 text-text-muted\">\n <Loader2 className=\"h-5 w-5 animate-spin\" />\n </div>\n )}\n {error && !loading && (\n <p\n role=\"alert\"\n className=\"rounded-input border border-[var(--color-status-error-border)] bg-status-error-bg-subtle px-2.5 py-2 text-xs text-[var(--color-status-error-text)]\"\n >\n {t('bigconsole.widget.comments.loadError', 'Couldn’t load comments.')}\n </p>\n )}\n {!loading && !error && threads.length === 0 && (\n <p className=\"py-8 text-center text-sm text-text-muted\">\n {t('bigconsole.widget.comments.empty', 'No comments yet. Start the conversation.')}\n </p>\n )}\n {threads.map(({ comment, replies }: WidgetCommentThread) => (\n <div key={comment.id} className=\"space-y-2\">\n <CommentItem\n comment={comment}\n busy={mutating}\n onResolve={(id, r) => void resolveComment(id, r)}\n onDelete={(id) => void deleteComment(id)}\n onReply={(id) => setReplyTo((cur) => (cur === id ? null : id))}\n />\n {replies.map((reply) => (\n <CommentItem\n key={reply.id}\n comment={reply}\n isReply\n busy={mutating}\n onResolve={(id, r) => void resolveComment(id, r)}\n onDelete={(id) => void deleteComment(id)}\n />\n ))}\n {replyTo === comment.id && (\n <div className=\"pl-6\">\n <Composer\n placeholder={t('bigconsole.widget.comments.replyPlaceholder', 'Write a reply…')}\n busy={mutating}\n autoFocus\n onSubmit={(text) => handleAdd(text, comment.id)}\n onCancel={() => setReplyTo(null)}\n />\n </div>\n )}\n </div>\n ))}\n </div>\n\n {/* New top-level comment */}\n <div className=\"border-t border-[var(--color-border-default)] p-3\">\n <Composer\n placeholder={t('bigconsole.widget.comments.addPlaceholder', 'Add a comment…')}\n busy={mutating}\n onSubmit={(text) => handleAdd(text)}\n />\n </div>\n </div>\n );\n};\n\nexport default WidgetComments;\n"],"mappings":"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"}
1
+ {"version":3,"file":"WidgetComments.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/comments/WidgetComments.tsx"],"sourcesContent":["/**\n * WidgetComments\n *\n * Collaborative comment thread for a single widget, rendered as a slide-over\n * panel inside the widget container. Supports posting top-level comments and\n * threaded replies, resolving/reopening, and (soft) deleting. Data flows through\n * useWidgetComments → the federated EntityComment API.\n */\n\nimport { type FC, useState, useCallback, useEffect, useRef } from 'react';\nimport { MessageSquare, Send, Check, RotateCcw, Trash2, X, CornerDownRight, Loader2 } from 'lucide-react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useWidgetComments, type WidgetCommentThread } from '../../../hooks/useWidgetComments';\nimport type { EntityCommentFieldsFragment } from '../../../../generated/wspace-operations';\n\nexport interface WidgetCommentsProps {\n widgetId: string;\n /** Close the panel. */\n onClose: () => void;\n}\n\nfunction authorLabel(\n t: (\n key: string,\n fallbackOrParams?: string | Record<string, string | number>,\n params?: Record<string, string | number>\n ) => string,\n authorId: string\n): string {\n // We only have an opaque actor id here; show a short, stable handle.\n return t('bigconsole.widget.comments.authorLabel', 'User {{id}}', { id: authorId.slice(0, 6) });\n}\n\nfunction formatWhen(iso: string): string {\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return '';\n return new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' }).format(d);\n}\n\ninterface CommentItemProps {\n comment: EntityCommentFieldsFragment;\n isReply?: boolean;\n busy: boolean;\n onResolve: (id: string, resolved: boolean) => void;\n onDelete: (id: string) => void;\n onReply?: (id: string) => void;\n}\n\nconst CommentItem: FC<CommentItemProps> = ({ comment, isReply, busy, onResolve, onDelete, onReply }) => {\n const { t } = useI18n();\n const resolved = !!comment.resolvedAt;\n const body = comment.content.text ?? comment.content.markdown ?? '';\n return (\n <div className={isReply ? 'pl-6' : ''}>\n <div className=\"rounded-md border border-[var(--color-border-default)] bg-bg-surface p-2.5\">\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"text-xs font-medium text-text-primary\">{authorLabel(t, comment.authorId)}</span>\n <span className=\"text-[10px] text-text-muted tabular-nums\">{formatWhen(comment.createdAt)}</span>\n </div>\n <p className=\"mt-1 whitespace-pre-wrap break-words text-sm text-text-secondary\">{body}</p>\n <div className=\"mt-1.5 flex items-center gap-2\">\n {resolved && (\n <span className=\"inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] font-medium bg-status-success-bg text-status-success-text\">\n <Check className=\"h-3 w-3\" /> {t('bigconsole.widget.comments.resolvedBadge', 'Resolved')}\n </span>\n )}\n {onReply && (\n <button\n type=\"button\"\n disabled={busy}\n onClick={() => onReply(comment.id)}\n className=\"inline-flex items-center gap-1 text-[11px] text-text-muted hover:text-text-primary disabled:opacity-50\"\n >\n <CornerDownRight className=\"h-3 w-3\" /> {t('bigconsole.widget.comments.reply', 'Reply')}\n </button>\n )}\n <button\n type=\"button\"\n disabled={busy}\n onClick={() => onResolve(comment.id, !resolved)}\n className=\"inline-flex items-center gap-1 text-[11px] text-text-muted hover:text-text-primary disabled:opacity-50\"\n >\n {resolved ? <RotateCcw className=\"h-3 w-3\" /> : <Check className=\"h-3 w-3\" />}\n {resolved\n ? t('bigconsole.widget.comments.reopen', 'Reopen')\n : t('bigconsole.widget.comments.resolve', 'Resolve')}\n </button>\n <button\n type=\"button\"\n disabled={busy}\n onClick={() => onDelete(comment.id)}\n className=\"inline-flex items-center gap-1 text-[11px] text-status-error-text hover:opacity-80 disabled:opacity-50\"\n >\n <Trash2 className=\"h-3 w-3\" /> {t('bigconsole.widget.comments.delete', 'Delete')}\n </button>\n </div>\n </div>\n </div>\n );\n};\n\ninterface ComposerProps {\n placeholder: string;\n busy: boolean;\n autoFocus?: boolean;\n /** Returns false when the post failed — the draft is kept so it isn't lost. */\n onSubmit: (text: string) => boolean | void | Promise<boolean | void>;\n onCancel?: () => void;\n}\n\nconst Composer: FC<ComposerProps> = ({ placeholder, busy, autoFocus, onSubmit, onCancel }) => {\n const { t } = useI18n();\n const [text, setText] = useState('');\n const submit = async () => {\n const v = text.trim();\n if (!v) return;\n // Only clear once the post succeeds; on failure the user keeps their draft.\n const ok = await onSubmit(v);\n if (ok !== false) setText('');\n };\n return (\n <div className=\"flex items-end gap-2\">\n <textarea\n autoFocus={autoFocus}\n value={text}\n onChange={(e) => setText(e.target.value)}\n onKeyDown={(e) => {\n // The composer lives inside an edit-mode widget, whose keyboard\n // handler consumes Space to select the widget. Keep text editing\n // events within the textarea so spaces and other input are intact.\n e.stopPropagation();\n if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {\n e.preventDefault();\n void submit();\n }\n }}\n rows={2}\n placeholder={placeholder}\n className=\"flex-1 resize-none rounded-md border border-[var(--color-border-default)] bg-bg-surface px-2.5 py-1.5 text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-action-primary-bg\"\n />\n <div className=\"flex flex-col gap-1\">\n <button\n type=\"button\"\n disabled={busy || !text.trim()}\n onClick={() => void submit()}\n title={t('bigconsole.widget.comments.postTitle', 'Post (⌘/Ctrl + Enter)')}\n className=\"inline-flex items-center justify-center rounded-md bg-action-primary-bg p-1.5 text-action-primary-text hover:opacity-90 disabled:opacity-50\"\n >\n {busy ? <Loader2 className=\"h-4 w-4 animate-spin\" /> : <Send className=\"h-4 w-4\" />}\n </button>\n {onCancel && (\n <button\n type=\"button\"\n onClick={onCancel}\n className=\"inline-flex items-center justify-center rounded-md border border-[var(--color-border-default)] p-1.5 text-text-muted hover:text-text-primary\"\n title={t('bigconsole.widget.comments.cancelReplyTitle', 'Cancel reply')}\n >\n <X className=\"h-4 w-4\" />\n </button>\n )}\n </div>\n </div>\n );\n};\n\nexport const WidgetComments: FC<WidgetCommentsProps> = ({ widgetId, onClose }) => {\n const { t } = useI18n();\n const { threads, count, loading, error, mutating, addComment, resolveComment, deleteComment } =\n useWidgetComments(widgetId);\n const [replyTo, setReplyTo] = useState<string | null>(null);\n const [scrollToNewest, setScrollToNewest] = useState(false);\n const threadListRef = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n if (!scrollToNewest) return;\n\n const threadList = threadListRef.current;\n threadList?.scrollTo({ top: threadList.scrollHeight, behavior: 'smooth' });\n setScrollToNewest(false);\n }, [scrollToNewest, threads]);\n\n const handleAdd = useCallback(\n async (text: string, parentId?: string): Promise<boolean> => {\n const ok = await addComment(text, parentId);\n if (ok) {\n if (parentId) setReplyTo(null);\n else setScrollToNewest(true);\n }\n return ok;\n },\n [addComment]\n );\n\n return (\n <div\n className=\"absolute inset-0 z-40 flex flex-col bg-bg-surface/98 backdrop-blur-sm\"\n role=\"dialog\"\n aria-label={t('bigconsole.widget.comments.dialogAriaLabel', 'Widget comments')}\n tabIndex={-1}\n onClick={(e) => e.stopPropagation()}\n onKeyDown={(e) => {\n if (e.key === 'Escape') {\n e.stopPropagation();\n onClose();\n }\n }}\n >\n {/*\n Header. Matches the widget card's own title band exactly — same padding,\n same seam, same semibold title — so opening comments does not change the\n card's header grammar mid-flight (BOFF-5636).\n */}\n <div className=\"flex min-w-0 items-center justify-between gap-2 border-b border-[var(--color-border-subtle)] bg-bg-surface px-3 py-2 sm:px-4\">\n <h3 className=\"flex min-w-0 items-center gap-2 text-sm font-semibold leading-tight tracking-tight text-text-primary\">\n <MessageSquare aria-hidden=\"true\" className=\"h-icon-sm w-icon-sm shrink-0 text-text-secondary\" />\n {t('bigconsole.widget.comments.title', 'Comments')}\n <span className=\"rounded-full bg-bg-sunken px-1.5 py-0.5 text-[10px] text-text-secondary tabular-nums\">\n {count}\n </span>\n </h3>\n <button\n type=\"button\"\n onClick={onClose}\n aria-label={t('bigconsole.widget.comments.closeAriaLabel', 'Close comments')}\n className=\"shrink-0 rounded-button p-1 text-text-muted hover:bg-bg-sunken hover:text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-bg-surface\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n </div>\n\n {/* Thread list */}\n <div ref={threadListRef} className=\"flex-1 space-y-3 overflow-auto p-3\">\n {loading && threads.length === 0 && (\n <div className=\"flex items-center justify-center py-8 text-text-muted\">\n <Loader2 className=\"h-5 w-5 animate-spin\" />\n </div>\n )}\n {error && !loading && (\n <p\n role=\"alert\"\n className=\"rounded-input border border-[var(--color-status-error-border)] bg-status-error-bg-subtle px-2.5 py-2 text-xs text-[var(--color-status-error-text)]\"\n >\n {t('bigconsole.widget.comments.loadError', 'Couldn’t load comments.')}\n </p>\n )}\n {!loading && !error && threads.length === 0 && (\n <p className=\"py-8 text-center text-sm text-text-muted\">\n {t('bigconsole.widget.comments.empty', 'No comments yet. Start the conversation.')}\n </p>\n )}\n {threads.map(({ comment, replies }: WidgetCommentThread) => (\n <div key={comment.id} className=\"space-y-2\">\n <CommentItem\n comment={comment}\n busy={mutating}\n onResolve={(id, r) => void resolveComment(id, r)}\n onDelete={(id) => void deleteComment(id)}\n onReply={(id) => setReplyTo((cur) => (cur === id ? null : id))}\n />\n {replies.map((reply) => (\n <CommentItem\n key={reply.id}\n comment={reply}\n isReply\n busy={mutating}\n onResolve={(id, r) => void resolveComment(id, r)}\n onDelete={(id) => void deleteComment(id)}\n />\n ))}\n {replyTo === comment.id && (\n <div className=\"pl-6\">\n <Composer\n placeholder={t('bigconsole.widget.comments.replyPlaceholder', 'Write a reply…')}\n busy={mutating}\n autoFocus\n onSubmit={(text) => handleAdd(text, comment.id)}\n onCancel={() => setReplyTo(null)}\n />\n </div>\n )}\n </div>\n ))}\n </div>\n\n {/* New top-level comment */}\n <div className=\"border-t border-[var(--color-border-default)] p-3\">\n <Composer\n placeholder={t('bigconsole.widget.comments.addPlaceholder', 'Add a comment…')}\n busy={mutating}\n onSubmit={(text) => handleAdd(text)}\n />\n </div>\n </div>\n );\n};\n\nexport default WidgetComments;\n"],"mappings":"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"}
@@ -1,22 +1,31 @@
1
1
  import { useCreateEntityCommentMutation as e, useDeleteEntityCommentMutation as t, useGetEntityCommentsQuery as n, useResolveEntityCommentMutation as r } from "../../generated/wspace-operations.js";
2
- import { useCallback as i, useMemo as a } from "react";
2
+ import { useCallback as i, useMemo as a, useState as o } from "react";
3
3
  //#region src/bigconsole/hooks/useWidgetComments.ts
4
- var o = "bigconsole-widget";
5
- function s(s, c = !0) {
6
- let { data: l, loading: u, error: d, refetch: f } = n({
4
+ var s = "bigconsole-widget";
5
+ function c(c, l = !0) {
6
+ let { data: u, loading: d, error: f, refetch: p } = n({
7
7
  variables: { input: {
8
- entityId: s ?? "",
9
- entityType: o,
8
+ entityId: c ?? "",
9
+ entityType: s,
10
10
  take: 100,
11
11
  sortBy: "createdAt",
12
12
  sortOrder: "asc"
13
13
  } },
14
- skip: !c || !s,
14
+ skip: !l || !c,
15
15
  fetchPolicy: "cache-and-network",
16
16
  notifyOnNetworkStatusChange: !0
17
- }), [p, m] = e(), [h, g] = r(), [_, v] = t(), y = a(() => l?.getEntityComments.items ?? [], [l]), b = l?.getEntityComments.total ?? y.length, x = a(() => {
18
- let e = y.filter((e) => !e.parentCommentId), t = /* @__PURE__ */ new Map();
19
- for (let e of y) if (e.parentCommentId) {
17
+ }), [m, h] = e(), [g, _] = r(), [v, y] = t(), [b, x] = o([]), S = a(() => {
18
+ let e = /* @__PURE__ */ new Map();
19
+ for (let t of u?.getEntityComments.items ?? []) e.set(t.id, t);
20
+ for (let t of b) t.entityId === c && e.set(t.id, t);
21
+ return [...e.values()].sort((e, t) => e.createdAt.localeCompare(t.createdAt));
22
+ }, [
23
+ b,
24
+ u,
25
+ c
26
+ ]), C = u?.getEntityComments.total ?? S.length, w = a(() => {
27
+ let e = S.filter((e) => !e.parentCommentId), t = /* @__PURE__ */ new Map();
28
+ for (let e of S) if (e.parentCommentId) {
20
29
  let n = t.get(e.parentCommentId) ?? [];
21
30
  n.push(e), t.set(e.parentCommentId, n);
22
31
  }
@@ -24,58 +33,60 @@ function s(s, c = !0) {
24
33
  comment: e,
25
34
  replies: t.get(e.id) ?? []
26
35
  }));
27
- }, [y]), S = i(async (e, t) => {
36
+ }, [S]), T = i(async (e, t) => {
28
37
  let n = e.trim();
29
- if (!n || !s) return !1;
38
+ if (!n || !c) return !1;
30
39
  try {
31
- return (await p({ variables: { input: {
32
- entityId: s,
33
- entityType: "bigconsole-widget",
40
+ let e = (await m({ variables: { input: {
41
+ entityId: c,
42
+ entityType: s,
34
43
  content: { text: n },
35
44
  contentType: "TEXT",
36
45
  ...t ? { parentCommentId: t } : {}
37
- } } })).data?.createEntityComment ? (await f(), !0) : !1;
46
+ } } })).data?.createEntityComment;
47
+ return e ? (x((t) => t.some((t) => t.id === e.id) ? t : [...t, e]), await p(), !0) : !1;
38
48
  } catch {
39
49
  return !1;
40
50
  }
41
51
  }, [
52
+ m,
42
53
  p,
43
- f,
44
- s
45
- ]), C = i(async (e, t) => {
54
+ c
55
+ ]), E = i(async (e, t) => {
46
56
  try {
47
- return (await h({ variables: { input: {
57
+ let n = (await g({ variables: { input: {
48
58
  id: e,
49
59
  resolved: t
50
- } } })).data?.resolveEntityComment ? (await f(), !0) : !1;
60
+ } } })).data?.resolveEntityComment;
61
+ return n ? (x((e) => e.map((e) => e.id === n.id ? n : e)), await p(), !0) : !1;
51
62
  } catch {
52
63
  return !1;
53
64
  }
54
- }, [h, f]), w = i(async (e) => {
65
+ }, [g, p]), D = i(async (e) => {
55
66
  try {
56
- return (await _({ variables: { input: {
67
+ return (await v({ variables: { input: {
57
68
  id: e,
58
69
  soft: !0
59
- } } })).data?.deleteEntityComment ? (await f(), !0) : !1;
70
+ } } })).data?.deleteEntityComment ? (x((t) => t.filter((t) => t.id !== e)), await p(), !0) : !1;
60
71
  } catch {
61
72
  return !1;
62
73
  }
63
- }, [_, f]);
74
+ }, [v, p]);
64
75
  return {
65
- threads: x,
66
- count: b,
67
- loading: u,
68
- error: d,
69
- mutating: m.loading || g.loading || v.loading,
70
- addComment: S,
71
- resolveComment: C,
72
- deleteComment: w,
76
+ threads: w,
77
+ count: C,
78
+ loading: d,
79
+ error: f,
80
+ mutating: h.loading || _.loading || y.loading,
81
+ addComment: T,
82
+ resolveComment: E,
83
+ deleteComment: D,
73
84
  refetch: i(() => {
74
- f();
75
- }, [f])
85
+ p();
86
+ }, [p])
76
87
  };
77
88
  }
78
89
  //#endregion
79
- export { o as WIDGET_COMMENT_ENTITY_TYPE, s as default };
90
+ export { s as WIDGET_COMMENT_ENTITY_TYPE, c as default };
80
91
 
81
92
  //# sourceMappingURL=useWidgetComments.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useWidgetComments.js","names":[],"sources":["../../../src/bigconsole/hooks/useWidgetComments.ts"],"sourcesContent":["/**\n * useWidgetComments Hook\n *\n * Comment thread for a single widget, backed by the federated polymorphic\n * EntityComment API (module-conversations-svc) with `entityType` =\n * `bigconsole-widget`. Returns the flat list plus a parent/child threaded\n * view, and create / resolve / delete actions that refetch on success.\n *\n * The query fragment is intentionally slim (no nested replies/reactions/files)\n * to stay under the gateway cost-limit; threads are assembled here from\n * `parentCommentId`.\n */\n\nimport { useCallback, useMemo } from 'react';\nimport {\n useGetEntityCommentsQuery,\n useCreateEntityCommentMutation,\n useResolveEntityCommentMutation,\n useDeleteEntityCommentMutation,\n} from '../../generated/wspace-operations';\nimport type { EntityCommentFieldsFragment } from '../../generated/wspace-operations';\n\n/** Entity type used to scope comments to a BigConsole widget. */\nexport const WIDGET_COMMENT_ENTITY_TYPE = 'bigconsole-widget';\n\n/** A top-level comment with its (one level of) replies attached. */\nexport interface WidgetCommentThread {\n comment: EntityCommentFieldsFragment;\n replies: EntityCommentFieldsFragment[];\n}\n\nexport interface UseWidgetCommentsResult {\n threads: WidgetCommentThread[];\n /** Total comment count (top-level + replies), excluding deleted. */\n count: number;\n loading: boolean;\n error?: Error;\n mutating: boolean;\n addComment: (text: string, parentCommentId?: string) => Promise<boolean>;\n resolveComment: (id: string, resolved: boolean) => Promise<boolean>;\n deleteComment: (id: string) => Promise<boolean>;\n refetch: () => void;\n}\n\nexport function useWidgetComments(widgetId: string | undefined, enabled = true): UseWidgetCommentsResult {\n const skip = !enabled || !widgetId;\n\n const { data, loading, error, refetch } = useGetEntityCommentsQuery({\n variables: {\n input: {\n entityId: widgetId ?? '',\n entityType: WIDGET_COMMENT_ENTITY_TYPE,\n take: 100,\n sortBy: 'createdAt',\n sortOrder: 'asc',\n },\n },\n skip,\n fetchPolicy: 'cache-and-network',\n notifyOnNetworkStatusChange: true,\n });\n\n const [createComment, createState] = useCreateEntityCommentMutation();\n const [resolveCommentMutation, resolveState] = useResolveEntityCommentMutation();\n const [deleteCommentMutation, deleteState] = useDeleteEntityCommentMutation();\n\n const items: EntityCommentFieldsFragment[] = useMemo(() => data?.getEntityComments.items ?? [], [data]);\n // Use the server-reported total for the count (items is capped at the page size).\n const total = data?.getEntityComments.total ?? items.length;\n\n // Reassemble a flat list into top-level threads + their direct replies.\n const threads = useMemo<WidgetCommentThread[]>(() => {\n const roots = items.filter((c) => !c.parentCommentId);\n const repliesByParent = new Map<string, EntityCommentFieldsFragment[]>();\n for (const c of items) {\n if (c.parentCommentId) {\n const list = repliesByParent.get(c.parentCommentId) ?? [];\n list.push(c);\n repliesByParent.set(c.parentCommentId, list);\n }\n }\n return roots.map((comment) => ({ comment, replies: repliesByParent.get(comment.id) ?? [] }));\n }, [items]);\n\n // All actions catch their own errors and resolve to a boolean (never reject),\n // so callers can `await` for success/failure without unhandled rejections and\n // can keep the user's draft when a post fails.\n const addComment = useCallback(\n async (text: string, parentCommentId?: string): Promise<boolean> => {\n const body = text.trim();\n if (!body || !widgetId) return false;\n try {\n const res = await createComment({\n variables: {\n input: {\n entityId: widgetId,\n entityType: WIDGET_COMMENT_ENTITY_TYPE,\n content: { text: body },\n contentType: 'TEXT',\n ...(parentCommentId ? { parentCommentId } : {}),\n },\n },\n });\n if (res.data?.createEntityComment) {\n await refetch();\n return true;\n }\n return false;\n } catch {\n return false;\n }\n },\n [createComment, refetch, widgetId]\n );\n\n const resolveComment = useCallback(\n async (id: string, resolved: boolean): Promise<boolean> => {\n try {\n const res = await resolveCommentMutation({ variables: { input: { id, resolved } } });\n if (res.data?.resolveEntityComment) {\n await refetch();\n return true;\n }\n return false;\n } catch {\n return false;\n }\n },\n [resolveCommentMutation, refetch]\n );\n\n const deleteComment = useCallback(\n async (id: string): Promise<boolean> => {\n try {\n const res = await deleteCommentMutation({ variables: { input: { id, soft: true } } });\n if (res.data?.deleteEntityComment) {\n await refetch();\n return true;\n }\n return false;\n } catch {\n return false;\n }\n },\n [deleteCommentMutation, refetch]\n );\n\n return {\n threads,\n count: total,\n loading,\n error: error as Error | undefined,\n mutating: createState.loading || resolveState.loading || deleteState.loading,\n addComment,\n resolveComment,\n deleteComment,\n refetch: useCallback(() => {\n void refetch();\n }, [refetch]),\n };\n}\n\nexport default useWidgetComments;\n"],"mappings":";;;AAuBA,IAAa,IAA6B;AAqB1C,SAAgB,EAAkB,GAA8B,IAAU,IAA+B;CAGvG,IAAM,EAAE,SAAM,YAAS,UAAO,eAAY,EAA0B;EAClE,WAAW,EACT,OAAO;GACL,UAAU,KAAY;GACtB,YAAY;GACZ,MAAM;GACN,QAAQ;GACR,WAAW;GACZ,EACF;EACD,MAZW,CAAC,KAAW,CAAC;EAaxB,aAAa;EACb,6BAA6B;EAC9B,CAAC,EAEI,CAAC,GAAe,KAAe,GAAgC,EAC/D,CAAC,GAAwB,KAAgB,GAAiC,EAC1E,CAAC,GAAuB,KAAe,GAAgC,EAEvE,IAAuC,QAAc,GAAM,kBAAkB,SAAS,EAAE,EAAE,CAAC,EAAK,CAAC,EAEjG,IAAQ,GAAM,kBAAkB,SAAS,EAAM,QAG/C,IAAU,QAAqC;EACnD,IAAM,IAAQ,EAAM,QAAQ,MAAM,CAAC,EAAE,gBAAgB,EAC/C,oBAAkB,IAAI,KAA4C;AACxE,OAAK,IAAM,KAAK,EACd,KAAI,EAAE,iBAAiB;GACrB,IAAM,IAAO,EAAgB,IAAI,EAAE,gBAAgB,IAAI,EAAE;AAEzD,GADA,EAAK,KAAK,EAAE,EACZ,EAAgB,IAAI,EAAE,iBAAiB,EAAK;;AAGhD,SAAO,EAAM,KAAK,OAAa;GAAE;GAAS,SAAS,EAAgB,IAAI,EAAQ,GAAG,IAAI,EAAE;GAAE,EAAE;IAC3F,CAAC,EAAM,CAAC,EAKL,IAAa,EACjB,OAAO,GAAc,MAA+C;EAClE,IAAM,IAAO,EAAK,MAAM;AACxB,MAAI,CAAC,KAAQ,CAAC,EAAU,QAAO;AAC/B,MAAI;AAgBF,WAfY,MAAM,EAAc,EAC9B,WAAW,EACT,OAAO;IACL,UAAU;IACV,YAAA;IACA,SAAS,EAAE,MAAM,GAAM;IACvB,aAAa;IACb,GAAI,IAAkB,EAAE,oBAAiB,GAAG,EAAE;IAC/C,EACF,EACF,CAAC,EACM,MAAM,uBACZ,MAAM,GAAS,EACR,MAEF;UACD;AACN,UAAO;;IAGX;EAAC;EAAe;EAAS;EAAS,CACnC,EAEK,IAAiB,EACrB,OAAO,GAAY,MAAwC;AACzD,MAAI;AAMF,WALY,MAAM,EAAuB,EAAE,WAAW,EAAE,OAAO;IAAE;IAAI;IAAU,EAAE,EAAE,CAAC,EAC5E,MAAM,wBACZ,MAAM,GAAS,EACR,MAEF;UACD;AACN,UAAO;;IAGX,CAAC,GAAwB,EAAQ,CAClC,EAEK,IAAgB,EACpB,OAAO,MAAiC;AACtC,MAAI;AAMF,WALY,MAAM,EAAsB,EAAE,WAAW,EAAE,OAAO;IAAE;IAAI,MAAM;IAAM,EAAE,EAAE,CAAC,EAC7E,MAAM,uBACZ,MAAM,GAAS,EACR,MAEF;UACD;AACN,UAAO;;IAGX,CAAC,GAAuB,EAAQ,CACjC;AAED,QAAO;EACL;EACA,OAAO;EACP;EACO;EACP,UAAU,EAAY,WAAW,EAAa,WAAW,EAAY;EACrE;EACA;EACA;EACA,SAAS,QAAkB;AACpB,MAAS;KACb,CAAC,EAAQ,CAAC;EACd"}
1
+ {"version":3,"file":"useWidgetComments.js","names":[],"sources":["../../../src/bigconsole/hooks/useWidgetComments.ts"],"sourcesContent":["/**\n * useWidgetComments Hook\n *\n * Comment thread for a single widget, backed by the federated polymorphic\n * EntityComment API (module-conversations-svc) with `entityType` =\n * `bigconsole-widget`. Returns the flat list plus a parent/child threaded\n * view, and create / resolve / delete actions that refetch on success. Created\n * comments are retained locally after the refetch so the newest comment stays\n * visible even when the server page is full.\n *\n * The query fragment is intentionally slim (no nested replies/reactions/files)\n * to stay under the gateway cost-limit; threads are assembled here from\n * `parentCommentId`.\n */\n\nimport { useCallback, useMemo, useState } from 'react';\nimport {\n useGetEntityCommentsQuery,\n useCreateEntityCommentMutation,\n useResolveEntityCommentMutation,\n useDeleteEntityCommentMutation,\n} from '../../generated/wspace-operations';\nimport type { EntityCommentFieldsFragment } from '../../generated/wspace-operations';\n\n/** Entity type used to scope comments to a BigConsole widget. */\nexport const WIDGET_COMMENT_ENTITY_TYPE = 'bigconsole-widget';\n\n/** A top-level comment with its (one level of) replies attached. */\nexport interface WidgetCommentThread {\n comment: EntityCommentFieldsFragment;\n replies: EntityCommentFieldsFragment[];\n}\n\nexport interface UseWidgetCommentsResult {\n threads: WidgetCommentThread[];\n /** Total comment count (top-level + replies), excluding deleted. */\n count: number;\n loading: boolean;\n error?: Error;\n mutating: boolean;\n addComment: (text: string, parentCommentId?: string) => Promise<boolean>;\n resolveComment: (id: string, resolved: boolean) => Promise<boolean>;\n deleteComment: (id: string) => Promise<boolean>;\n refetch: () => void;\n}\n\nexport function useWidgetComments(widgetId: string | undefined, enabled = true): UseWidgetCommentsResult {\n const skip = !enabled || !widgetId;\n\n const { data, loading, error, refetch } = useGetEntityCommentsQuery({\n variables: {\n input: {\n entityId: widgetId ?? '',\n entityType: WIDGET_COMMENT_ENTITY_TYPE,\n take: 100,\n sortBy: 'createdAt',\n sortOrder: 'asc',\n },\n },\n skip,\n fetchPolicy: 'cache-and-network',\n notifyOnNetworkStatusChange: true,\n });\n\n const [createComment, createState] = useCreateEntityCommentMutation();\n const [resolveCommentMutation, resolveState] = useResolveEntityCommentMutation();\n const [deleteCommentMutation, deleteState] = useDeleteEntityCommentMutation();\n const [createdComments, setCreatedComments] = useState<EntityCommentFieldsFragment[]>([]);\n\n // Keep comments returned by this session's create mutation alongside the\n // server page. The page is oldest-first, so a refetch after its 100-comment\n // limit is reached does not otherwise include a newly created comment.\n const items: EntityCommentFieldsFragment[] = useMemo(() => {\n const commentsById = new Map<string, EntityCommentFieldsFragment>();\n for (const comment of data?.getEntityComments.items ?? []) {\n commentsById.set(comment.id, comment);\n }\n for (const comment of createdComments) {\n if (comment.entityId === widgetId) commentsById.set(comment.id, comment);\n }\n return [...commentsById.values()].sort((a, b) => a.createdAt.localeCompare(b.createdAt));\n }, [createdComments, data, widgetId]);\n // Use the server-reported total for the count (items is capped at the page size).\n const total = data?.getEntityComments.total ?? items.length;\n\n // Reassemble a flat list into top-level threads + their direct replies.\n const threads = useMemo<WidgetCommentThread[]>(() => {\n const roots = items.filter((c) => !c.parentCommentId);\n const repliesByParent = new Map<string, EntityCommentFieldsFragment[]>();\n for (const c of items) {\n if (c.parentCommentId) {\n const list = repliesByParent.get(c.parentCommentId) ?? [];\n list.push(c);\n repliesByParent.set(c.parentCommentId, list);\n }\n }\n return roots.map((comment) => ({ comment, replies: repliesByParent.get(comment.id) ?? [] }));\n }, [items]);\n\n // All actions catch their own errors and resolve to a boolean (never reject),\n // so callers can `await` for success/failure without unhandled rejections and\n // can keep the user's draft when a post fails.\n const addComment = useCallback(\n async (text: string, parentCommentId?: string): Promise<boolean> => {\n const body = text.trim();\n if (!body || !widgetId) return false;\n try {\n const res = await createComment({\n variables: {\n input: {\n entityId: widgetId,\n entityType: WIDGET_COMMENT_ENTITY_TYPE,\n content: { text: body },\n contentType: 'TEXT',\n ...(parentCommentId ? { parentCommentId } : {}),\n },\n },\n });\n const comment = res.data?.createEntityComment;\n if (comment) {\n setCreatedComments((current) =>\n current.some((existing) => existing.id === comment.id) ? current : [...current, comment]\n );\n await refetch();\n return true;\n }\n return false;\n } catch {\n return false;\n }\n },\n [createComment, refetch, widgetId]\n );\n\n const resolveComment = useCallback(\n async (id: string, resolved: boolean): Promise<boolean> => {\n try {\n const res = await resolveCommentMutation({ variables: { input: { id, resolved } } });\n const comment = res.data?.resolveEntityComment;\n if (comment) {\n setCreatedComments((current) => current.map((existing) => (existing.id === comment.id ? comment : existing)));\n await refetch();\n return true;\n }\n return false;\n } catch {\n return false;\n }\n },\n [resolveCommentMutation, refetch]\n );\n\n const deleteComment = useCallback(\n async (id: string): Promise<boolean> => {\n try {\n const res = await deleteCommentMutation({ variables: { input: { id, soft: true } } });\n if (res.data?.deleteEntityComment) {\n setCreatedComments((current) => current.filter((comment) => comment.id !== id));\n await refetch();\n return true;\n }\n return false;\n } catch {\n return false;\n }\n },\n [deleteCommentMutation, refetch]\n );\n\n return {\n threads,\n count: total,\n loading,\n error: error as Error | undefined,\n mutating: createState.loading || resolveState.loading || deleteState.loading,\n addComment,\n resolveComment,\n deleteComment,\n refetch: useCallback(() => {\n void refetch();\n }, [refetch]),\n };\n}\n\nexport default useWidgetComments;\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-bigconsole",
3
- "version": "2026.922.1",
3
+ "version": "2026.924.1",
4
4
  "description": "BigConsole - AI-powered analytics and dashboard platform",
5
5
  "type": "module",
6
6
  "files": [