@burdenoff/microfe-bigconsole 2026.916.1 → 2026.917.2

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,19 +1,20 @@
1
1
  import { CreateSandboxDocument as e, ExtendSandboxTtlDocument as t, GetActiveSandboxesDocument as n, GetReusableAssistantSandboxDocument as r, GetSandboxDocument as i, ProxySandboxRequestDocument as a } from "../../generated/wspace-operations.js";
2
- import { Kind as o, print as s } from "graphql";
3
- import { graphqlFetch as c } from "@burdenoff/fe-libs/shared/graphql";
4
- import { ASSISTANT_API_AUTH_STRATEGY as l, getImageForMode as u, getImageTagForMode as d, resolveSandboxAssistantMode as f } from "@burdenoff/fe-libs/shared/assistant/images";
2
+ import { BIGCONSOLE_PRODUCT as o } from "./product.js";
3
+ import { Kind as s, print as c } from "graphql";
4
+ import { graphqlFetch as l } from "@burdenoff/fe-libs/shared/graphql";
5
+ import { ASSISTANT_API_AUTH_STRATEGY as u, getImageForMode as d, getImageTagForMode as f, resolveSandboxAssistantMode as p } from "@burdenoff/fe-libs/shared/assistant/images";
5
6
  //#region src/bigconsole/assistant/assistantApi.ts
6
- var p = "bigconsole";
7
- function m() {
7
+ var m = o;
8
+ function h() {
8
9
  return typeof window > "u" ? !1 : window.location.hostname.toLowerCase() === "app.bigconsole.com";
9
10
  }
10
- function h(e, t = m()) {
11
- return d(e, t);
11
+ function g(e, t = h()) {
12
+ return f(e, t);
12
13
  }
13
- function g(e, t = m()) {
14
- return u(e, t);
14
+ function _(e, t = h()) {
15
+ return d(e, t);
15
16
  }
16
- function _(e) {
17
+ function v(e) {
17
18
  switch (e) {
18
19
  case "general": return {
19
20
  cpuRequest: "100m",
@@ -31,26 +32,26 @@ function _(e) {
31
32
  };
32
33
  }
33
34
  }
34
- function v(e) {
35
- return e.definitions.find((e) => e.kind === o.OPERATION_DEFINITION)?.name?.value;
35
+ function y(e) {
36
+ return e.definitions.find((e) => e.kind === s.OPERATION_DEFINITION)?.name?.value;
36
37
  }
37
- function y() {
38
+ function b() {
38
39
  try {
39
40
  return `bc-assistant-${crypto.randomUUID()}`;
40
41
  } catch {
41
42
  return `bc-assistant-${Date.now()}-${Math.random().toString(36).slice(2)}`;
42
43
  }
43
44
  }
44
- function b(e, t) {
45
+ function x(e, t) {
45
46
  return {
46
47
  workspaceId: e,
47
48
  ...t?.organizationId ? { custom: { organizationId: t.organizationId } } : {}
48
49
  };
49
50
  }
50
- async function x(e) {
51
+ async function S(e) {
51
52
  let t;
52
53
  for (let n = 0; n < 3; n++) try {
53
- return await S(e);
54
+ return await C(e);
54
55
  } catch (e) {
55
56
  t = e;
56
57
  let r = e instanceof Error ? e.message.toLowerCase() : String(e).toLowerCase();
@@ -65,10 +66,10 @@ async function x(e) {
65
66
  }
66
67
  throw t;
67
68
  }
68
- async function S(e) {
69
- let t = v(e.document), n = await c({
69
+ async function C(e) {
70
+ let t = y(e.document), n = await l({
70
71
  gateway: "workspace",
71
- query: s(e.document),
72
+ query: c(e.document),
72
73
  variables: e.variables,
73
74
  operationName: t,
74
75
  authToken: e.authContext?.accessToken ?? void 0,
@@ -76,7 +77,7 @@ async function S(e) {
76
77
  workspaceId: e.workspaceId,
77
78
  organizationId: e.authContext?.organizationId ?? !0,
78
79
  suppressGlobalErrorEvent: !0,
79
- extraHeaders: { "x-request-id": y() }
80
+ extraHeaders: { "x-request-id": b() }
80
81
  });
81
82
  if (!n) throw Error("Sandbox GraphQL returned empty response");
82
83
  if (n.errors?.length) throw console.error("[BigConsole-Assistant] executeSandboxGraphQL errors", {
@@ -86,32 +87,32 @@ async function S(e) {
86
87
  if (!n.data) throw Error("Sandbox GraphQL returned no data");
87
88
  return n.data;
88
89
  }
89
- function C(e, t, n) {
90
- return (e.status === "RUNNING" || e.status === "PROVISIONING" || e.status === "PENDING") && e.template === "AI_BUILDER" && f(e) === t && !!n && e.createdBy === n && e.metadata?.authStrategy === l && e.metadata?.assistantProduct === "bigconsole" && e.metadata?.confirmWrites !== !0;
90
+ function w(e, t, n) {
91
+ return (e.status === "RUNNING" || e.status === "PROVISIONING" || e.status === "PENDING") && e.template === "AI_BUILDER" && p(e) === t && !!n && e.createdBy === n && e.metadata?.authStrategy === u && e.metadata?.assistantProduct === m && e.metadata?.confirmWrites !== !0;
91
92
  }
92
- async function w(e, t, n, i) {
93
+ async function T(e, t, n, i) {
93
94
  if (!i?.userId) return !1;
94
- let a = (await x({
95
+ let a = (await S({
95
96
  document: r,
96
97
  variables: {
97
98
  id: e,
98
- context: b(t, i)
99
+ context: x(t, i)
99
100
  },
100
101
  workspaceId: t,
101
102
  authContext: i
102
103
  })).getSandbox;
103
- return !!a && a.id === e && C(a, n, i.userId);
104
+ return !!a && a.id === e && w(a, n, i.userId);
104
105
  }
105
- async function T(e, t, r) {
106
- return ((await x({
106
+ async function E(e, t, r) {
107
+ return ((await S({
107
108
  document: n,
108
- variables: { context: b(e, r) },
109
+ variables: { context: x(e, r) },
109
110
  workspaceId: e,
110
111
  authContext: r
111
- })).getActiveSandboxes ?? []).find((e) => C(e, t, r?.userId))?.id ?? null;
112
+ })).getActiveSandboxes ?? []).find((e) => w(e, t, r?.userId))?.id ?? null;
112
113
  }
113
- async function E(t, n, r) {
114
- let i = _(t), a = [
114
+ async function D(t, n, r) {
115
+ let i = v(t), a = [
115
116
  {
116
117
  name: "AI_ASSISTANT_MODE",
117
118
  value: t
@@ -126,9 +127,9 @@ async function E(t, n, r) {
126
127
  },
127
128
  {
128
129
  name: "AI_ASSISTANT_PRODUCT",
129
- value: p
130
+ value: m
130
131
  }
131
- ], o = m(), s = await x({
132
+ ], o = h(), s = await S({
132
133
  document: e,
133
134
  variables: { input: {
134
135
  name: `ai-assistant-${t}-${Date.now()}`,
@@ -137,7 +138,7 @@ async function E(t, n, r) {
137
138
  mode: "DEPLOYMENT",
138
139
  containers: [{
139
140
  name: "ai-assistant",
140
- image: g(t, o),
141
+ image: _(t, o),
141
142
  ports: [{
142
143
  containerPort: 8080,
143
144
  name: "http"
@@ -153,14 +154,14 @@ async function E(t, n, r) {
153
154
  ttlSeconds: 600,
154
155
  metadata: {
155
156
  assistantMode: t,
156
- assistantProduct: p,
157
+ assistantProduct: m,
157
158
  assistantOwnerUserId: r?.userId ?? null,
158
159
  assistantOrganizationId: r?.organizationId ?? null,
159
- authStrategy: l,
160
- imageTag: h(t, o),
160
+ authStrategy: u,
161
+ imageTag: g(t, o),
161
162
  confirmWrites: !1
162
163
  },
163
- context: b(n, r)
164
+ context: x(n, r)
164
165
  } },
165
166
  workspaceId: n,
166
167
  authContext: r
@@ -168,14 +169,14 @@ async function E(t, n, r) {
168
169
  if (!s?.createSandbox?.id) throw Error("createSandbox returned empty response");
169
170
  return s.createSandbox.id;
170
171
  }
171
- async function D(e, t, n, r = 3e5) {
172
+ async function O(e, t, n, r = 3e5) {
172
173
  let a = Date.now();
173
174
  for (; Date.now() - a < r;) {
174
- let r = await x({
175
+ let r = await S({
175
176
  document: i,
176
177
  variables: {
177
178
  id: e,
178
- context: b(t, n)
179
+ context: x(t, n)
179
180
  },
180
181
  workspaceId: t,
181
182
  authContext: n
@@ -186,7 +187,7 @@ async function D(e, t, n, r = 3e5) {
186
187
  }
187
188
  throw Error("Sandbox startup timed out");
188
189
  }
189
- async function O(e, t, n, r, i, o) {
190
+ async function k(e, t, n, r, i, o) {
190
191
  console.log("[BigConsole-Assistant] proxyToSandbox REQUEST", {
191
192
  sandboxId: e,
192
193
  workspaceId: t,
@@ -194,14 +195,14 @@ async function O(e, t, n, r, i, o) {
194
195
  method: r,
195
196
  hasBody: !!i
196
197
  });
197
- let s = (await x({
198
+ let s = (await S({
198
199
  document: a,
199
200
  variables: { input: {
200
201
  sandboxId: e,
201
202
  path: n,
202
203
  method: r,
203
204
  body: i,
204
- context: b(t, o)
205
+ context: x(t, o)
205
206
  } },
206
207
  workspaceId: t,
207
208
  authContext: o
@@ -221,11 +222,11 @@ async function O(e, t, n, r, i, o) {
221
222
  }
222
223
  return s.body;
223
224
  }
224
- async function k(e, t, n, r, i = 9e4) {
225
+ async function A(e, t, n, r, i = 9e4) {
225
226
  let a = Date.now();
226
227
  for (; Date.now() - a < i;) {
227
228
  try {
228
- await O(e, t, "/sessions", "POST", { mode: n }, r);
229
+ await k(e, t, "/sessions", "POST", { mode: n }, r);
229
230
  return;
230
231
  } catch (e) {
231
232
  let t = e instanceof Error ? e.message : String(e);
@@ -235,33 +236,33 @@ async function k(e, t, n, r, i = 9e4) {
235
236
  }
236
237
  throw Error("Assistant service did not become ready before timeout");
237
238
  }
238
- async function A(e, t, n, r) {
239
- return { sessionId: (await O(e, t, "/sessions", "POST", { mode: n }, r)).session.id };
239
+ async function j(e, t, n, r) {
240
+ return { sessionId: (await k(e, t, "/sessions", "POST", { mode: n }, r)).session.id };
240
241
  }
241
- async function j(e, t, n, r, i, a, o) {
242
+ async function M(e, t, n, r, i, a, o) {
242
243
  let s = {
243
244
  mode: i,
244
245
  prompt: r,
245
246
  pageContext: a ?? void 0
246
247
  };
247
- await O(e, t, `/sessions/${n}/prompt-async`, "POST", s, o);
248
+ await k(e, t, `/sessions/${n}/prompt-async`, "POST", s, o);
248
249
  }
249
- async function M(e, t, n, r, i = 50) {
250
- return (await O(e, t, `/sessions/${n}/messages?limit=${i}`, "GET", void 0, r)).messages ?? [];
250
+ async function N(e, t, n, r, i = 50) {
251
+ return (await k(e, t, `/sessions/${n}/messages?limit=${i}`, "GET", void 0, r)).messages ?? [];
251
252
  }
252
- async function N(e, n, r = 600, i) {
253
- await x({
253
+ async function P(e, n, r = 600, i) {
254
+ await S({
254
255
  document: t,
255
256
  variables: {
256
257
  id: e,
257
258
  additionalSeconds: r,
258
- context: b(n, i)
259
+ context: x(n, i)
259
260
  },
260
261
  workspaceId: n,
261
262
  authContext: i
262
263
  });
263
264
  }
264
265
  //#endregion
265
- export { E as createAssistantSandbox, A as createAssistantSession, N as extendAssistantSandboxTTL, T as findExistingSandbox, M as getAssistantMessages, w as isCachedAssistantSandboxReusable, j as sendAssistantPromptAsync, k as waitForAssistantServiceReady, D as waitForSandboxReady };
266
+ export { D as createAssistantSandbox, j as createAssistantSession, P as extendAssistantSandboxTTL, E as findExistingSandbox, N as getAssistantMessages, T as isCachedAssistantSandboxReusable, M as sendAssistantPromptAsync, A as waitForAssistantServiceReady, O as waitForSandboxReady };
266
267
 
267
268
  //# sourceMappingURL=assistantApi.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"assistantApi.js","names":[],"sources":["../../../src/bigconsole/assistant/assistantApi.ts"],"sourcesContent":["/**\n * AI Assistant API service for BigConsole.\n *\n * All requests go through the wspace-public-gateway (GraphQL Federation):\n * Frontend → wspace-public-gateway → wspace-sandbox-svc → ai-assistant (sandbox)\n *\n * This is a faithful port of microfe-vibecontrols' `services/assistantApi.ts`,\n * adapted for BigConsole: the only product-specific difference is\n * `AI_ASSISTANT_PRODUCT=bigconsole` in the sandbox container env (drives the\n * agent's per-product docs fetch + system prompt).\n */\n\nimport { print, type DocumentNode, Kind, type OperationDefinitionNode } from 'graphql';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\n// Deep import: images.js has no dependencies, so this does not pull the shared\n// assistant barrel into the consumer's chunk. fe-libs owns the registry (ECR\n// since the AWS cutover), the image tags, the auth strategy and the mode\n// resolution; this file used to carry byte-for-byte copies of them.\nimport {\n ASSISTANT_API_AUTH_STRATEGY,\n getImageForMode as getSharedImageForMode,\n getImageTagForMode as getSharedImageTagForMode,\n resolveSandboxAssistantMode,\n} from '@burdenoff/fe-libs/shared/assistant/images';\nimport type { AssistantMode, AssistantRawMessage, AssistantRawSession, AssistantSandboxAuthContext } from './types';\nimport type { PageContext } from './pageContext';\nimport {\n CreateSandboxDocument,\n GetSandboxDocument,\n GetReusableAssistantSandboxDocument,\n GetActiveSandboxesDocument,\n ProxySandboxRequestDocument,\n ExtendSandboxTtlDocument,\n} from '../../generated/wspace-operations';\n\n// ── Product, runtime and image ─────────────────────────────────────\n\n/**\n * The product this transport stamps on every assistant sandbox it creates\n * (`metadata.assistantProduct`) — the same key the shared fe-libs assistant\n * stamps for HealthyBowl and VibeControls, and FluidGrids stamps for itself — and\n * the ONLY product whose sandboxes it will reuse (BOFF-7331).\n */\nexport const ASSISTANT_PRODUCT = 'bigconsole';\n\nexport function isProdRuntime(): boolean {\n if (typeof window === 'undefined') {\n return import.meta.env.VITE_DEPLOY_ENV === 'prod' || import.meta.env.VITE_APP_ENV === 'prod';\n }\n const host = window.location.hostname.toLowerCase();\n return host === 'app.bigconsole.com';\n}\n\n/**\n * Image tag recorded on the sandbox. It is metadata only: the sandbox\n * cf-proxy's AiAssistant pin decides which image actually runs (plan decision\n * D9), so the tag is NOT used to pick a sandbox for reuse — see\n * `isReusableAssistantSandbox`.\n */\nexport function getImageTagForMode(mode: AssistantMode, isProd: boolean = isProdRuntime()): string {\n return getSharedImageTagForMode(mode, isProd);\n}\n\n/**\n * `assistant` reuses the api-calls image (mode is selected at runtime via\n * AI_ASSISTANT_MODE). The registry is fe-libs' — the Azure registry this used to\n * hardcode was decommissioned on 2026-08-15 and is not in wspace-sandbox-svc's\n * default ALLOWED_IMAGE_REGISTRIES.\n */\nexport function getImageForMode(mode: AssistantMode, isProd: boolean = isProdRuntime()): string {\n return getSharedImageForMode(mode, isProd);\n}\n\nfunction getResourcesForMode(mode: AssistantMode): {\n cpuRequest: string;\n cpuLimit: string;\n memoryRequest: string;\n memoryLimit: string;\n} {\n switch (mode) {\n case 'general':\n return {\n cpuRequest: '100m',\n cpuLimit: '500m',\n memoryRequest: '256Mi',\n memoryLimit: '1Gi',\n };\n case 'vibe-plugins':\n case 'assistant':\n case 'api-calls':\n return {\n cpuRequest: '150m',\n cpuLimit: '750m',\n memoryRequest: '384Mi',\n memoryLimit: '1536Mi',\n };\n }\n}\n\n// ── Types ───────────────────────────────────────────────────────────\n\ninterface ProxyResponse {\n proxySandboxRequest: {\n status: number;\n body: Record<string, unknown>;\n };\n}\n\nfunction getOperationName(document: DocumentNode): string | undefined {\n const operation = document.definitions.find(\n (definition): definition is OperationDefinitionNode => definition.kind === Kind.OPERATION_DEFINITION\n );\n return operation?.name?.value;\n}\n\nfunction buildAssistantRequestId(): string {\n try {\n return `bc-assistant-${crypto.randomUUID()}`;\n } catch {\n return `bc-assistant-${Date.now()}-${Math.random().toString(36).slice(2)}`;\n }\n}\n\nfunction buildSandboxContextInput(\n workspaceId: string,\n authContext?: AssistantSandboxAuthContext\n): Record<string, unknown> {\n return {\n workspaceId,\n ...(authContext?.organizationId\n ? {\n custom: {\n organizationId: authContext.organizationId,\n },\n }\n : {}),\n };\n}\n\nasync function executeSandboxGraphQL<TData>(params: {\n document: DocumentNode;\n variables: Record<string, unknown>;\n workspaceId: string;\n authContext?: AssistantSandboxAuthContext;\n}): Promise<TData> {\n // Retry transient failures (network drops, CORS preflight rejections,\n // gateway 524s, etc.) with bounded exponential backoff. The browser\n // surfaces these as \"Failed to fetch\" via fetch() throwing TypeError\n // before the runtime can inspect the response — so the only signal we\n // have is the throw itself. Bounded retries mask the gateway's\n // occasional CORS preflight races without papering over hard failures\n // (unauth, validation) which the inner block below raises as typed\n // errors that bubble up to the assistant's existing `isRecoverable`\n // retry path.\n const maxRetries = 3;\n let lastError: unknown;\n for (let attempt = 0; attempt < maxRetries; attempt++) {\n try {\n return await executeSandboxGraphQLOnce<TData>(params);\n } catch (error) {\n lastError = error;\n const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();\n // Only retry on transient network-level failures. Let domain errors\n // (auth, validation, server 5xx with a body) bubble immediately so\n // the outer `isRecoverable` logic can decide whether to drop the\n // sandbox and try a clean runtime instead.\n const isTransient =\n message.includes('failed to fetch') ||\n message.includes('networkerror') ||\n message.includes('network request failed') ||\n message.includes('aborted') ||\n message.includes('load failed');\n if (!isTransient || attempt === maxRetries - 1) {\n throw error;\n }\n // 250ms, 500ms, 1000ms — bounded so we don't blow the 90s gateway\n // execution timeout on a stuck connection.\n const backoff = 250 * Math.pow(2, attempt);\n console.warn('[BigConsole-Assistant] executeSandboxGraphQL transient failure, retrying', {\n attempt: attempt + 1,\n maxRetries,\n backoffMs: backoff,\n error: error instanceof Error ? error.message : String(error),\n });\n await new Promise((resolve) => setTimeout(resolve, backoff));\n }\n }\n // Unreachable — the loop either returns or throws.\n throw lastError;\n}\n\nasync function executeSandboxGraphQLOnce<TData>(params: {\n document: DocumentNode;\n variables: Record<string, unknown>;\n workspaceId: string;\n authContext?: AssistantSandboxAuthContext;\n}): Promise<TData> {\n const operationName = getOperationName(params.document);\n const result = await graphqlFetch<TData>({\n gateway: 'workspace',\n query: print(params.document),\n variables: params.variables,\n operationName,\n authToken: params.authContext?.accessToken ?? undefined,\n // Critical: always opt into workspace-token auto-minting here instead of\n // passing through a cached token string from the shell. If we pass a stale\n // string, graphqlFetch validates it for the current workspace/project and\n // nulls it out, but it WILL NOT mint a replacement unless the caller used\n // `workspaceToken: true`. That exact behavior caused proxySandboxRequest\n // calls to fail with `Dual-token authentication failed` even though the\n // browser had a login session and the workspace context was known.\n workspaceToken: true,\n workspaceId: params.workspaceId,\n organizationId: params.authContext?.organizationId ?? true,\n suppressGlobalErrorEvent: true,\n // Give assistant calls a unique queue key so they never batch with the\n // dashboard's burst of background widget requests. This avoids mixing\n // long-lived assistant polls with unrelated queries in the shared\n // graphqlFetch batching queue.\n extraHeaders: {\n 'x-request-id': buildAssistantRequestId(),\n },\n });\n\n if (!result) {\n throw new Error('Sandbox GraphQL returned empty response');\n }\n\n if (result.errors?.length) {\n console.error('[BigConsole-Assistant] executeSandboxGraphQL errors', {\n operationName,\n errors: result.errors,\n });\n throw new Error(result.errors.map((error: { message: string }) => error.message).join(', '));\n }\n\n if (!result.data) {\n throw new Error('Sandbox GraphQL returned no data');\n }\n\n return result.data;\n}\n\n// ── Find existing sandbox for a mode ───────────────────────────────\n\nexport interface ActiveAssistantSandbox {\n id: string;\n name: string;\n status: string;\n metadata: Record<string, unknown> | null;\n template: string;\n createdBy: string;\n}\n\n/**\n * May this active sandbox be reused for `mode`?\n *\n * Every product's assistant runs as the same user in the same workspace, so\n * `getActiveSandboxes` returns HealthyBowl's, VibeControls' and FluidGrids'\n * assistant sandboxes too (BOFF-7331). Adopting one of those runs the wrong\n * product guide, and HB/VC `api-calls` sandboxes have CONFIRM_WRITES on, which\n * the legacy widget BigConsole mounts has no Approve UI for. So:\n * - the sandbox must be stamped `assistantProduct: 'bigconsole'`. Unstamped\n * sandboxes — every BigConsole sandbox created before this change — are NOT\n * reused: their product cannot be proven. They lapse on their 600s TTL.\n * - a sandbox recording `confirmWrites: true` is never reused, whoever\n * stamped it: it waits for approvals this widget cannot give.\n * - the image tag is NOT compared (D9). It never decided what runs.\n * - ownership FAILS CLOSED: with no `userId` nothing is reusable. fe-libs'\n * AuthTokenProvider starts with `userId` null, and a super_admin caller is\n * handed every sandbox in the workspace, so skipping the owner check when\n * the id is unknown would adopt another user's BigConsole sandbox. The\n * caller creates a fresh sandbox instead.\n */\nexport function isReusableAssistantSandbox(\n sandbox: ActiveAssistantSandbox,\n mode: AssistantMode,\n userId?: string | null\n): boolean {\n return (\n (sandbox.status === 'RUNNING' || sandbox.status === 'PROVISIONING' || sandbox.status === 'PENDING') &&\n sandbox.template === 'AI_BUILDER' &&\n resolveSandboxAssistantMode(sandbox) === mode &&\n !!userId &&\n sandbox.createdBy === userId &&\n sandbox.metadata?.authStrategy === ASSISTANT_API_AUTH_STRATEGY &&\n sandbox.metadata?.assistantProduct === ASSISTANT_PRODUCT &&\n sandbox.metadata?.confirmWrites !== true\n );\n}\n\n/**\n * May a sandbox id cached from an earlier page lifecycle (sessionStorage) be\n * adopted for `mode`?\n *\n * A cached id carries no proof of what it points at: a tab that adopted another\n * product's, an unstamped or another user's sandbox under an older build would\n * otherwise keep driving it. So the sandbox is fetched and held to exactly the\n * same predicate as a freshly listed one. A sandbox that no longer exists is not\n * reusable. Transport errors propagate; the caller treats them as \"not reusable\".\n */\nexport async function isCachedAssistantSandboxReusable(\n sandboxId: string,\n workspaceId: string,\n mode: AssistantMode,\n authContext?: AssistantSandboxAuthContext\n): Promise<boolean> {\n // Checked before the call so a caller with no identity yet costs no round-trip.\n if (!authContext?.userId) return false;\n const data = await executeSandboxGraphQL<{ getSandbox: ActiveAssistantSandbox | null }>({\n document: GetReusableAssistantSandboxDocument,\n variables: { id: sandboxId, context: buildSandboxContextInput(workspaceId, authContext) },\n workspaceId,\n authContext,\n });\n const sandbox = data.getSandbox;\n return !!sandbox && sandbox.id === sandboxId && isReusableAssistantSandbox(sandbox, mode, authContext.userId);\n}\n\nexport async function findExistingSandbox(\n workspaceId: string,\n mode: AssistantMode,\n authContext?: AssistantSandboxAuthContext\n): Promise<string | null> {\n const data = await executeSandboxGraphQL<{\n getActiveSandboxes: ActiveAssistantSandbox[];\n }>({\n document: GetActiveSandboxesDocument,\n variables: { context: buildSandboxContextInput(workspaceId, authContext) },\n workspaceId,\n authContext,\n });\n const sandboxes = data.getActiveSandboxes ?? [];\n\n return sandboxes.find((s) => isReusableAssistantSandbox(s, mode, authContext?.userId))?.id ?? null;\n}\n\n// ── Sandbox lifecycle ──────────────────────────────────────────────\n\nexport async function createAssistantSandbox(\n mode: AssistantMode,\n workspaceId: string,\n authContext?: AssistantSandboxAuthContext\n): Promise<string> {\n const resources = getResourcesForMode(mode);\n const env = [\n { name: 'AI_ASSISTANT_MODE', value: mode },\n { name: 'HOSTNAME', value: '0.0.0.0' },\n { name: 'WORKSPACE_ID', value: workspaceId },\n // Product the user is in — drives the agent's dynamic per-product docs fetch\n // (the docs repo is `<product>-docs`) and personalizes its system prompt.\n // This package is the BigConsole MFE, so the product is bigconsole.\n { name: 'AI_ASSISTANT_PRODUCT', value: ASSISTANT_PRODUCT },\n ];\n // Resolved once so the image and its recorded tag can never disagree.\n const isProd = isProdRuntime();\n\n const data = await executeSandboxGraphQL<{ createSandbox: { id: string } }>({\n document: CreateSandboxDocument,\n variables: {\n input: {\n name: `ai-assistant-${mode}-${Date.now()}`,\n description: `AI Assistant sandbox (${mode} mode)`,\n template: 'AI_BUILDER',\n mode: 'DEPLOYMENT',\n containers: [\n {\n name: 'ai-assistant',\n image: getImageForMode(mode, isProd),\n // The agent (start-assistant.sh) binds :8080; the sandbox svc proxies\n // to this declared containerPort, so it MUST be 8080.\n ports: [{ containerPort: 8080, name: 'http' }],\n env,\n },\n ],\n resources: {\n cpuRequest: resources.cpuRequest,\n cpuLimit: resources.cpuLimit,\n memoryRequest: resources.memoryRequest,\n memoryLimit: resources.memoryLimit,\n },\n ttlSeconds: 600,\n metadata: {\n assistantMode: mode,\n // Product stamp: the reuse filter only adopts sandboxes carrying it.\n assistantProduct: ASSISTANT_PRODUCT,\n assistantOwnerUserId: authContext?.userId ?? null,\n assistantOrganizationId: authContext?.organizationId ?? null,\n authStrategy: ASSISTANT_API_AUTH_STRATEGY,\n imageTag: getImageTagForMode(mode, isProd),\n // Recorded explicitly (as fe-libs does): the legacy widget has no\n // Approve UI, so these sandboxes never run with\n // AI_ASSISTANT_CONFIRM_WRITES.\n confirmWrites: false,\n },\n context: buildSandboxContextInput(workspaceId, authContext),\n },\n },\n workspaceId,\n authContext,\n });\n\n if (!data?.createSandbox?.id) {\n throw new Error('createSandbox returned empty response');\n }\n return data.createSandbox.id;\n}\n\nexport async function waitForSandboxReady(\n sandboxId: string,\n workspaceId: string,\n authContext?: AssistantSandboxAuthContext,\n maxWaitMs = 300_000\n): Promise<void> {\n const start = Date.now();\n while (Date.now() - start < maxWaitMs) {\n const data = await executeSandboxGraphQL<{\n getSandbox: { status?: string; errorMessage?: string } | null;\n }>({\n document: GetSandboxDocument,\n variables: { id: sandboxId, context: buildSandboxContextInput(workspaceId, authContext) },\n workspaceId,\n authContext,\n });\n const status = data?.getSandbox?.status;\n const errorMessage = data?.getSandbox?.errorMessage as string | undefined;\n if (status === 'RUNNING') return;\n if (status === 'FAILED' || status === 'STOPPED' || status === 'EXPIRED') {\n throw new Error(\n errorMessage ? `Sandbox ${status.toLowerCase()}: ${errorMessage}` : `Sandbox ${status.toLowerCase()}`\n );\n }\n await new Promise((r) => setTimeout(r, 2000));\n }\n throw new Error('Sandbox startup timed out');\n}\n\n// ── Proxy calls to ai-assistant (via gateway → sandbox-svc) ───────\n\nasync function proxyToSandbox(\n sandboxId: string,\n workspaceId: string,\n path: string,\n method: string,\n body?: unknown,\n authContext?: AssistantSandboxAuthContext\n): Promise<Record<string, unknown>> {\n console.log('[BigConsole-Assistant] proxyToSandbox REQUEST', {\n sandboxId,\n workspaceId,\n path,\n method,\n hasBody: !!body,\n });\n const result = await executeSandboxGraphQL<ProxyResponse>({\n document: ProxySandboxRequestDocument,\n variables: {\n input: {\n sandboxId,\n path,\n method,\n body,\n context: buildSandboxContextInput(workspaceId, authContext),\n },\n },\n workspaceId,\n authContext,\n });\n\n const proxyResult = result.proxySandboxRequest;\n console.log('[BigConsole-Assistant] proxyToSandbox RESPONSE', { sandboxId, path, status: proxyResult?.status });\n if (!proxyResult || proxyResult.status >= 400) {\n const errBody = proxyResult?.body as Record<string, unknown> | undefined;\n console.error('[BigConsole-Assistant] proxyToSandbox ERROR', {\n sandboxId,\n path,\n status: proxyResult?.status,\n errBody,\n });\n throw new Error((errBody?.error as string) ?? `Proxy error: ${proxyResult?.status ?? 'unknown'}`);\n }\n\n return proxyResult.body;\n}\n\nexport async function waitForAssistantServiceReady(\n sandboxId: string,\n workspaceId: string,\n mode: AssistantMode,\n authContext?: AssistantSandboxAuthContext,\n maxWaitMs = 90_000\n): Promise<void> {\n const startedAt = Date.now();\n\n while (Date.now() - startedAt < maxWaitMs) {\n try {\n // Use the documented assistant API contract itself as the readiness\n // signal. The assistant image definitively supports POST /sessions,\n // whereas /health is an implementation assumption and has proven to hang\n // behind the Cloudflare sandbox proxy.\n await proxyToSandbox(sandboxId, workspaceId, '/sessions', 'POST', { mode }, authContext);\n return;\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n if (!/proxy error: 502|proxy error: 503|proxy error: 404|service not available|timeout/i.test(message)) {\n throw error;\n }\n }\n\n await new Promise((resolve) => setTimeout(resolve, 2000));\n }\n\n throw new Error('Assistant service did not become ready before timeout');\n}\n\n// ── Session management ─────────────────────────────────────────────\n\nexport async function createAssistantSession(\n sandboxId: string,\n workspaceId: string,\n mode: AssistantMode,\n authContext?: AssistantSandboxAuthContext\n): Promise<{ sessionId: string }> {\n const body = await proxyToSandbox(sandboxId, workspaceId, '/sessions', 'POST', { mode }, authContext);\n const session = body.session as Record<string, unknown>;\n return { sessionId: session.id as string };\n}\n\n/**\n * All conversations held by this sandbox, newest first.\n *\n * The sandbox is the source of truth for the transcript, so the History list is\n * built from it rather than from anything we cache client-side — what the user\n * can reopen is exactly what the agent still has.\n */\nexport async function listAssistantSessions(\n sandboxId: string,\n workspaceId: string,\n authContext?: AssistantSandboxAuthContext\n): Promise<AssistantRawSession[]> {\n const body = await proxyToSandbox(sandboxId, workspaceId, '/sessions', 'GET', undefined, authContext);\n const sessions = body.sessions;\n return Array.isArray(sessions) ? (sessions as AssistantRawSession[]) : [];\n}\n\nexport async function deleteAssistantSession(\n sandboxId: string,\n workspaceId: string,\n sessionId: string,\n authContext?: AssistantSandboxAuthContext\n): Promise<void> {\n await proxyToSandbox(sandboxId, workspaceId, `/sessions/${sessionId}`, 'DELETE', undefined, authContext);\n}\n\n// ── Send prompt ────────────────────────────────────────────────────\n\nexport async function sendAssistantPromptAsync(\n sandboxId: string,\n workspaceId: string,\n sessionId: string,\n prompt: string,\n mode: AssistantMode,\n pageContext?: PageContext,\n authContext?: AssistantSandboxAuthContext\n): Promise<void> {\n const requestBody = {\n mode,\n prompt,\n pageContext: pageContext ?? undefined,\n };\n\n await proxyToSandbox(sandboxId, workspaceId, `/sessions/${sessionId}/prompt-async`, 'POST', requestBody, authContext);\n}\n\nexport async function getAssistantMessages(\n sandboxId: string,\n workspaceId: string,\n sessionId: string,\n authContext?: AssistantSandboxAuthContext,\n limit = 50\n): Promise<AssistantRawMessage[]> {\n const body = await proxyToSandbox(\n sandboxId,\n workspaceId,\n `/sessions/${sessionId}/messages?limit=${limit}`,\n 'GET',\n undefined,\n authContext\n );\n\n return (body.messages as AssistantRawMessage[] | undefined) ?? [];\n}\n\n// ── TTL extension (keep the sandbox warm across the turn) ──────────\n\nexport async function extendAssistantSandboxTTL(\n sandboxId: string,\n workspaceId: string,\n additionalSeconds = 600,\n authContext?: AssistantSandboxAuthContext\n): Promise<void> {\n await executeSandboxGraphQL<{ extendSandboxTTL: { id: string } }>({\n document: ExtendSandboxTtlDocument,\n variables: {\n id: sandboxId,\n additionalSeconds,\n context: buildSandboxContextInput(workspaceId, authContext),\n },\n workspaceId,\n authContext,\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"assistantApi.js","names":[],"sources":["../../../src/bigconsole/assistant/assistantApi.ts"],"sourcesContent":["/**\n * AI Assistant API service for BigConsole.\n *\n * All requests go through the wspace-public-gateway (GraphQL Federation):\n * Frontend → wspace-public-gateway → wspace-sandbox-svc → ai-assistant (sandbox)\n *\n * This is a faithful port of microfe-vibecontrols' `services/assistantApi.ts`,\n * adapted for BigConsole: the only product-specific difference is\n * `AI_ASSISTANT_PRODUCT=bigconsole` in the sandbox container env (drives the\n * agent's per-product docs fetch + system prompt).\n */\n\nimport { print, type DocumentNode, Kind, type OperationDefinitionNode } from 'graphql';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\n// Deep import: images.js has no dependencies, so this does not pull the shared\n// assistant barrel into the consumer's chunk. fe-libs owns the registry (ECR\n// since the AWS cutover), the image tags, the auth strategy and the mode\n// resolution; this file used to carry byte-for-byte copies of them.\nimport {\n ASSISTANT_API_AUTH_STRATEGY,\n getImageForMode as getSharedImageForMode,\n getImageTagForMode as getSharedImageTagForMode,\n resolveSandboxAssistantMode,\n} from '@burdenoff/fe-libs/shared/assistant/images';\nimport type { AssistantMode, AssistantRawMessage, AssistantRawSession, AssistantSandboxAuthContext } from './types';\nimport type { PageContext } from './pageContext';\nimport { BIGCONSOLE_PRODUCT } from './product';\nimport {\n CreateSandboxDocument,\n GetSandboxDocument,\n GetReusableAssistantSandboxDocument,\n GetActiveSandboxesDocument,\n ProxySandboxRequestDocument,\n ExtendSandboxTtlDocument,\n} from '../../generated/wspace-operations';\n\n// ── Product, runtime and image ─────────────────────────────────────\n\n/**\n * The product this transport stamps on every assistant sandbox it creates\n * (`metadata.assistantProduct`) — the same key the shared fe-libs assistant\n * stamps for HealthyBowl and VibeControls, and FluidGrids stamps for itself — and\n * the ONLY product whose sandboxes it will reuse (BOFF-7331).\n *\n * ★ The VALUE now comes from `./product`, which is the one place the id is\n * spelled. This constant keeps its name because the stamp, the env var and the\n * reuse predicate below all read it — but a second literal spelled the same way\n * is exactly how `AI_ASSISTANT_PRODUCT` and `metadata.assistantProduct` drift\n * apart, and `assistantSeamWiring.test.ts` now forbids one.\n */\nexport const ASSISTANT_PRODUCT = BIGCONSOLE_PRODUCT;\n\nexport function isProdRuntime(): boolean {\n if (typeof window === 'undefined') {\n return import.meta.env.VITE_DEPLOY_ENV === 'prod' || import.meta.env.VITE_APP_ENV === 'prod';\n }\n const host = window.location.hostname.toLowerCase();\n return host === 'app.bigconsole.com';\n}\n\n/**\n * Image tag recorded on the sandbox. It is metadata only: the sandbox\n * cf-proxy's AiAssistant pin decides which image actually runs (plan decision\n * D9), so the tag is NOT used to pick a sandbox for reuse — see\n * `isReusableAssistantSandbox`.\n */\nexport function getImageTagForMode(mode: AssistantMode, isProd: boolean = isProdRuntime()): string {\n return getSharedImageTagForMode(mode, isProd);\n}\n\n/**\n * `assistant` reuses the api-calls image (mode is selected at runtime via\n * AI_ASSISTANT_MODE). The registry is fe-libs' — the Azure registry this used to\n * hardcode was decommissioned on 2026-08-15 and is not in wspace-sandbox-svc's\n * default ALLOWED_IMAGE_REGISTRIES.\n */\nexport function getImageForMode(mode: AssistantMode, isProd: boolean = isProdRuntime()): string {\n return getSharedImageForMode(mode, isProd);\n}\n\nfunction getResourcesForMode(mode: AssistantMode): {\n cpuRequest: string;\n cpuLimit: string;\n memoryRequest: string;\n memoryLimit: string;\n} {\n switch (mode) {\n case 'general':\n return {\n cpuRequest: '100m',\n cpuLimit: '500m',\n memoryRequest: '256Mi',\n memoryLimit: '1Gi',\n };\n case 'vibe-plugins':\n case 'assistant':\n case 'api-calls':\n return {\n cpuRequest: '150m',\n cpuLimit: '750m',\n memoryRequest: '384Mi',\n memoryLimit: '1536Mi',\n };\n }\n}\n\n// ── Types ───────────────────────────────────────────────────────────\n\ninterface ProxyResponse {\n proxySandboxRequest: {\n status: number;\n body: Record<string, unknown>;\n };\n}\n\nfunction getOperationName(document: DocumentNode): string | undefined {\n const operation = document.definitions.find(\n (definition): definition is OperationDefinitionNode => definition.kind === Kind.OPERATION_DEFINITION\n );\n return operation?.name?.value;\n}\n\nfunction buildAssistantRequestId(): string {\n try {\n return `bc-assistant-${crypto.randomUUID()}`;\n } catch {\n return `bc-assistant-${Date.now()}-${Math.random().toString(36).slice(2)}`;\n }\n}\n\nfunction buildSandboxContextInput(\n workspaceId: string,\n authContext?: AssistantSandboxAuthContext\n): Record<string, unknown> {\n return {\n workspaceId,\n ...(authContext?.organizationId\n ? {\n custom: {\n organizationId: authContext.organizationId,\n },\n }\n : {}),\n };\n}\n\nasync function executeSandboxGraphQL<TData>(params: {\n document: DocumentNode;\n variables: Record<string, unknown>;\n workspaceId: string;\n authContext?: AssistantSandboxAuthContext;\n}): Promise<TData> {\n // Retry transient failures (network drops, CORS preflight rejections,\n // gateway 524s, etc.) with bounded exponential backoff. The browser\n // surfaces these as \"Failed to fetch\" via fetch() throwing TypeError\n // before the runtime can inspect the response — so the only signal we\n // have is the throw itself. Bounded retries mask the gateway's\n // occasional CORS preflight races without papering over hard failures\n // (unauth, validation) which the inner block below raises as typed\n // errors that bubble up to the assistant's existing `isRecoverable`\n // retry path.\n const maxRetries = 3;\n let lastError: unknown;\n for (let attempt = 0; attempt < maxRetries; attempt++) {\n try {\n return await executeSandboxGraphQLOnce<TData>(params);\n } catch (error) {\n lastError = error;\n const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();\n // Only retry on transient network-level failures. Let domain errors\n // (auth, validation, server 5xx with a body) bubble immediately so\n // the outer `isRecoverable` logic can decide whether to drop the\n // sandbox and try a clean runtime instead.\n const isTransient =\n message.includes('failed to fetch') ||\n message.includes('networkerror') ||\n message.includes('network request failed') ||\n message.includes('aborted') ||\n message.includes('load failed');\n if (!isTransient || attempt === maxRetries - 1) {\n throw error;\n }\n // 250ms, 500ms, 1000ms — bounded so we don't blow the 90s gateway\n // execution timeout on a stuck connection.\n const backoff = 250 * Math.pow(2, attempt);\n console.warn('[BigConsole-Assistant] executeSandboxGraphQL transient failure, retrying', {\n attempt: attempt + 1,\n maxRetries,\n backoffMs: backoff,\n error: error instanceof Error ? error.message : String(error),\n });\n await new Promise((resolve) => setTimeout(resolve, backoff));\n }\n }\n // Unreachable — the loop either returns or throws.\n throw lastError;\n}\n\nasync function executeSandboxGraphQLOnce<TData>(params: {\n document: DocumentNode;\n variables: Record<string, unknown>;\n workspaceId: string;\n authContext?: AssistantSandboxAuthContext;\n}): Promise<TData> {\n const operationName = getOperationName(params.document);\n const result = await graphqlFetch<TData>({\n gateway: 'workspace',\n query: print(params.document),\n variables: params.variables,\n operationName,\n authToken: params.authContext?.accessToken ?? undefined,\n // Critical: always opt into workspace-token auto-minting here instead of\n // passing through a cached token string from the shell. If we pass a stale\n // string, graphqlFetch validates it for the current workspace/project and\n // nulls it out, but it WILL NOT mint a replacement unless the caller used\n // `workspaceToken: true`. That exact behavior caused proxySandboxRequest\n // calls to fail with `Dual-token authentication failed` even though the\n // browser had a login session and the workspace context was known.\n workspaceToken: true,\n workspaceId: params.workspaceId,\n organizationId: params.authContext?.organizationId ?? true,\n suppressGlobalErrorEvent: true,\n // Give assistant calls a unique queue key so they never batch with the\n // dashboard's burst of background widget requests. This avoids mixing\n // long-lived assistant polls with unrelated queries in the shared\n // graphqlFetch batching queue.\n extraHeaders: {\n 'x-request-id': buildAssistantRequestId(),\n },\n });\n\n if (!result) {\n throw new Error('Sandbox GraphQL returned empty response');\n }\n\n if (result.errors?.length) {\n console.error('[BigConsole-Assistant] executeSandboxGraphQL errors', {\n operationName,\n errors: result.errors,\n });\n throw new Error(result.errors.map((error: { message: string }) => error.message).join(', '));\n }\n\n if (!result.data) {\n throw new Error('Sandbox GraphQL returned no data');\n }\n\n return result.data;\n}\n\n// ── Find existing sandbox for a mode ───────────────────────────────\n\nexport interface ActiveAssistantSandbox {\n id: string;\n name: string;\n status: string;\n metadata: Record<string, unknown> | null;\n template: string;\n createdBy: string;\n}\n\n/**\n * May this active sandbox be reused for `mode`?\n *\n * Every product's assistant runs as the same user in the same workspace, so\n * `getActiveSandboxes` returns HealthyBowl's, VibeControls' and FluidGrids'\n * assistant sandboxes too (BOFF-7331). Adopting one of those runs the wrong\n * product guide, and HB/VC `api-calls` sandboxes have CONFIRM_WRITES on, which\n * the legacy widget BigConsole mounts has no Approve UI for. So:\n * - the sandbox must be stamped `assistantProduct: 'bigconsole'`. Unstamped\n * sandboxes — every BigConsole sandbox created before this change — are NOT\n * reused: their product cannot be proven. They lapse on their 600s TTL.\n * - a sandbox recording `confirmWrites: true` is never reused, whoever\n * stamped it: it waits for approvals this widget cannot give.\n * - the image tag is NOT compared (D9). It never decided what runs.\n * - ownership FAILS CLOSED: with no `userId` nothing is reusable. fe-libs'\n * AuthTokenProvider starts with `userId` null, and a super_admin caller is\n * handed every sandbox in the workspace, so skipping the owner check when\n * the id is unknown would adopt another user's BigConsole sandbox. The\n * caller creates a fresh sandbox instead.\n */\nexport function isReusableAssistantSandbox(\n sandbox: ActiveAssistantSandbox,\n mode: AssistantMode,\n userId?: string | null\n): boolean {\n return (\n (sandbox.status === 'RUNNING' || sandbox.status === 'PROVISIONING' || sandbox.status === 'PENDING') &&\n sandbox.template === 'AI_BUILDER' &&\n resolveSandboxAssistantMode(sandbox) === mode &&\n !!userId &&\n sandbox.createdBy === userId &&\n sandbox.metadata?.authStrategy === ASSISTANT_API_AUTH_STRATEGY &&\n sandbox.metadata?.assistantProduct === ASSISTANT_PRODUCT &&\n sandbox.metadata?.confirmWrites !== true\n );\n}\n\n/**\n * May a sandbox id cached from an earlier page lifecycle (sessionStorage) be\n * adopted for `mode`?\n *\n * A cached id carries no proof of what it points at: a tab that adopted another\n * product's, an unstamped or another user's sandbox under an older build would\n * otherwise keep driving it. So the sandbox is fetched and held to exactly the\n * same predicate as a freshly listed one. A sandbox that no longer exists is not\n * reusable. Transport errors propagate; the caller treats them as \"not reusable\".\n */\nexport async function isCachedAssistantSandboxReusable(\n sandboxId: string,\n workspaceId: string,\n mode: AssistantMode,\n authContext?: AssistantSandboxAuthContext\n): Promise<boolean> {\n // Checked before the call so a caller with no identity yet costs no round-trip.\n if (!authContext?.userId) return false;\n const data = await executeSandboxGraphQL<{ getSandbox: ActiveAssistantSandbox | null }>({\n document: GetReusableAssistantSandboxDocument,\n variables: { id: sandboxId, context: buildSandboxContextInput(workspaceId, authContext) },\n workspaceId,\n authContext,\n });\n const sandbox = data.getSandbox;\n return !!sandbox && sandbox.id === sandboxId && isReusableAssistantSandbox(sandbox, mode, authContext.userId);\n}\n\nexport async function findExistingSandbox(\n workspaceId: string,\n mode: AssistantMode,\n authContext?: AssistantSandboxAuthContext\n): Promise<string | null> {\n const data = await executeSandboxGraphQL<{\n getActiveSandboxes: ActiveAssistantSandbox[];\n }>({\n document: GetActiveSandboxesDocument,\n variables: { context: buildSandboxContextInput(workspaceId, authContext) },\n workspaceId,\n authContext,\n });\n const sandboxes = data.getActiveSandboxes ?? [];\n\n return sandboxes.find((s) => isReusableAssistantSandbox(s, mode, authContext?.userId))?.id ?? null;\n}\n\n// ── Sandbox lifecycle ──────────────────────────────────────────────\n\nexport async function createAssistantSandbox(\n mode: AssistantMode,\n workspaceId: string,\n authContext?: AssistantSandboxAuthContext\n): Promise<string> {\n const resources = getResourcesForMode(mode);\n const env = [\n { name: 'AI_ASSISTANT_MODE', value: mode },\n { name: 'HOSTNAME', value: '0.0.0.0' },\n { name: 'WORKSPACE_ID', value: workspaceId },\n // Product the user is in — drives the agent's dynamic per-product docs fetch\n // (the docs repo is `<product>-docs`) and personalizes its system prompt.\n // This package is the BigConsole MFE, so the product is bigconsole.\n { name: 'AI_ASSISTANT_PRODUCT', value: ASSISTANT_PRODUCT },\n ];\n // Resolved once so the image and its recorded tag can never disagree.\n const isProd = isProdRuntime();\n\n const data = await executeSandboxGraphQL<{ createSandbox: { id: string } }>({\n document: CreateSandboxDocument,\n variables: {\n input: {\n name: `ai-assistant-${mode}-${Date.now()}`,\n description: `AI Assistant sandbox (${mode} mode)`,\n template: 'AI_BUILDER',\n mode: 'DEPLOYMENT',\n containers: [\n {\n name: 'ai-assistant',\n image: getImageForMode(mode, isProd),\n // The agent (start-assistant.sh) binds :8080; the sandbox svc proxies\n // to this declared containerPort, so it MUST be 8080.\n ports: [{ containerPort: 8080, name: 'http' }],\n env,\n },\n ],\n resources: {\n cpuRequest: resources.cpuRequest,\n cpuLimit: resources.cpuLimit,\n memoryRequest: resources.memoryRequest,\n memoryLimit: resources.memoryLimit,\n },\n ttlSeconds: 600,\n metadata: {\n assistantMode: mode,\n // Product stamp: the reuse filter only adopts sandboxes carrying it.\n assistantProduct: ASSISTANT_PRODUCT,\n assistantOwnerUserId: authContext?.userId ?? null,\n assistantOrganizationId: authContext?.organizationId ?? null,\n authStrategy: ASSISTANT_API_AUTH_STRATEGY,\n imageTag: getImageTagForMode(mode, isProd),\n // Recorded explicitly (as fe-libs does): the legacy widget has no\n // Approve UI, so these sandboxes never run with\n // AI_ASSISTANT_CONFIRM_WRITES.\n confirmWrites: false,\n },\n context: buildSandboxContextInput(workspaceId, authContext),\n },\n },\n workspaceId,\n authContext,\n });\n\n if (!data?.createSandbox?.id) {\n throw new Error('createSandbox returned empty response');\n }\n return data.createSandbox.id;\n}\n\nexport async function waitForSandboxReady(\n sandboxId: string,\n workspaceId: string,\n authContext?: AssistantSandboxAuthContext,\n maxWaitMs = 300_000\n): Promise<void> {\n const start = Date.now();\n while (Date.now() - start < maxWaitMs) {\n const data = await executeSandboxGraphQL<{\n getSandbox: { status?: string; errorMessage?: string } | null;\n }>({\n document: GetSandboxDocument,\n variables: { id: sandboxId, context: buildSandboxContextInput(workspaceId, authContext) },\n workspaceId,\n authContext,\n });\n const status = data?.getSandbox?.status;\n const errorMessage = data?.getSandbox?.errorMessage as string | undefined;\n if (status === 'RUNNING') return;\n if (status === 'FAILED' || status === 'STOPPED' || status === 'EXPIRED') {\n throw new Error(\n errorMessage ? `Sandbox ${status.toLowerCase()}: ${errorMessage}` : `Sandbox ${status.toLowerCase()}`\n );\n }\n await new Promise((r) => setTimeout(r, 2000));\n }\n throw new Error('Sandbox startup timed out');\n}\n\n// ── Proxy calls to ai-assistant (via gateway → sandbox-svc) ───────\n\nasync function proxyToSandbox(\n sandboxId: string,\n workspaceId: string,\n path: string,\n method: string,\n body?: unknown,\n authContext?: AssistantSandboxAuthContext\n): Promise<Record<string, unknown>> {\n console.log('[BigConsole-Assistant] proxyToSandbox REQUEST', {\n sandboxId,\n workspaceId,\n path,\n method,\n hasBody: !!body,\n });\n const result = await executeSandboxGraphQL<ProxyResponse>({\n document: ProxySandboxRequestDocument,\n variables: {\n input: {\n sandboxId,\n path,\n method,\n body,\n context: buildSandboxContextInput(workspaceId, authContext),\n },\n },\n workspaceId,\n authContext,\n });\n\n const proxyResult = result.proxySandboxRequest;\n console.log('[BigConsole-Assistant] proxyToSandbox RESPONSE', { sandboxId, path, status: proxyResult?.status });\n if (!proxyResult || proxyResult.status >= 400) {\n const errBody = proxyResult?.body as Record<string, unknown> | undefined;\n console.error('[BigConsole-Assistant] proxyToSandbox ERROR', {\n sandboxId,\n path,\n status: proxyResult?.status,\n errBody,\n });\n throw new Error((errBody?.error as string) ?? `Proxy error: ${proxyResult?.status ?? 'unknown'}`);\n }\n\n return proxyResult.body;\n}\n\nexport async function waitForAssistantServiceReady(\n sandboxId: string,\n workspaceId: string,\n mode: AssistantMode,\n authContext?: AssistantSandboxAuthContext,\n maxWaitMs = 90_000\n): Promise<void> {\n const startedAt = Date.now();\n\n while (Date.now() - startedAt < maxWaitMs) {\n try {\n // Use the documented assistant API contract itself as the readiness\n // signal. The assistant image definitively supports POST /sessions,\n // whereas /health is an implementation assumption and has proven to hang\n // behind the Cloudflare sandbox proxy.\n await proxyToSandbox(sandboxId, workspaceId, '/sessions', 'POST', { mode }, authContext);\n return;\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n if (!/proxy error: 502|proxy error: 503|proxy error: 404|service not available|timeout/i.test(message)) {\n throw error;\n }\n }\n\n await new Promise((resolve) => setTimeout(resolve, 2000));\n }\n\n throw new Error('Assistant service did not become ready before timeout');\n}\n\n// ── Session management ─────────────────────────────────────────────\n\nexport async function createAssistantSession(\n sandboxId: string,\n workspaceId: string,\n mode: AssistantMode,\n authContext?: AssistantSandboxAuthContext\n): Promise<{ sessionId: string }> {\n const body = await proxyToSandbox(sandboxId, workspaceId, '/sessions', 'POST', { mode }, authContext);\n const session = body.session as Record<string, unknown>;\n return { sessionId: session.id as string };\n}\n\n/**\n * All conversations held by this sandbox, newest first.\n *\n * The sandbox is the source of truth for the transcript, so the History list is\n * built from it rather than from anything we cache client-side — what the user\n * can reopen is exactly what the agent still has.\n */\nexport async function listAssistantSessions(\n sandboxId: string,\n workspaceId: string,\n authContext?: AssistantSandboxAuthContext\n): Promise<AssistantRawSession[]> {\n const body = await proxyToSandbox(sandboxId, workspaceId, '/sessions', 'GET', undefined, authContext);\n const sessions = body.sessions;\n return Array.isArray(sessions) ? (sessions as AssistantRawSession[]) : [];\n}\n\nexport async function deleteAssistantSession(\n sandboxId: string,\n workspaceId: string,\n sessionId: string,\n authContext?: AssistantSandboxAuthContext\n): Promise<void> {\n await proxyToSandbox(sandboxId, workspaceId, `/sessions/${sessionId}`, 'DELETE', undefined, authContext);\n}\n\n// ── Send prompt ────────────────────────────────────────────────────\n\nexport async function sendAssistantPromptAsync(\n sandboxId: string,\n workspaceId: string,\n sessionId: string,\n prompt: string,\n mode: AssistantMode,\n pageContext?: PageContext,\n authContext?: AssistantSandboxAuthContext\n): Promise<void> {\n const requestBody = {\n mode,\n prompt,\n pageContext: pageContext ?? undefined,\n };\n\n await proxyToSandbox(sandboxId, workspaceId, `/sessions/${sessionId}/prompt-async`, 'POST', requestBody, authContext);\n}\n\nexport async function getAssistantMessages(\n sandboxId: string,\n workspaceId: string,\n sessionId: string,\n authContext?: AssistantSandboxAuthContext,\n limit = 50\n): Promise<AssistantRawMessage[]> {\n const body = await proxyToSandbox(\n sandboxId,\n workspaceId,\n `/sessions/${sessionId}/messages?limit=${limit}`,\n 'GET',\n undefined,\n authContext\n );\n\n return (body.messages as AssistantRawMessage[] | undefined) ?? [];\n}\n\n// ── TTL extension (keep the sandbox warm across the turn) ──────────\n\nexport async function extendAssistantSandboxTTL(\n sandboxId: string,\n workspaceId: string,\n additionalSeconds = 600,\n authContext?: AssistantSandboxAuthContext\n): Promise<void> {\n await executeSandboxGraphQL<{ extendSandboxTTL: { id: string } }>({\n document: ExtendSandboxTtlDocument,\n variables: {\n id: sandboxId,\n additionalSeconds,\n context: buildSandboxContextInput(workspaceId, authContext),\n },\n workspaceId,\n authContext,\n });\n}\n"],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import { BIGCONSOLE_PRODUCT as e } from "./product.js";
2
+ import { describeCurrentPage as t, gatherPageContext as n } from "./pageContext.js";
3
+ import { buildPromptForMode as r } from "./assistantPrompt.js";
4
+ import { BIGCONSOLE_ASSISTANT_MODES as i } from "./modes.js";
5
+ //#region src/bigconsole/assistant/assistantProduct.ts
6
+ var a = {
7
+ attachments: "/BigConsole/AI Assistant/Attachments",
8
+ artifacts: "/BigConsole/AI Assistant/Artifacts"
9
+ }, o = {
10
+ product: e,
11
+ modes: i,
12
+ describeCurrentPage: t,
13
+ gatherPageContext: n,
14
+ buildPrompt: r,
15
+ storageFolders: a
16
+ };
17
+ //#endregion
18
+ export { o as BIGCONSOLE_ASSISTANT_PRODUCT, a as STORAGE_FOLDERS };
19
+
20
+ //# sourceMappingURL=assistantProduct.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assistantProduct.js","names":[],"sources":["../../../src/bigconsole/assistant/assistantProduct.ts"],"sourcesContent":["/**\n * BigConsole's answers to the shared assistant's product seam, as one value.\n *\n * ★★ Every capability field on fe-libs' `AssistantProductProvider` is OPTIONAL,\n * and a missing one does not throw — it degrades SILENTLY. No `gatherPageContext`\n * sends every turn with no page context; no `buildPrompt` drops both the\n * confirm-before-mutate guardrail and the language directive; no\n * `storageFolders` starts a second folder beside the one holding every saved\n * attachment.\n *\n * So the value is typed `Required<…>` over the provider's own props: forgetting\n * a field, or fe-libs ADDING one, is a type error in this file rather than a\n * quiet regression in production.\n */\nimport type { AssistantProductProviderProps } from '@burdenoff/fe-libs/shared/assistant/product';\n\nimport { buildPromptForMode } from './assistantPrompt';\nimport { BIGCONSOLE_ASSISTANT_MODES } from './modes';\nimport { describeCurrentPage, gatherPageContext } from './pageContext';\nimport { BIGCONSOLE_PRODUCT } from './product';\n\n/**\n * Where this product's assistant files land in the workspace file library.\n *\n * ★★ Capitalised as a DISPLAY name while the product id is `bigconsole`, which\n * is exactly why fe-libs cannot derive it: its fallback is\n * `/${product}/AI Assistant/…` → `/bigconsole/AI Assistant/…`.\n *\n * These strings become load-bearing the moment anything is saved. A wrong value\n * does not fail loudly — it quietly starts a SECOND folder beside the first and\n * orphans everything in the original. Today nothing can be orphaned: BigConsole's\n * legacy transport extracts uploaded files into the prompt and saves nothing to\n * the library, so no `/BigConsole/AI Assistant/*` path exists yet. That makes\n * this a free choice now and an expensive one the moment BC4 ships attachments.\n */\nexport const STORAGE_FOLDERS = {\n attachments: '/BigConsole/AI Assistant/Attachments',\n artifacts: '/BigConsole/AI Assistant/Artifacts',\n} as const;\n\n/**\n * Every seam field, required — the provider's props minus its children, and\n * minus the one answer BigConsole deliberately does not give.\n *\n * ★★ `reportTokenUsage` is omitted ON EVIDENCE, not by oversight, and this\n * `Omit` member is the only place that decision is expressed in the type system.\n * `wspace-bigconsole-svc` declares NO token-usage mutation at all, and nothing in\n * this MFE reports one today. The obvious \"fix\" — wiring fe-libs' shared\n * reporter — would bill VibeControls for BigConsole turns: `reportAiTokenUsage`\n * is declared only in `wspace-vibecontrols-svc`, against the\n * `vibecontrols.ai.credits` quota. Metering BigConsole is a backend change.\n *\n * Note that BigConsole's `bigconsole.*` quotas are a different mechanism —\n * they cap entity counts, not AI spend — so their presence is not evidence that\n * turns are metered.\n */\nexport type BigConsoleAssistantProduct = Required<Omit<AssistantProductProviderProps, 'children' | 'reportTokenUsage'>>;\n\n/**\n * ★ Each field points at a product-owned implementation BY IDENTITY — no inline\n * arrow wrappers. The seam guard asserts `toBe`, because a wrapper is how a\n * function silently stops being the one the tests cover.\n *\n * ★ `writeAccess` is deliberately absent: it is a PROP on the shared panel, not\n * a seam field, so it belongs to BC4's panel mount rather than here. Same for\n * the preview controller, which the panel takes as `panelProps.preview`.\n */\nexport const BIGCONSOLE_ASSISTANT_PRODUCT: BigConsoleAssistantProduct = {\n product: BIGCONSOLE_PRODUCT,\n modes: BIGCONSOLE_ASSISTANT_MODES,\n describeCurrentPage,\n gatherPageContext,\n buildPrompt: buildPromptForMode,\n storageFolders: STORAGE_FOLDERS,\n};\n"],"mappings":";;;;;AAmCA,IAAa,IAAkB;CAC7B,aAAa;CACb,WAAW;CACZ,EA6BY,IAA2D;CACtE,SAAS;CACT,OAAO;CACP;CACA;CACA,aAAa;CACb,gBAAgB;CACjB"}
@@ -0,0 +1,34 @@
1
+ //#region src/bigconsole/assistant/assistantPrompt.ts
2
+ var e = "--- User request ---", t = [
3
+ "You are in api-calls mode for BigConsole and can run GraphQL queries and mutations on the user behalf.",
4
+ "Before performing ANY write (create, update, delete — any mutation):",
5
+ "1. State exactly what you are about to change (entity, fields, and values).",
6
+ "2. Ask the user to confirm, and WAIT for an explicit \"yes\"/\"confirm\" before executing it.",
7
+ "Read-only queries may run without confirmation. Confirm each write separately; never batch mutations behind a single confirmation.",
8
+ "",
9
+ e
10
+ ].join("\n");
11
+ function n(e, t) {
12
+ return [
13
+ t ? `Reply in ${e}. The user has explicitly chosen this language, so use it even if they write to you in another one.` : `Reply in ${e}, matching the language the user wrote in.`,
14
+ `Translate any English documentation you ground your answer in into ${e} rather than switching to English to quote it.`,
15
+ "Keep identifiers, code, CLI flags, API field names and product nouns in their original form — translating them would make them wrong."
16
+ ].join("\n");
17
+ }
18
+ var r = [
19
+ "This turn was DICTATED, so treat unusual words as probable speech-recognition errors rather than as what the user meant.",
20
+ "Repair obvious mis-hearings of technical terms, product nouns, column names and command names using the surrounding context.",
21
+ "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."
22
+ ].join("\n"), i = ["Your reply will be read aloud. Write it to be heard, not scanned:", "short sentences, no markdown syntax, no tables, and no code blocks unless the user asked for code."].join("\n");
23
+ function a(a, o, s, c = !1, l = !1) {
24
+ let u = [
25
+ s ? n(s, c) : "",
26
+ l ? r : "",
27
+ l ? i : ""
28
+ ].filter(Boolean).join("\n\n"), d = u ? `${u}\n\n` : "";
29
+ return a === "api-calls" ? `${d}${t}\n${o}` : d ? `${d}${e}\n${o}` : o;
30
+ }
31
+ //#endregion
32
+ export { t as API_CALLS_CONFIRM_PREAMBLE, e as USER_REQUEST_DELIMITER, a as buildPromptForMode };
33
+
34
+ //# sourceMappingURL=assistantPrompt.js.map
@@ -0,0 +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,45 +1,46 @@
1
- import { graphqlFetch as e } from "@burdenoff/fe-libs/shared/graphql";
1
+ import { BIGCONSOLE_PRODUCT as e } from "./product.js";
2
+ import { graphqlFetch as t } from "@burdenoff/fe-libs/shared/graphql";
2
3
  //#region src/bigconsole/assistant/conversationHistoryApi.ts
3
- var t = "bigconsole", n = "\n id\n title\n agentSessionId\n updatedAt\n", r = `
4
+ var n = e, r = "\n id\n title\n agentSessionId\n updatedAt\n", i = `
4
5
  query AssistantConversations($productId: String!, $first: Int) {
5
- assistantConversations(productId: $productId, first: $first) { ${n} }
6
+ assistantConversations(productId: $productId, first: $first) { ${r} }
6
7
  }
7
- `, i = "\n query AssistantConversationMessages($conversationId: ID!, $first: Int) {\n assistantConversationMessages(conversationId: $conversationId, first: $first) {\n id\n role\n content\n createdAt\n }\n }\n", a = `
8
+ `, a = "\n query AssistantConversationMessages($conversationId: ID!, $first: Int) {\n assistantConversationMessages(conversationId: $conversationId, first: $first) {\n id\n role\n content\n createdAt\n }\n }\n", o = `
8
9
  mutation SaveAssistantTurn($input: SaveAssistantTurnInput!) {
9
- saveAssistantTurn(input: $input) { ${n} }
10
+ saveAssistantTurn(input: $input) { ${r} }
10
11
  }
11
- `, o = "\n mutation DeleteAssistantConversation($conversationId: ID!) {\n deleteAssistantConversation(conversationId: $conversationId)\n }\n";
12
- async function s(t) {
13
- let n = await e({
12
+ `, s = "\n mutation DeleteAssistantConversation($conversationId: ID!) {\n deleteAssistantConversation(conversationId: $conversationId)\n }\n";
13
+ async function c(e) {
14
+ let n = await t({
14
15
  gateway: "workspace",
15
- query: t.query,
16
- operationName: t.operationName,
17
- variables: t.variables,
18
- authToken: t.authContext?.accessToken ?? void 0,
16
+ query: e.query,
17
+ operationName: e.operationName,
18
+ variables: e.variables,
19
+ authToken: e.authContext?.accessToken ?? void 0,
19
20
  workspaceToken: !0,
20
- workspaceId: t.workspaceId,
21
- organizationId: t.authContext?.organizationId ?? !0,
21
+ workspaceId: e.workspaceId,
22
+ organizationId: e.authContext?.organizationId ?? !0,
22
23
  suppressGlobalErrorEvent: !0
23
24
  });
24
25
  if (n.errors?.length) throw Error(n.errors.map((e) => e.message).join(", "));
25
- if (!n.data) throw Error(`${t.operationName} returned no data`);
26
+ if (!n.data) throw Error(`${e.operationName} returned no data`);
26
27
  return n.data;
27
28
  }
28
- async function c(e, t, n = 25) {
29
- return (await s({
30
- query: r,
29
+ async function l(e, t, r = 25) {
30
+ return (await c({
31
+ query: i,
31
32
  operationName: "AssistantConversations",
32
33
  variables: {
33
- productId: "bigconsole",
34
- first: n
34
+ productId: n,
35
+ first: r
35
36
  },
36
37
  workspaceId: e,
37
38
  authContext: t
38
39
  })).assistantConversations ?? [];
39
40
  }
40
- async function l(e, t, n, r = 200) {
41
- return (await s({
42
- query: i,
41
+ async function u(e, t, n, r = 200) {
42
+ return (await c({
43
+ query: a,
43
44
  operationName: "AssistantConversationMessages",
44
45
  variables: {
45
46
  conversationId: e,
@@ -49,24 +50,24 @@ async function l(e, t, n, r = 200) {
49
50
  authContext: n
50
51
  })).assistantConversationMessages ?? [];
51
52
  }
52
- async function u(e, n, r) {
53
- return (await s({
54
- query: a,
53
+ async function d(e, t, r) {
54
+ return (await c({
55
+ query: o,
55
56
  operationName: "SaveAssistantTurn",
56
57
  variables: { input: {
57
58
  conversationId: e.conversationId,
58
- productId: t,
59
+ productId: n,
59
60
  prompt: e.prompt,
60
61
  reply: e.reply,
61
62
  agentSessionId: e.agentSessionId
62
63
  } },
63
- workspaceId: n,
64
+ workspaceId: t,
64
65
  authContext: r
65
66
  })).saveAssistantTurn;
66
67
  }
67
- async function d(e, t, n) {
68
- await s({
69
- query: o,
68
+ async function f(e, t, n) {
69
+ await c({
70
+ query: s,
70
71
  operationName: "DeleteAssistantConversation",
71
72
  variables: { conversationId: e },
72
73
  workspaceId: t,
@@ -74,6 +75,6 @@ async function d(e, t, n) {
74
75
  });
75
76
  }
76
77
  //#endregion
77
- export { d as deleteAssistantConversation, l as getAssistantConversationMessages, c as listAssistantConversations, u as saveAssistantTurn };
78
+ export { f as deleteAssistantConversation, u as getAssistantConversationMessages, l as listAssistantConversations, d as saveAssistantTurn };
78
79
 
79
80
  //# sourceMappingURL=conversationHistoryApi.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"conversationHistoryApi.js","names":[],"sources":["../../../src/bigconsole/assistant/conversationHistoryApi.ts"],"sourcesContent":["/**\n * Durable assistant history, stored in wspace-conversations.\n *\n * The agent's own session lives in a sandbox with a 10-minute TTL and no\n * persistent volume, so it cannot be the store of record for a transcript the\n * user expects to keep. Every completed turn is written here instead, tagged\n * with the product that owns it, so history is durable AND product-scoped —\n * a BigConsole chat can never appear in another product's panel.\n *\n * Raw operation strings rather than generated documents: these ops are only used\n * here, and `graphqlFetch` takes a query string, so there is nothing to gain\n * from putting them through codegen.\n */\n\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\n\nimport type { AssistantSandboxAuthContext } from './types';\n\n/** Every conversation written from here belongs to BigConsole. */\nexport const ASSISTANT_PRODUCT_ID = 'bigconsole';\n\nexport interface AssistantConversationSummary {\n id: string;\n title: string | null;\n agentSessionId: string | null;\n updatedAt: string;\n}\n\nexport interface AssistantHistoryTurnMessage {\n id: string;\n role: 'USER' | 'ASSISTANT';\n content: string;\n createdAt: string;\n}\n\nconst CONVERSATION_FIELDS = `\n id\n title\n agentSessionId\n updatedAt\n`;\n\nconst LIST_CONVERSATIONS = `\n query AssistantConversations($productId: String!, $first: Int) {\n assistantConversations(productId: $productId, first: $first) { ${CONVERSATION_FIELDS} }\n }\n`;\n\nconst CONVERSATION_MESSAGES = `\n query AssistantConversationMessages($conversationId: ID!, $first: Int) {\n assistantConversationMessages(conversationId: $conversationId, first: $first) {\n id\n role\n content\n createdAt\n }\n }\n`;\n\nconst SAVE_TURN = `\n mutation SaveAssistantTurn($input: SaveAssistantTurnInput!) {\n saveAssistantTurn(input: $input) { ${CONVERSATION_FIELDS} }\n }\n`;\n\nconst DELETE_CONVERSATION = `\n mutation DeleteAssistantConversation($conversationId: ID!) {\n deleteAssistantConversation(conversationId: $conversationId)\n }\n`;\n\nasync function historyFetch<TData>(params: {\n query: string;\n operationName: string;\n variables: Record<string, unknown>;\n workspaceId: string;\n authContext?: AssistantSandboxAuthContext;\n}): Promise<TData> {\n const result = await graphqlFetch<TData>({\n gateway: 'workspace',\n query: params.query,\n operationName: params.operationName,\n variables: params.variables,\n authToken: params.authContext?.accessToken ?? undefined,\n // Mint the workspace token rather than passing a possibly-stale string —\n // same reasoning as assistantApi: a stale token is nulled out and NOT\n // replaced unless the caller opts in here.\n workspaceToken: true,\n workspaceId: params.workspaceId,\n organizationId: params.authContext?.organizationId ?? true,\n suppressGlobalErrorEvent: true,\n });\n\n if (result.errors?.length) {\n throw new Error(result.errors.map((error) => error.message).join(', '));\n }\n if (!result.data) {\n throw new Error(`${params.operationName} returned no data`);\n }\n\n return result.data;\n}\n\nexport async function listAssistantConversations(\n workspaceId: string,\n authContext?: AssistantSandboxAuthContext,\n first = 25\n): Promise<AssistantConversationSummary[]> {\n const data = await historyFetch<{ assistantConversations: AssistantConversationSummary[] }>({\n query: LIST_CONVERSATIONS,\n operationName: 'AssistantConversations',\n variables: { productId: ASSISTANT_PRODUCT_ID, first },\n workspaceId,\n authContext,\n });\n return data.assistantConversations ?? [];\n}\n\nexport async function getAssistantConversationMessages(\n conversationId: string,\n workspaceId: string,\n authContext?: AssistantSandboxAuthContext,\n first = 200\n): Promise<AssistantHistoryTurnMessage[]> {\n const data = await historyFetch<{\n assistantConversationMessages: AssistantHistoryTurnMessage[];\n }>({\n query: CONVERSATION_MESSAGES,\n operationName: 'AssistantConversationMessages',\n variables: { conversationId, first },\n workspaceId,\n authContext,\n });\n return data.assistantConversationMessages ?? [];\n}\n\n/**\n * Persist one completed turn. Creates the conversation on the first turn and\n * returns it, so the caller can hold on to the id.\n */\nexport async function saveAssistantTurn(\n params: {\n conversationId: string | null;\n prompt: string;\n reply: string;\n agentSessionId: string | null;\n },\n workspaceId: string,\n authContext?: AssistantSandboxAuthContext\n): Promise<AssistantConversationSummary> {\n const data = await historyFetch<{ saveAssistantTurn: AssistantConversationSummary }>({\n query: SAVE_TURN,\n operationName: 'SaveAssistantTurn',\n variables: {\n input: {\n conversationId: params.conversationId,\n productId: ASSISTANT_PRODUCT_ID,\n prompt: params.prompt,\n reply: params.reply,\n agentSessionId: params.agentSessionId,\n },\n },\n workspaceId,\n authContext,\n });\n return data.saveAssistantTurn;\n}\n\nexport async function deleteAssistantConversation(\n conversationId: string,\n workspaceId: string,\n authContext?: AssistantSandboxAuthContext\n): Promise<void> {\n await historyFetch<{ deleteAssistantConversation: boolean }>({\n query: DELETE_CONVERSATION,\n operationName: 'DeleteAssistantConversation',\n variables: { conversationId },\n workspaceId,\n authContext,\n });\n}\n"],"mappings":";;AAmBA,IAAa,IAAuB,cAgB9B,IAAsB,oDAOtB,IAAqB;;qEAE0C,EAAoB;;GAInF,IAAwB,yOAWxB,IAAY;;yCAEuB,EAAoB;;GAIvD,IAAsB;AAM5B,eAAe,EAAoB,GAMhB;CACjB,IAAM,IAAS,MAAM,EAAoB;EACvC,SAAS;EACT,OAAO,EAAO;EACd,eAAe,EAAO;EACtB,WAAW,EAAO;EAClB,WAAW,EAAO,aAAa,eAAe,KAAA;EAI9C,gBAAgB;EAChB,aAAa,EAAO;EACpB,gBAAgB,EAAO,aAAa,kBAAkB;EACtD,0BAA0B;EAC3B,CAAC;AAEF,KAAI,EAAO,QAAQ,OACjB,OAAU,MAAM,EAAO,OAAO,KAAK,MAAU,EAAM,QAAQ,CAAC,KAAK,KAAK,CAAC;AAEzE,KAAI,CAAC,EAAO,KACV,OAAU,MAAM,GAAG,EAAO,cAAc,mBAAmB;AAG7D,QAAO,EAAO;;AAGhB,eAAsB,EACpB,GACA,GACA,IAAQ,IACiC;AAQzC,SAPa,MAAM,EAAyE;EAC1F,OAAO;EACP,eAAe;EACf,WAAW;GAAE,WAAA;GAAiC;GAAO;EACrD;EACA;EACD,CAAC,EACU,0BAA0B,EAAE;;AAG1C,eAAsB,EACpB,GACA,GACA,GACA,IAAQ,KACgC;AAUxC,SATa,MAAM,EAEhB;EACD,OAAO;EACP,eAAe;EACf,WAAW;GAAE;GAAgB;GAAO;EACpC;EACA;EACD,CAAC,EACU,iCAAiC,EAAE;;AAOjD,eAAsB,EACpB,GAMA,GACA,GACuC;AAgBvC,SAfa,MAAM,EAAkE;EACnF,OAAO;EACP,eAAe;EACf,WAAW,EACT,OAAO;GACL,gBAAgB,EAAO;GACvB,WAAW;GACX,QAAQ,EAAO;GACf,OAAO,EAAO;GACd,gBAAgB,EAAO;GACxB,EACF;EACD;EACA;EACD,CAAC,EACU;;AAGd,eAAsB,EACpB,GACA,GACA,GACe;AACf,OAAM,EAAuD;EAC3D,OAAO;EACP,eAAe;EACf,WAAW,EAAE,mBAAgB;EAC7B;EACA;EACD,CAAC"}
1
+ {"version":3,"file":"conversationHistoryApi.js","names":[],"sources":["../../../src/bigconsole/assistant/conversationHistoryApi.ts"],"sourcesContent":["/**\n * Durable assistant history, stored in wspace-conversations.\n *\n * The agent's own session lives in a sandbox with a 10-minute TTL and no\n * persistent volume, so it cannot be the store of record for a transcript the\n * user expects to keep. Every completed turn is written here instead, tagged\n * with the product that owns it, so history is durable AND product-scoped —\n * a BigConsole chat can never appear in another product's panel.\n *\n * Raw operation strings rather than generated documents: these ops are only used\n * here, and `graphqlFetch` takes a query string, so there is nothing to gain\n * from putting them through codegen.\n */\n\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\n\nimport { BIGCONSOLE_PRODUCT } from './product';\nimport type { AssistantSandboxAuthContext } from './types';\n\n/**\n * Every conversation written from here belongs to BigConsole.\n *\n * ★ Same value as `assistantApi`'s `ASSISTANT_PRODUCT`, and now literally the\n * same source. These were two independent string literals under two different\n * names — history rows are tagged with this one while sandboxes are stamped with\n * that one, so a divergence would have hidden a product's whole transcript from\n * its own panel without failing anything.\n */\nexport const ASSISTANT_PRODUCT_ID = BIGCONSOLE_PRODUCT;\n\nexport interface AssistantConversationSummary {\n id: string;\n title: string | null;\n agentSessionId: string | null;\n updatedAt: string;\n}\n\nexport interface AssistantHistoryTurnMessage {\n id: string;\n role: 'USER' | 'ASSISTANT';\n content: string;\n createdAt: string;\n}\n\nconst CONVERSATION_FIELDS = `\n id\n title\n agentSessionId\n updatedAt\n`;\n\nconst LIST_CONVERSATIONS = `\n query AssistantConversations($productId: String!, $first: Int) {\n assistantConversations(productId: $productId, first: $first) { ${CONVERSATION_FIELDS} }\n }\n`;\n\nconst CONVERSATION_MESSAGES = `\n query AssistantConversationMessages($conversationId: ID!, $first: Int) {\n assistantConversationMessages(conversationId: $conversationId, first: $first) {\n id\n role\n content\n createdAt\n }\n }\n`;\n\nconst SAVE_TURN = `\n mutation SaveAssistantTurn($input: SaveAssistantTurnInput!) {\n saveAssistantTurn(input: $input) { ${CONVERSATION_FIELDS} }\n }\n`;\n\nconst DELETE_CONVERSATION = `\n mutation DeleteAssistantConversation($conversationId: ID!) {\n deleteAssistantConversation(conversationId: $conversationId)\n }\n`;\n\nasync function historyFetch<TData>(params: {\n query: string;\n operationName: string;\n variables: Record<string, unknown>;\n workspaceId: string;\n authContext?: AssistantSandboxAuthContext;\n}): Promise<TData> {\n const result = await graphqlFetch<TData>({\n gateway: 'workspace',\n query: params.query,\n operationName: params.operationName,\n variables: params.variables,\n authToken: params.authContext?.accessToken ?? undefined,\n // Mint the workspace token rather than passing a possibly-stale string —\n // same reasoning as assistantApi: a stale token is nulled out and NOT\n // replaced unless the caller opts in here.\n workspaceToken: true,\n workspaceId: params.workspaceId,\n organizationId: params.authContext?.organizationId ?? true,\n suppressGlobalErrorEvent: true,\n });\n\n if (result.errors?.length) {\n throw new Error(result.errors.map((error) => error.message).join(', '));\n }\n if (!result.data) {\n throw new Error(`${params.operationName} returned no data`);\n }\n\n return result.data;\n}\n\nexport async function listAssistantConversations(\n workspaceId: string,\n authContext?: AssistantSandboxAuthContext,\n first = 25\n): Promise<AssistantConversationSummary[]> {\n const data = await historyFetch<{ assistantConversations: AssistantConversationSummary[] }>({\n query: LIST_CONVERSATIONS,\n operationName: 'AssistantConversations',\n variables: { productId: ASSISTANT_PRODUCT_ID, first },\n workspaceId,\n authContext,\n });\n return data.assistantConversations ?? [];\n}\n\nexport async function getAssistantConversationMessages(\n conversationId: string,\n workspaceId: string,\n authContext?: AssistantSandboxAuthContext,\n first = 200\n): Promise<AssistantHistoryTurnMessage[]> {\n const data = await historyFetch<{\n assistantConversationMessages: AssistantHistoryTurnMessage[];\n }>({\n query: CONVERSATION_MESSAGES,\n operationName: 'AssistantConversationMessages',\n variables: { conversationId, first },\n workspaceId,\n authContext,\n });\n return data.assistantConversationMessages ?? [];\n}\n\n/**\n * Persist one completed turn. Creates the conversation on the first turn and\n * returns it, so the caller can hold on to the id.\n */\nexport async function saveAssistantTurn(\n params: {\n conversationId: string | null;\n prompt: string;\n reply: string;\n agentSessionId: string | null;\n },\n workspaceId: string,\n authContext?: AssistantSandboxAuthContext\n): Promise<AssistantConversationSummary> {\n const data = await historyFetch<{ saveAssistantTurn: AssistantConversationSummary }>({\n query: SAVE_TURN,\n operationName: 'SaveAssistantTurn',\n variables: {\n input: {\n conversationId: params.conversationId,\n productId: ASSISTANT_PRODUCT_ID,\n prompt: params.prompt,\n reply: params.reply,\n agentSessionId: params.agentSessionId,\n },\n },\n workspaceId,\n authContext,\n });\n return data.saveAssistantTurn;\n}\n\nexport async function deleteAssistantConversation(\n conversationId: string,\n workspaceId: string,\n authContext?: AssistantSandboxAuthContext\n): Promise<void> {\n await historyFetch<{ deleteAssistantConversation: boolean }>({\n query: DELETE_CONVERSATION,\n operationName: 'DeleteAssistantConversation',\n variables: { conversationId },\n workspaceId,\n authContext,\n });\n}\n"],"mappings":";;;AA4BA,IAAa,IAAuB,GAgB9B,IAAsB,oDAOtB,IAAqB;;qEAE0C,EAAoB;;GAInF,IAAwB,yOAWxB,IAAY;;yCAEuB,EAAoB;;GAIvD,IAAsB;AAM5B,eAAe,EAAoB,GAMhB;CACjB,IAAM,IAAS,MAAM,EAAoB;EACvC,SAAS;EACT,OAAO,EAAO;EACd,eAAe,EAAO;EACtB,WAAW,EAAO;EAClB,WAAW,EAAO,aAAa,eAAe,KAAA;EAI9C,gBAAgB;EAChB,aAAa,EAAO;EACpB,gBAAgB,EAAO,aAAa,kBAAkB;EACtD,0BAA0B;EAC3B,CAAC;AAEF,KAAI,EAAO,QAAQ,OACjB,OAAU,MAAM,EAAO,OAAO,KAAK,MAAU,EAAM,QAAQ,CAAC,KAAK,KAAK,CAAC;AAEzE,KAAI,CAAC,EAAO,KACV,OAAU,MAAM,GAAG,EAAO,cAAc,mBAAmB;AAG7D,QAAO,EAAO;;AAGhB,eAAsB,EACpB,GACA,GACA,IAAQ,IACiC;AAQzC,SAPa,MAAM,EAAyE;EAC1F,OAAO;EACP,eAAe;EACf,WAAW;GAAE,WAAW;GAAsB;GAAO;EACrD;EACA;EACD,CAAC,EACU,0BAA0B,EAAE;;AAG1C,eAAsB,EACpB,GACA,GACA,GACA,IAAQ,KACgC;AAUxC,SATa,MAAM,EAEhB;EACD,OAAO;EACP,eAAe;EACf,WAAW;GAAE;GAAgB;GAAO;EACpC;EACA;EACD,CAAC,EACU,iCAAiC,EAAE;;AAOjD,eAAsB,EACpB,GAMA,GACA,GACuC;AAgBvC,SAfa,MAAM,EAAkE;EACnF,OAAO;EACP,eAAe;EACf,WAAW,EACT,OAAO;GACL,gBAAgB,EAAO;GACvB,WAAW;GACX,QAAQ,EAAO;GACf,OAAO,EAAO;GACd,gBAAgB,EAAO;GACxB,EACF;EACD;EACA;EACD,CAAC,EACU;;AAGd,eAAsB,EACpB,GACA,GACA,GACe;AACf,OAAM,EAAuD;EAC3D,OAAO;EACP,eAAe;EACf,WAAW,EAAE,mBAAgB;EAC7B;EACA;EACD,CAAC"}
@@ -1,4 +1,10 @@
1
- import { gatherPageContext as e, resolveBigConsoleConversationContext as t } from "./pageContext.js";
2
- import { useSandboxAssistantTransport as n } from "./createSandboxAssistantTransport.js";
3
- import { AiPreviewMiniPanel as r } from "../components/preview/AiPreviewMiniPanel.js";
4
- export { r as AiPreviewMiniPanel, e as gatherPageContext, t as resolveBigConsoleConversationContext, n as useSandboxAssistantTransport };
1
+ import { BIGCONSOLE_PRODUCT as e } from "./product.js";
2
+ import { describeCurrentPage as t, gatherPageContext as n, resolveBigConsoleConversationContext as r } from "./pageContext.js";
3
+ import { useSandboxAssistantTransport as i } from "./createSandboxAssistantTransport.js";
4
+ import { AiPreviewMiniPanel as a } from "../components/preview/AiPreviewMiniPanel.js";
5
+ import { API_CALLS_CONFIRM_PREAMBLE as o, USER_REQUEST_DELIMITER as s, buildPromptForMode as c } from "./assistantPrompt.js";
6
+ import { ASK as l, BIGCONSOLE_ASSISTANT_MODES as u, BUILD as d } from "./modes.js";
7
+ import { BIGCONSOLE_ASSISTANT_PRODUCT as f, STORAGE_FOLDERS as p } from "./assistantProduct.js";
8
+ import { PROBE_RESOURCES as m, useWorkspaceWriteAccess as h } from "./useWorkspaceWriteAccess.js";
9
+ import { useBigConsoleAssistantPreview as g } from "./previewController.js";
10
+ export { o as API_CALLS_CONFIRM_PREAMBLE, l as ASK, a as AiPreviewMiniPanel, u as BIGCONSOLE_ASSISTANT_MODES, f as BIGCONSOLE_ASSISTANT_PRODUCT, e as BIGCONSOLE_PRODUCT, d as BUILD, m as PROBE_RESOURCES, p as STORAGE_FOLDERS, s as USER_REQUEST_DELIMITER, c as buildPromptForMode, t as describeCurrentPage, n as gatherPageContext, r as resolveBigConsoleConversationContext, g as useBigConsoleAssistantPreview, i as useSandboxAssistantTransport, h as useWorkspaceWriteAccess };
@@ -0,0 +1,29 @@
1
+ //#region src/bigconsole/assistant/modes.ts
2
+ var e = {
3
+ id: "assistant",
4
+ label: "Ask",
5
+ description: "Ask about BigConsole and your workspace. It reads your dashboards, data sinks, parsers and widgets, explains what you are looking at and points you to the right page. Read-only.",
6
+ suggestions: [
7
+ "What can I do on this screen?",
8
+ "What is in this data sink?",
9
+ "Which widgets are on this dashboard?",
10
+ "Why is my parser returning no rows?"
11
+ ]
12
+ }, t = {
13
+ id: "api-calls",
14
+ label: "Build",
15
+ description: "Let the assistant build for you: create a data sink, a dashboard, a parser and the widgets on it. It shows you each change and waits for your approval before anything is saved.",
16
+ suggestions: [
17
+ "Build a school attendance dashboard from this CSV",
18
+ "Add a bar chart of monthly totals to this dashboard",
19
+ "Create a parser that splits the address column",
20
+ "Show me what this data sink contains"
21
+ ],
22
+ requiresAdmin: !0,
23
+ writeCapable: !0,
24
+ streamBudgetMs: 42e4
25
+ }, n = [e, t];
26
+ //#endregion
27
+ export { e as ASK, n as BIGCONSOLE_ASSISTANT_MODES, t as BUILD };
28
+
29
+ //# sourceMappingURL=modes.js.map
@@ -0,0 +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,24 +1,37 @@
1
+ import { BIGCONSOLE_PRODUCT as e } from "./product.js";
1
2
  //#region src/bigconsole/assistant/pageContext.ts
2
- function e() {
3
- let e = window.location.href, t = window.location.pathname.split("/").filter(Boolean).slice(0, 3).join("/") || "home", r = n(window.location.pathname, window.location.search);
3
+ function t() {
4
+ let t = window.location.href, n = window.location.pathname.split("/").filter(Boolean).slice(0, 3).join("/") || "home", r = o(window.location.pathname, window.location.search);
4
5
  return {
5
- currentUrl: e,
6
- currentPage: t,
6
+ currentUrl: t,
7
+ currentPage: n,
7
8
  entityType: r.entityType,
8
9
  entityId: r.entityId,
9
- platform: "bigconsole",
10
+ platform: e,
10
11
  version: "2026.430.500",
11
12
  viewportSize: `${window.innerWidth}x${window.innerHeight}`,
12
13
  userAgent: navigator.userAgent,
13
14
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
14
15
  };
15
16
  }
16
- function t(e, t) {
17
+ var n = "Home", r = {
18
+ home: n,
19
+ dashboards: "Dashboards",
20
+ datasinks: "Data sinks",
21
+ parsers: "Parsers",
22
+ pipelines: "Pipelines",
23
+ workflows: "Workflows"
24
+ };
25
+ function i() {
26
+ let { currentPage: e } = t(), i = e.split("/").filter(Boolean), a = (i[0] === "bigconsole" ? i[1] : i[0]) ?? "";
27
+ return !a || a === "home" ? n : r[a] ?? a.split("-").map((e) => e && e.charAt(0).toUpperCase() + e.slice(1)).join(" ");
28
+ }
29
+ function a(e, t) {
17
30
  return t.match(e);
18
31
  }
19
- function n(e, n) {
20
- let i = new URLSearchParams(n);
21
- for (let [n, i] of [
32
+ function o(e, t) {
33
+ let n = new URLSearchParams(t);
34
+ for (let [t, n] of [
22
35
  [/^\/bigconsole\/dashboards\/[^/]+\/parsers\/([^/]+)(?:\/edit)?$/, "BIGCONSOLE_PARSER"],
23
36
  [/^\/bigconsole\/dashboards\/([^/]+)(?:\/builder)?$/, "BIGCONSOLE_DASHBOARD"],
24
37
  [/^\/bigconsole\/datasinks\/([^/]+)(?:\/edit)?$/, "BIGCONSOLE_DATASINK"],
@@ -26,26 +39,26 @@ function n(e, n) {
26
39
  [/^\/bigconsole\/pipelines\/([^/]+)(?:\/edit)?$/, "BIGCONSOLE_PIPELINE"],
27
40
  [/^\/bigconsole\/workflows\/([^/]+)(?:\/edit)?$/, "BIGCONSOLE_WORKFLOW"]
28
41
  ]) {
29
- let a = t(n, e);
30
- if (a && a[1] && !r(a[1])) return {
31
- entityType: i,
32
- entityId: a[1],
33
- metadata: { routePattern: n.source }
42
+ let r = a(t, e);
43
+ if (r && r[1] && !s(r[1])) return {
44
+ entityType: n,
45
+ entityId: r[1],
46
+ metadata: { routePattern: t.source }
34
47
  };
35
48
  }
36
- let a = i.get("tab") ?? (() => {
49
+ let r = n.get("tab") ?? (() => {
37
50
  let t = e.split("/").filter(Boolean);
38
51
  return t.length > 2 ? t[t.length - 1] : void 0;
39
52
  })();
40
53
  return {
41
- activeTab: a ?? void 0,
42
- metadata: { ...a ? { activeTab: a } : {} }
54
+ activeTab: r ?? void 0,
55
+ metadata: { ...r ? { activeTab: r } : {} }
43
56
  };
44
57
  }
45
- function r(e) {
58
+ function s(e) {
46
59
  return e === "new" || e === "builder" || e === "list";
47
60
  }
48
61
  //#endregion
49
- export { e as gatherPageContext, n as resolveBigConsoleConversationContext };
62
+ export { i as describeCurrentPage, t as gatherPageContext, o as resolveBigConsoleConversationContext };
50
63
 
51
64
  //# sourceMappingURL=pageContext.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"pageContext.js","names":[],"sources":["../../../src/bigconsole/assistant/pageContext.ts"],"sourcesContent":["/**\n * Page context gathering for BigConsole — captures the user's current browser\n * context to be sent alongside every assistant prompt for better AI awareness.\n *\n * Mirrors microfe-vibecontrols' `utils/pageContext.ts`. The agent filters the\n * payload to an allow-list (`ai-assistant/src/context-loader.ts` →\n * `ALLOWED_CONTEXT_KEYS`), so the keys MUST be `currentUrl` (NOT `activeUrl`)\n * and the generic `entityType` / `entityId`, or the value is silently dropped.\n */\n\ninterface ConversationContextShape {\n entityType?: string;\n entityId?: string;\n activeTab?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface PageContext {\n // `currentUrl` (NOT `activeUrl`) — must match the agent's allowed-context-key\n // contract, otherwise the agent filters it out.\n currentUrl: string;\n currentPage: string;\n // The entity the user is currently looking at, resolved from the route. Lets\n // the assistant act on \"this / current / here\" without asking for an id.\n entityType?: string;\n entityId?: string;\n platform: string;\n version: string;\n viewportSize: string;\n userAgent: string;\n timestamp: string;\n}\n\nexport function gatherPageContext(): PageContext {\n const url = window.location.href;\n const pathSegments = window.location.pathname.split('/').filter(Boolean);\n const currentPage = pathSegments.slice(0, 3).join('/') || 'home';\n const entity = resolveBigConsoleConversationContext(window.location.pathname, window.location.search);\n\n return {\n currentUrl: url,\n currentPage,\n entityType: entity.entityType,\n entityId: entity.entityId,\n platform: 'bigconsole',\n version: import.meta.env.VITE_APP_VERSION ?? '2026.430.500',\n viewportSize: `${window.innerWidth}x${window.innerHeight}`,\n userAgent: navigator.userAgent,\n timestamp: new Date().toISOString(),\n };\n}\n\nfunction matchPath(pattern: RegExp, pathname: string): RegExpMatchArray | null {\n return pathname.match(pattern);\n}\n\n/**\n * Maps the current BigConsole route to `{ entityType, entityId }`. The BigConsole\n * MFE mounts under the `/bigconsole` base path in the host shell, so detail\n * routes are matched as `/bigconsole/<collection>/:id`.\n *\n * More specific routes (e.g. dashboard-scoped parsers, builder/edit sub-routes)\n * are matched before their parent so the most relevant entity wins.\n */\nexport function resolveBigConsoleConversationContext(pathname: string, search: string): ConversationContextShape {\n const params = new URLSearchParams(search);\n\n const directMatches: Array<[RegExp, string]> = [\n // Dashboard-scoped parser detail (more specific than the bare parser route).\n [/^\\/bigconsole\\/dashboards\\/[^/]+\\/parsers\\/([^/]+)(?:\\/edit)?$/, 'BIGCONSOLE_PARSER'],\n // Dashboard detail + builder.\n [/^\\/bigconsole\\/dashboards\\/([^/]+)(?:\\/builder)?$/, 'BIGCONSOLE_DASHBOARD'],\n // Data sinks.\n [/^\\/bigconsole\\/datasinks\\/([^/]+)(?:\\/edit)?$/, 'BIGCONSOLE_DATASINK'],\n // Parsers.\n [/^\\/bigconsole\\/parsers\\/([^/]+)(?:\\/edit)?$/, 'BIGCONSOLE_PARSER'],\n // Pipelines.\n [/^\\/bigconsole\\/pipelines\\/([^/]+)(?:\\/edit)?$/, 'BIGCONSOLE_PIPELINE'],\n // Workflows (BigConsole-native list/view; the SDK builder uses /workflow/...).\n [/^\\/bigconsole\\/workflows\\/([^/]+)(?:\\/edit)?$/, 'BIGCONSOLE_WORKFLOW'],\n ];\n\n for (const [pattern, entityType] of directMatches) {\n const match = matchPath(pattern, pathname);\n if (match && match[1] && !isReservedSegment(match[1])) {\n return {\n entityType,\n entityId: match[1],\n metadata: {\n routePattern: pattern.source,\n },\n };\n }\n }\n\n const activeTab =\n params.get('tab') ??\n (() => {\n const segments = pathname.split('/').filter(Boolean);\n return segments.length > 2 ? segments[segments.length - 1] : undefined;\n })();\n\n return {\n activeTab: activeTab ?? undefined,\n metadata: {\n ...(activeTab ? { activeTab } : {}),\n },\n };\n}\n\n/**\n * Static segments that share a route shape with `:id` detail routes\n * (e.g. `/bigconsole/datasinks/new`, `/bigconsole/parsers/new`). These are\n * pages, not entities, so they must not be reported as an `entityId`.\n */\nfunction isReservedSegment(segment: string): boolean {\n return segment === 'new' || segment === 'builder' || segment === 'list';\n}\n"],"mappings":";AAiCA,SAAgB,IAAiC;CAC/C,IAAM,IAAM,OAAO,SAAS,MAEtB,IADe,OAAO,SAAS,SAAS,MAAM,IAAI,CAAC,OAAO,QAAQ,CACvC,MAAM,GAAG,EAAE,CAAC,KAAK,IAAI,IAAI,QACpD,IAAS,EAAqC,OAAO,SAAS,UAAU,OAAO,SAAS,OAAO;AAErG,QAAO;EACL,YAAY;EACZ;EACA,YAAY,EAAO;EACnB,UAAU,EAAO;EACjB,UAAU;EACV,SAA6C;EAC7C,cAAc,GAAG,OAAO,WAAW,GAAG,OAAO;EAC7C,WAAW,UAAU;EACrB,4BAAW,IAAI,MAAM,EAAC,aAAa;EACpC;;AAGH,SAAS,EAAU,GAAiB,GAA2C;AAC7E,QAAO,EAAS,MAAM,EAAQ;;AAWhC,SAAgB,EAAqC,GAAkB,GAA0C;CAC/G,IAAM,IAAS,IAAI,gBAAgB,EAAO;AAiB1C,MAAK,IAAM,CAAC,GAAS,MAf0B;EAE7C,CAAC,kEAAkE,oBAAoB;EAEvF,CAAC,qDAAqD,uBAAuB;EAE7E,CAAC,iDAAiD,sBAAsB;EAExE,CAAC,+CAA+C,oBAAoB;EAEpE,CAAC,iDAAiD,sBAAsB;EAExE,CAAC,iDAAiD,sBAAsB;EACzE,EAEkD;EACjD,IAAM,IAAQ,EAAU,GAAS,EAAS;AAC1C,MAAI,KAAS,EAAM,MAAM,CAAC,EAAkB,EAAM,GAAG,CACnD,QAAO;GACL;GACA,UAAU,EAAM;GAChB,UAAU,EACR,cAAc,EAAQ,QACvB;GACF;;CAIL,IAAM,IACJ,EAAO,IAAI,MAAM,WACV;EACL,IAAM,IAAW,EAAS,MAAM,IAAI,CAAC,OAAO,QAAQ;AACpD,SAAO,EAAS,SAAS,IAAI,EAAS,EAAS,SAAS,KAAK,KAAA;KAC3D;AAEN,QAAO;EACL,WAAW,KAAa,KAAA;EACxB,UAAU,EACR,GAAI,IAAY,EAAE,cAAW,GAAG,EAAE,EACnC;EACF;;AAQH,SAAS,EAAkB,GAA0B;AACnD,QAAO,MAAY,SAAS,MAAY,aAAa,MAAY"}
1
+ {"version":3,"file":"pageContext.js","names":[],"sources":["../../../src/bigconsole/assistant/pageContext.ts"],"sourcesContent":["/**\n * Page context gathering for BigConsole — captures the user's current browser\n * context to be sent alongside every assistant prompt for better AI awareness.\n *\n * Mirrors microfe-vibecontrols' `utils/pageContext.ts`. The agent filters the\n * payload to an allow-list (`ai-assistant/src/context-loader.ts` →\n * `ALLOWED_CONTEXT_KEYS`), so the keys MUST be `currentUrl` (NOT `activeUrl`)\n * and the generic `entityType` / `entityId`, or the value is silently dropped.\n */\n\nimport { BIGCONSOLE_PRODUCT } from './product';\n\ninterface ConversationContextShape {\n entityType?: string;\n entityId?: string;\n activeTab?: string;\n metadata?: Record<string, unknown>;\n}\n\n/**\n * ★★ A `type`, NOT an `interface`, and the difference is load-bearing.\n *\n * fe-libs' product seam declares `gatherPageContext?: () => PageContext` against\n * ITS OWN `PageContext`, which carries an index signature\n * (`[key: string]: unknown`) so a product may add keys of its own. TypeScript\n * gives a type alias an IMPLICIT index signature and an interface NONE —\n * an interface stays open to declaration merging, so the compiler cannot know\n * its full set of keys and refuses the assignment.\n *\n * So this exact shape, written as an interface, is not assignable to fe-libs'\n * parameter: `assistantProduct.ts` fails with TS2322 while the runtime value is\n * perfectly correct. Changing this back to `interface` re-breaks the seam.\n */\nexport type PageContext = {\n // `currentUrl` (NOT `activeUrl`) — must match the agent's allowed-context-key\n // contract, otherwise the agent filters it out.\n currentUrl: string;\n currentPage: string;\n // The entity the user is currently looking at, resolved from the route. Lets\n // the assistant act on \"this / current / here\" without asking for an id.\n entityType?: string;\n entityId?: string;\n platform: string;\n version: string;\n viewportSize: string;\n userAgent: string;\n timestamp: string;\n};\n\nexport function gatherPageContext(): PageContext {\n const url = window.location.href;\n const pathSegments = window.location.pathname.split('/').filter(Boolean);\n const currentPage = pathSegments.slice(0, 3).join('/') || 'home';\n const entity = resolveBigConsoleConversationContext(window.location.pathname, window.location.search);\n\n return {\n currentUrl: url,\n currentPage,\n entityType: entity.entityType,\n entityId: entity.entityId,\n platform: BIGCONSOLE_PRODUCT,\n version: import.meta.env.VITE_APP_VERSION ?? '2026.430.500',\n viewportSize: `${window.innerWidth}x${window.innerHeight}`,\n userAgent: navigator.userAgent,\n timestamp: new Date().toISOString(),\n };\n}\n\n/** Shown to the user as \"where you are\", e.g. in the panel's empty state. */\nconst HOME_LABEL = 'Home';\n\n/**\n * Route segment → human label.\n *\n * ★ Only the collections BigConsole actually routes to. Anything missing falls\n * through to a title-cased segment, which is right far more often than a wrong\n * label would be — and a wrong answer here is worse than none, because the panel\n * states it as fact.\n */\nconst PAGE_LABELS: Readonly<Record<string, string>> = {\n home: HOME_LABEL,\n dashboards: 'Dashboards',\n datasinks: 'Data sinks',\n parsers: 'Parsers',\n pipelines: 'Pipelines',\n workflows: 'Workflows',\n};\n\n/**\n * A human label for where the user is right now, for the shared panel.\n *\n * ★★ The `/bigconsole` MOUNT SEGMENT IS SKIPPED. This MFE mounts under\n * `/bigconsole` in the host shell, so the first segment is the product name on\n * every single page — without this, every location would be described as\n * \"Bigconsole\", which is both useless and wrong.\n *\n * ★ Derived from `gatherPageContext().currentPage` rather than reading\n * `window.location` a second time, so the label and the agent's structured\n * context can never disagree about which page they describe.\n */\nexport function describeCurrentPage(): string {\n const { currentPage } = gatherPageContext();\n const segments = currentPage.split('/').filter(Boolean);\n const first = (segments[0] === BIGCONSOLE_PRODUCT ? segments[1] : segments[0]) ?? '';\n if (!first || first === 'home') return HOME_LABEL;\n return (\n PAGE_LABELS[first] ??\n first\n .split('-')\n .map((word) => (word ? word.charAt(0).toUpperCase() + word.slice(1) : word))\n .join(' ')\n );\n}\n\nfunction matchPath(pattern: RegExp, pathname: string): RegExpMatchArray | null {\n return pathname.match(pattern);\n}\n\n/**\n * Maps the current BigConsole route to `{ entityType, entityId }`. The BigConsole\n * MFE mounts under the `/bigconsole` base path in the host shell, so detail\n * routes are matched as `/bigconsole/<collection>/:id`.\n *\n * More specific routes (e.g. dashboard-scoped parsers, builder/edit sub-routes)\n * are matched before their parent so the most relevant entity wins.\n */\nexport function resolveBigConsoleConversationContext(pathname: string, search: string): ConversationContextShape {\n const params = new URLSearchParams(search);\n\n const directMatches: Array<[RegExp, string]> = [\n // Dashboard-scoped parser detail (more specific than the bare parser route).\n [/^\\/bigconsole\\/dashboards\\/[^/]+\\/parsers\\/([^/]+)(?:\\/edit)?$/, 'BIGCONSOLE_PARSER'],\n // Dashboard detail + builder.\n [/^\\/bigconsole\\/dashboards\\/([^/]+)(?:\\/builder)?$/, 'BIGCONSOLE_DASHBOARD'],\n // Data sinks.\n [/^\\/bigconsole\\/datasinks\\/([^/]+)(?:\\/edit)?$/, 'BIGCONSOLE_DATASINK'],\n // Parsers.\n [/^\\/bigconsole\\/parsers\\/([^/]+)(?:\\/edit)?$/, 'BIGCONSOLE_PARSER'],\n // Pipelines.\n [/^\\/bigconsole\\/pipelines\\/([^/]+)(?:\\/edit)?$/, 'BIGCONSOLE_PIPELINE'],\n // Workflows (BigConsole-native list/view; the SDK builder uses /workflow/...).\n [/^\\/bigconsole\\/workflows\\/([^/]+)(?:\\/edit)?$/, 'BIGCONSOLE_WORKFLOW'],\n ];\n\n for (const [pattern, entityType] of directMatches) {\n const match = matchPath(pattern, pathname);\n if (match && match[1] && !isReservedSegment(match[1])) {\n return {\n entityType,\n entityId: match[1],\n metadata: {\n routePattern: pattern.source,\n },\n };\n }\n }\n\n const activeTab =\n params.get('tab') ??\n (() => {\n const segments = pathname.split('/').filter(Boolean);\n return segments.length > 2 ? segments[segments.length - 1] : undefined;\n })();\n\n return {\n activeTab: activeTab ?? undefined,\n metadata: {\n ...(activeTab ? { activeTab } : {}),\n },\n };\n}\n\n/**\n * Static segments that share a route shape with `:id` detail routes\n * (e.g. `/bigconsole/datasinks/new`, `/bigconsole/parsers/new`). These are\n * pages, not entities, so they must not be reported as an `entityId`.\n */\nfunction isReservedSegment(segment: string): boolean {\n return segment === 'new' || segment === 'builder' || segment === 'list';\n}\n"],"mappings":";;AAiDA,SAAgB,IAAiC;CAC/C,IAAM,IAAM,OAAO,SAAS,MAEtB,IADe,OAAO,SAAS,SAAS,MAAM,IAAI,CAAC,OAAO,QAAQ,CACvC,MAAM,GAAG,EAAE,CAAC,KAAK,IAAI,IAAI,QACpD,IAAS,EAAqC,OAAO,SAAS,UAAU,OAAO,SAAS,OAAO;AAErG,QAAO;EACL,YAAY;EACZ;EACA,YAAY,EAAO;EACnB,UAAU,EAAO;EACjB,UAAU;EACV,SAA6C;EAC7C,cAAc,GAAG,OAAO,WAAW,GAAG,OAAO;EAC7C,WAAW,UAAU;EACrB,4BAAW,IAAI,MAAM,EAAC,aAAa;EACpC;;AAIH,IAAM,IAAa,QAUb,IAAgD;CACpD,MAAM;CACN,YAAY;CACZ,WAAW;CACX,SAAS;CACT,WAAW;CACX,WAAW;CACZ;AAcD,SAAgB,IAA8B;CAC5C,IAAM,EAAE,mBAAgB,GAAmB,EACrC,IAAW,EAAY,MAAM,IAAI,CAAC,OAAO,QAAQ,EACjD,KAAS,EAAS,OAAA,eAA4B,EAAS,KAAK,EAAS,OAAO;AAElF,QADI,CAAC,KAAS,MAAU,SAAe,IAErC,EAAY,MACZ,EACG,MAAM,IAAI,CACV,KAAK,MAAU,KAAO,EAAK,OAAO,EAAE,CAAC,aAAa,GAAG,EAAK,MAAM,EAAE,CAAS,CAC3E,KAAK,IAAI;;AAIhB,SAAS,EAAU,GAAiB,GAA2C;AAC7E,QAAO,EAAS,MAAM,EAAQ;;AAWhC,SAAgB,EAAqC,GAAkB,GAA0C;CAC/G,IAAM,IAAS,IAAI,gBAAgB,EAAO;AAiB1C,MAAK,IAAM,CAAC,GAAS,MAf0B;EAE7C,CAAC,kEAAkE,oBAAoB;EAEvF,CAAC,qDAAqD,uBAAuB;EAE7E,CAAC,iDAAiD,sBAAsB;EAExE,CAAC,+CAA+C,oBAAoB;EAEpE,CAAC,iDAAiD,sBAAsB;EAExE,CAAC,iDAAiD,sBAAsB;EACzE,EAEkD;EACjD,IAAM,IAAQ,EAAU,GAAS,EAAS;AAC1C,MAAI,KAAS,EAAM,MAAM,CAAC,EAAkB,EAAM,GAAG,CACnD,QAAO;GACL;GACA,UAAU,EAAM;GAChB,UAAU,EACR,cAAc,EAAQ,QACvB;GACF;;CAIL,IAAM,IACJ,EAAO,IAAI,MAAM,WACV;EACL,IAAM,IAAW,EAAS,MAAM,IAAI,CAAC,OAAO,QAAQ;AACpD,SAAO,EAAS,SAAS,IAAI,EAAS,EAAS,SAAS,KAAK,KAAA;KAC3D;AAEN,QAAO;EACL,WAAW,KAAa,KAAA;EACxB,UAAU,EACR,GAAI,IAAY,EAAE,cAAW,GAAG,EAAE,EACnC;EACF;;AAQH,SAAS,EAAkB,GAA0B;AACnD,QAAO,MAAY,SAAS,MAAY,aAAa,MAAY"}
@@ -0,0 +1,60 @@
1
+ import { PREVIEW_STEPS as e, useAssistantRunStore as t } from "./assistantRunStore.js";
2
+ import { PreviewMeta as n, STEP_LABEL as r, formatAgo as i, previewLabels as a } from "../components/preview/previewShared.js";
3
+ import { previewRunAction as o } from "./previewRunAction.js";
4
+ import { useMemo as s } from "react";
5
+ import { useI18n as c } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
6
+ import { jsx as l } from "react/jsx-runtime";
7
+ import { AssistantEntityPreview as u, useAssistantStore as d } from "@burdenoff/fe-libs/shared/assistant";
8
+ //#region src/bigconsole/assistant/previewController.tsx
9
+ function f() {
10
+ let { t: o } = c(), s = t((e) => e.isRunning), d = t((e) => e.steps), f = t((e) => e.caption), p = t((e) => e.error), m = t((e) => e.updatedAt), h = e.map((e) => ({
11
+ id: e,
12
+ label: o(r[e].key, r[e].fallback),
13
+ status: d[e]
14
+ }));
15
+ return /* @__PURE__ */ l("div", {
16
+ "data-testid": "ai-preview-panel",
17
+ "data-variant": "assistant",
18
+ className: "min-w-0 p-3",
19
+ children: /* @__PURE__ */ l(u, {
20
+ title: o("bigconsole.widget.aiPreviewMini.title", "AI Preview"),
21
+ steps: h,
22
+ caption: s ? f || o("bigconsole.widget.aiPreviewMini.working", "Working…") : o("bigconsole.widget.aiPreviewMini.done", "Done"),
23
+ error: p,
24
+ meta: /* @__PURE__ */ l(n, {
25
+ state: s ? "running" : p ? "stopped" : "idle",
26
+ stateLabel: s ? o("bigconsole.widget.aiPreviewMini.live", "Live") : p ? o("bigconsole.widget.aiPreviewMini.stopped", "Stopped") : null,
27
+ updatedLabel: o("bigconsole.widget.aiPreviewMini.updated", "updated"),
28
+ updatedAt: m,
29
+ ago: (e) => i(e, o)
30
+ }),
31
+ labels: a(o)
32
+ })
33
+ });
34
+ }
35
+ var p = /* @__PURE__ */ new Set();
36
+ function m(e) {
37
+ let n = d.getState(), r = n.conversations.find((t) => t.id === e.run.conversationId)?.mode ?? n.mode;
38
+ if (o(e.kind, e.run.runId, r, p) === "ignore") return;
39
+ let i = t.getState();
40
+ if (e.kind === "start") {
41
+ p.add(e.run.runId), i.startRun("");
42
+ return;
43
+ }
44
+ if (e.kind === "snapshot") {
45
+ i.applyProgress(e.content);
46
+ return;
47
+ }
48
+ p.delete(e.run.runId), i.finishRun(e.outcome === "error" ? e.error ?? "The build stopped." : null);
49
+ }
50
+ function h() {
51
+ return s(() => ({
52
+ label: "Build preview",
53
+ render: () => /* @__PURE__ */ l(f, {}),
54
+ onEvent: m
55
+ }), []);
56
+ }
57
+ //#endregion
58
+ export { h as useBigConsoleAssistantPreview };
59
+
60
+ //# sourceMappingURL=previewController.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"previewController.js","names":[],"sources":["../../../src/bigconsole/assistant/previewController.tsx"],"sourcesContent":["/**\n * BigConsole's preview capability for the shared assistant panel (BC3).\n *\n * ★★★ THIS IS WHAT REPLACES THE TRANSPORT AS THE RUN STORE'S DRIVER.\n *\n * Today `createSandboxAssistantTransport` calls `startRun` / `applyProgress` /\n * `finishRun` itself, because it owns the turn. The shared panel consumes NO\n * transport at all — `AssistantChatArea` calls fe-libs' own `assistant/api`\n * directly — so once BC4 mounts the panel there is nothing left to drive the\n * store, and the live build preview would simply stop moving.\n *\n * fe-libs' preview capability is that missing wire: the bridge announces\n * `start` / `snapshot` / `end` for the run in flight, and this controller\n * forwards them to the same three store actions the transport used to call. The\n * store, both preview panels, and everything downstream are untouched.\n *\n * ★★ ABSENCE IS THE OFF-SWITCH. A product that supplies no controller gets no\n * toggle and no overlay; there is deliberately no second `enablePreview` flag to\n * disagree with the controller's presence. So this hook always returns a\n * controller — gating belongs at the mount site (BC4), not here.\n */\nimport { useMemo } from 'react';\nimport {\n AssistantEntityPreview,\n useAssistantStore,\n type AssistantPreviewController,\n type AssistantPreviewEvent,\n type AssistantPreviewStep,\n} from '@burdenoff/fe-libs/shared/assistant';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\nimport { PreviewMeta, STEP_LABEL, formatAgo, previewLabels } from '../components/preview/previewShared';\nimport { PREVIEW_STEPS, useAssistantRunStore } from './assistantRunStore';\nimport { previewRunAction, type PreviewRunAction } from './previewRunAction';\n\n/**\n * The surface fe-libs renders inside its overlay.\n *\n * ★★ A COMPONENT, returned as JSX from `render()` — never hooks called inside\n * `render()` itself. fe-libs invokes `render()` from a single `PreviewSurface`\n * component, so hooks called there would belong to THAT fiber and any change in\n * their layout becomes \"rendered more hooks than during the previous render\".\n * Returning an element gives this surface its own fiber, which is both safer and\n * what lets it subscribe to the store independently.\n *\n * ★ Store-only by construction: no Apollo, no BigConsole context, no data\n * fetching — the same discipline that lets `AiPreviewMiniPanel` mount above the\n * router. The rich per-entity tabs stay in `AiPreviewPanel`, which lives inside\n * `BigConsoleRoot` where the data layer actually exists.\n *\n * ★ It renders the shared `AssistantEntityPreview` with the SAME mapping the\n * mini panel uses, and reuses `STEP_LABEL` / `previewLabels` / `PreviewMeta`\n * rather than restating them. Those helpers were extracted precisely because the\n * two existing panels kept drifting apart; a third copy here would reintroduce\n * the bug they were created to kill.\n */\nfunction BigConsoleAssistantPreviewSurface() {\n const { t } = useI18n();\n const isRunning = useAssistantRunStore((state) => state.isRunning);\n const steps = useAssistantRunStore((state) => state.steps);\n const caption = useAssistantRunStore((state) => state.caption);\n const error = useAssistantRunStore((state) => state.error);\n const updatedAt = useAssistantRunStore((state) => state.updatedAt);\n\n const railSteps: AssistantPreviewStep[] = PREVIEW_STEPS.map((key) => ({\n id: key,\n label: t(STEP_LABEL[key].key, STEP_LABEL[key].fallback),\n status: steps[key],\n }));\n\n return (\n <div data-testid=\"ai-preview-panel\" data-variant=\"assistant\" className=\"min-w-0 p-3\">\n <AssistantEntityPreview\n title={t('bigconsole.widget.aiPreviewMini.title', 'AI Preview')}\n steps={railSteps}\n // ★★ TWO branches, not three. The shared surface renders\n // `{error ? '' : caption}` — it BLANKS the caption whenever `error` is\n // set — so a failure branch here would compute a string that is then\n // discarded. The failure is named in `meta` instead, which\n // `PreviewHeader` renders without consulting `error`.\n caption={\n isRunning\n ? caption || t('bigconsole.widget.aiPreviewMini.working', 'Working…')\n : t('bigconsole.widget.aiPreviewMini.done', 'Done')\n }\n error={error}\n meta={\n <PreviewMeta\n state={isRunning ? 'running' : error ? 'stopped' : 'idle'}\n stateLabel={\n isRunning\n ? t('bigconsole.widget.aiPreviewMini.live', 'Live')\n : error\n ? t('bigconsole.widget.aiPreviewMini.stopped', 'Stopped')\n : null\n }\n updatedLabel={t('bigconsole.widget.aiPreviewMini.updated', 'updated')}\n updatedAt={updatedAt}\n ago={(seconds) => formatAgo(seconds, t)}\n />\n }\n labels={previewLabels(t)}\n />\n </div>\n );\n}\n\n/**\n * ★ Re-exported so consumers keep ONE import path, while the RULE itself lives\n * in a module that loads without a DOM. Importing THIS file pulls fe-libs'\n * assistant barrel, which touches `document` at load time — so the decision had\n * to move out before it could be tested. See `previewRunAction.ts`.\n *\n * ★ Local bindings, not `export … from`: a bare re-export creates no local name,\n * so `applyPreviewEvent` below would be calling something this module does not\n * actually have.\n */\nexport { previewRunAction };\nexport type { PreviewRunAction };\n\n/**\n * The runs this controller has adopted — Build runs only.\n *\n * ★ Module-level, like the handler itself: it must outlive the controller memo\n * and every re-render, and nothing renders it.\n */\nconst adoptedRuns = new Set<string>();\n\n/**\n * Forward one bridge event to the run store.\n *\n * ★ Module-level and imperative (`getState()`), NOT a hook: the bridge calls\n * this from an effect, and reading the store through a selector here would make\n * the controller depend on state it never renders.\n */\nfunction applyPreviewEvent(event: AssistantPreviewEvent): void {\n /**\n * ★ The mode of THIS RUN's conversation, never the panel's. The panel's `mode`\n * is whatever the user last selected, which need not be the mode the run in\n * flight was started in. Falls back to the panel mode only when the\n * conversation is gone.\n */\n const assistant = useAssistantStore.getState();\n const modeOfRun = assistant.conversations.find((c) => c.id === event.run.conversationId)?.mode ?? assistant.mode;\n\n if (previewRunAction(event.kind, event.run.runId, modeOfRun, adoptedRuns) === 'ignore') {\n return;\n }\n\n const store = useAssistantRunStore.getState();\n\n if (event.kind === 'start') {\n adoptedRuns.add(event.run.runId);\n /**\n * ★★ THE PROMPT IS NOT AVAILABLE, and that is a real gap rather than an\n * oversight. `AssistantPreviewRun` carries `runId`, `conversationId`,\n * `messageId`, `workspaceId` and `actorId` — no turn text — so the store's\n * `prompt` (which only feeds an informational line) is left empty. The\n * transport could pass it because it owned the turn; the bridge cannot,\n * because it observes one. Everything the preview actually draws comes from\n * `applyProgress`, so nothing visible depends on this.\n */\n store.startRun('');\n return;\n }\n\n if (event.kind === 'snapshot') {\n /**\n * ★ `content` is the PROSE with the tool-activity lines already removed by\n * fe-libs. That is what this store wants: it harvests entity links, raw\n * mutation ids and the newest `⏳ …` caption out of the agent's narration,\n * all of which live in the prose. The structured `event.steps` describe TOOL\n * calls, which are a different rail from BigConsole's four build steps.\n */\n store.applyProgress(event.content);\n return;\n }\n\n /**\n * ★★ `abandoned` is neither success nor failure — it is a run the bridge can\n * no longer follow (cancelled, superseded, or navigated away from mid-turn),\n * and the turn may well still be executing. Passing its text as an error would\n * paint a red \"Build stopped\" banner over a build that is very likely still\n * running; passing nothing at all would leave the spinner going forever. So it\n * ends the run WITHOUT an error, which stops the spinner and leaves every step\n * already marked `done` exactly as it was.\n */\n /**\n * ★ Released here, and only here. Run ids are `conversationId:messageId`, so\n * they are never reused and this is a leak rather than a correctness bug — but\n * an unbounded set that grows for the life of the tab is still a leak, and\n * `end` is the one event guaranteed to arrive for every adopted run\n * (`abandoned` included — that outcome exists precisely so a run the bridge\n * can no longer follow still terminates).\n */\n adoptedRuns.delete(event.run.runId);\n store.finishRun(event.outcome === 'error' ? (event.error ?? 'The build stopped.') : null);\n}\n\n/**\n * BigConsole's preview controller, stable across renders.\n *\n * ★★★ THE MEMO IS LOAD-BEARING, not tidiness. fe-libs' bridge deliberately does\n * NOT reset its run state when the controller identity changes — precisely so\n * that the natural `preview={{ render, onEvent }}` object literal does not\n * re-announce `start` on every poll tick. The cost of that choice is that\n * REPLACING a controller mid-run leaves the newcomer without a `start` for the\n * run already in flight. An empty dependency list is what guarantees one\n * controller for the life of the mount.\n *\n * ★ `label` is the overlay's toggle text. Plain English here, like FluidGrids':\n * the seam's user-facing strings are translated in BC5 along with the rest of\n * the bundle, and a half-translated toggle beside untranslated mode labels is\n * worse than a consistently English one.\n */\nexport function useBigConsoleAssistantPreview(): AssistantPreviewController {\n return useMemo<AssistantPreviewController>(\n () => ({\n label: 'Build preview',\n render: () => <BigConsoleAssistantPreviewSurface />,\n onEvent: applyPreviewEvent,\n }),\n []\n );\n}\n"],"mappings":";;;;;;;;AAwDA,SAAS,IAAoC;CAC3C,IAAM,EAAE,SAAM,GAAS,EACjB,IAAY,GAAsB,MAAU,EAAM,UAAU,EAC5D,IAAQ,GAAsB,MAAU,EAAM,MAAM,EACpD,IAAU,GAAsB,MAAU,EAAM,QAAQ,EACxD,IAAQ,GAAsB,MAAU,EAAM,MAAM,EACpD,IAAY,GAAsB,MAAU,EAAM,UAAU,EAE5D,IAAoC,EAAc,KAAK,OAAS;EACpE,IAAI;EACJ,OAAO,EAAE,EAAW,GAAK,KAAK,EAAW,GAAK,SAAS;EACvD,QAAQ,EAAM;EACf,EAAE;AAEH,QACE,kBAAC,OAAD;EAAK,eAAY;EAAmB,gBAAa;EAAY,WAAU;YACrE,kBAAC,GAAD;GACE,OAAO,EAAE,yCAAyC,aAAa;GAC/D,OAAO;GAMP,SACE,IACI,KAAW,EAAE,2CAA2C,WAAW,GACnE,EAAE,wCAAwC,OAAO;GAEhD;GACP,MACE,kBAAC,GAAD;IACE,OAAO,IAAY,YAAY,IAAQ,YAAY;IACnD,YACE,IACI,EAAE,wCAAwC,OAAO,GACjD,IACE,EAAE,2CAA2C,UAAU,GACvD;IAER,cAAc,EAAE,2CAA2C,UAAU;IAC1D;IACX,MAAM,MAAY,EAAU,GAAS,EAAE;IACvC,CAAA;GAEJ,QAAQ,EAAc,EAAE;GACxB,CAAA;EACE,CAAA;;AAuBV,IAAM,oBAAc,IAAI,KAAa;AASrC,SAAS,EAAkB,GAAoC;CAO7D,IAAM,IAAY,EAAkB,UAAU,EACxC,IAAY,EAAU,cAAc,MAAM,MAAM,EAAE,OAAO,EAAM,IAAI,eAAe,EAAE,QAAQ,EAAU;AAE5G,KAAI,EAAiB,EAAM,MAAM,EAAM,IAAI,OAAO,GAAW,EAAY,KAAK,SAC5E;CAGF,IAAM,IAAQ,EAAqB,UAAU;AAE7C,KAAI,EAAM,SAAS,SAAS;AAW1B,EAVA,EAAY,IAAI,EAAM,IAAI,MAAM,EAUhC,EAAM,SAAS,GAAG;AAClB;;AAGF,KAAI,EAAM,SAAS,YAAY;AAQ7B,IAAM,cAAc,EAAM,QAAQ;AAClC;;AAqBF,CADA,EAAY,OAAO,EAAM,IAAI,MAAM,EACnC,EAAM,UAAU,EAAM,YAAY,UAAW,EAAM,SAAS,uBAAwB,KAAK;;AAmB3F,SAAgB,IAA4D;AAC1E,QAAO,SACE;EACL,OAAO;EACP,cAAc,kBAAC,GAAD,EAAqC,CAAA;EACnD,SAAS;EACV,GACD,EAAE,CACH"}
@@ -0,0 +1,9 @@
1
+ import { BUILD as e } from "./modes.js";
2
+ //#region src/bigconsole/assistant/previewRunAction.ts
3
+ function t(t, n, r, i) {
4
+ return t === "start" ? r === e.id ? "start" : "ignore" : i.has(n) ? t === "snapshot" ? "progress" : "end" : "ignore";
5
+ }
6
+ //#endregion
7
+ export { t as previewRunAction };
8
+
9
+ //# sourceMappingURL=previewRunAction.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"previewRunAction.js","names":[],"sources":["../../../src/bigconsole/assistant/previewRunAction.ts"],"sourcesContent":["/**\n * Which bridge events may drive BigConsole's BUILD preview.\n *\n * ★★★ A MODULE OF ITS OWN, AND THAT IS NOT ORGANISATION — IT IS WHAT MAKES THIS\n * TESTABLE AT ALL.\n *\n * This rule started life inside `previewController.tsx`, next to the React\n * surface, described as \"pure, and separate from the wiring\". It was not\n * separate: that file imports `useAssistantStore` from fe-libs' assistant\n * BARREL, and the barrel pulls modules that touch `document` at load time. So\n * importing it from a `bun test` (no DOM) threw `ReferenceError: document is not\n * defined` before a single assertion ran.\n *\n * Here the only runtime import is `./modes`, whose own fe-libs import is\n * type-only and therefore erased. That keeps this file loadable anywhere — which\n * is the whole point of splitting a decision out from its wiring.\n */\nimport type { AssistantPreviewEvent } from '@burdenoff/fe-libs/shared/assistant';\n\nimport { BUILD } from './modes';\n\n/** What, if anything, a bridge event should do to the run store. */\nexport type PreviewRunAction = 'start' | 'progress' | 'end' | 'ignore';\n\n/**\n * ★★★ ASK TURNS MUST NOT DRIVE THE BUILD PREVIEW (Codex review, PR #154).\n *\n * BC3 is what introduces a second mode. Before it, `api-calls` was the ONLY mode\n * BigConsole offered, so forwarding every event unconditionally was correct.\n * Now the controller is registered panel-wide while Ask is read-only, and an\n * unfiltered `start` would mark the build store running, open the preview and\n * light the DataSink step for an ordinary question. Worse, `applyProgress`\n * harvests entity links out of PROSE — so an Ask answer that merely mentions\n * `/bigconsole/dashboards/abc` would be recorded as an entity a build had just\n * created. Every member who only has Ask would see that, on every question.\n *\n * ★★ ADOPTION IS KEYED ON THE RUN, and that is the subtle half. Re-checking \"is\n * the mode Build right now?\" on every event looks equivalent and is not: a run\n * that STARTS in Build whose conversation is switched to Ask mid-run would have\n * its `end` filtered out, leaving the store spinning on a build that had already\n * finished. The mode is consulted ONCE, at `start`; after that the run is\n * followed to its end — the same way fe-libs' own bridge keys runs.\n */\nexport function previewRunAction(\n kind: AssistantPreviewEvent['kind'],\n runId: string,\n modeOfRun: string,\n adoptedRuns: ReadonlySet<string>\n): PreviewRunAction {\n if (kind === 'start') return modeOfRun === BUILD.id ? 'start' : 'ignore';\n if (!adoptedRuns.has(runId)) return 'ignore';\n return kind === 'snapshot' ? 'progress' : 'end';\n}\n"],"mappings":";;AA2CA,SAAgB,EACd,GACA,GACA,GACA,GACkB;AAGlB,QAFI,MAAS,UAAgB,MAAc,EAAM,KAAK,UAAU,WAC3D,EAAY,IAAI,EAAM,GACpB,MAAS,aAAa,aAAa,QADN"}
@@ -0,0 +1,6 @@
1
+ //#region src/bigconsole/assistant/product.ts
2
+ var e = "bigconsole";
3
+ //#endregion
4
+ export { e as BIGCONSOLE_PRODUCT };
5
+
6
+ //# sourceMappingURL=product.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product.js","names":[],"sources":["../../../src/bigconsole/assistant/product.ts"],"sourcesContent":["/**\n * BigConsole's product id, in one place.\n *\n * ★★ This single string answers three different questions, and they must never\n * be allowed to drift apart:\n *\n * 1. `AI_ASSISTANT_PRODUCT` — the env the sandbox is created with, which\n * selects the agent's product guide.\n * 2. `metadata.assistantProduct` — the reuse stamp (BOFF-7312). A sandbox\n * stamped with another product's id is never reused for this one. That is\n * not bookkeeping: adopting another product's container inherits its\n * persona and docs corpus, which is the production incident pinned in\n * fe-libs' `sandboxProductIsolation.test.ts` — a HealthyBowl user greeted\n * by \"I am the AI assistant for the Burdenoff platform\".\n * 3. The i18n namespace prefix: every shared-panel string resolves as\n * `bigconsole.assistant.<relative key>`.\n *\n * A second literal spelled the same way would satisfy all three today and\n * silently stop doing so the moment one of them is edited.\n * `assistantSeamWiring.test.ts` asserts that no other production file in this\n * folder contains the bare quoted literal.\n *\n * ★ Deliberately a module with NO other imports: the id is read by the seam, by\n * the i18n namespace and by the sandbox stamp, and none of them should have to\n * load anything else to get it.\n */\nexport const BIGCONSOLE_PRODUCT = 'bigconsole';\n"],"mappings":";AA0BA,IAAa,IAAqB"}
@@ -0,0 +1,15 @@
1
+ import { useWorkspaceWriteAccess as e } from "@burdenoff/fe-libs/shared/assistant/useWorkspaceWriteAccess";
2
+ //#region src/bigconsole/assistant/useWorkspaceWriteAccess.ts
3
+ var t = [
4
+ "bigconsole-datasink",
5
+ "bigconsole-dashboard",
6
+ "bigconsole-parser",
7
+ "bigconsole-widget"
8
+ ];
9
+ function n(n, r) {
10
+ return e(n, r, t);
11
+ }
12
+ //#endregion
13
+ export { t as PROBE_RESOURCES, n as useWorkspaceWriteAccess };
14
+
15
+ //# sourceMappingURL=useWorkspaceWriteAccess.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useWorkspaceWriteAccess.js","names":[],"sources":["../../../src/bigconsole/assistant/useWorkspaceWriteAccess.ts"],"sourcesContent":["/**\n * BigConsole's write-access probe.\n *\n * ★ The mechanism lives in `fe-libs/shared/assistant`; the RESOURCE TYPES stay\n * here, because which resources prove \"this user can write in this workspace\"\n * is the product's answer. The shared hook takes them with no default, so\n * another product cannot accidentally probe BigConsole's dashboards — or the\n * other way round.\n */\nimport { useWorkspaceWriteAccess as useShared } from '@burdenoff/fe-libs/shared/assistant/useWorkspaceWriteAccess';\nexport type { WorkspaceWriteAccess } from '@burdenoff/fe-libs/shared/assistant/useWorkspaceWriteAccess';\n\n/**\n * The resources a Build turn actually writes.\n *\n * ★★ READ OUT OF THE BACKEND, NOT INVENTED. These are the exact `resource:`\n * values on `@rbac` directives in `wspace-bigconsole-svc`, and they are\n * HYPHENATED — `bigconsole-datasink`, not `bigconsole_datasink`.\n *\n * ★★★ Do NOT copy HealthyBowl's `healthybowl_crop` underscore convention.\n * VibeControls did exactly that, probed `vibecontrols_vibe` and friends, matched\n * no resource type that exists, and so silently default-denied — its composer\n * warning said nothing about the permissions a write turn actually needs, and\n * nothing failed loudly enough to notice.\n *\n * ★ Scoped to the four the assistant ISSUES, not all 17 the service defines.\n * The agent's build path is `createDataSink` → `createBigConsoleDashboard` →\n * `createParser` → `createBigConsoleWidget`; probing `bigconsole-webhook` or\n * `bigconsole-rls-policy`, which the assistant never calls, would make\n * \"granted\" mean less than it does. All four carry `action: \"create\"` @rbac\n * entries, which is the only action the shared hook asks about.\n *\n * ★ Advisory, never a gate: it warns in the composer BEFORE a write turn is\n * spent rather than after, and self-corrects once workspace RBAC rows are\n * seeded. Gating on it would remove Build from the product entirely — at\n * workspace scope in prod the honest answer today is \"denied\" for everyone, org\n * owners included, because no roles are provisioned there yet.\n */\nexport const PROBE_RESOURCES = [\n 'bigconsole-datasink',\n 'bigconsole-dashboard',\n 'bigconsole-parser',\n 'bigconsole-widget',\n] as const;\n\n/**\n * ★ `PROBE_RESOURCES` is a module-level const, never an inline array. The shared\n * hook's effect deps are `[actorId, workspaceId]` only — `probeResources` is not\n * among them — so an array literal created during render would silently pin the\n * first render's list forever.\n */\nexport function useWorkspaceWriteAccess(actorId: string | null, workspaceId: string | null) {\n return useShared(actorId, workspaceId, PROBE_RESOURCES);\n}\n"],"mappings":";;AAsCA,IAAa,IAAkB;CAC7B;CACA;CACA;CACA;CACD;AAQD,SAAgB,EAAwB,GAAwB,GAA4B;AAC1F,QAAO,EAAU,GAAS,GAAa,EAAgB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-bigconsole",
3
- "version": "2026.916.1",
3
+ "version": "2026.917.2",
4
4
  "description": "BigConsole - AI-powered analytics and dashboard platform",
5
5
  "type": "module",
6
6
  "files": [
@@ -34,7 +34,8 @@
34
34
  "format": "prettier --write \"src/**/*.{ts,tsx}\"",
35
35
  "format:check": "prettier --check \"src/**/*.{ts,tsx}\"",
36
36
  "type:check": "bash scripts/build-limits.sh tsc --noEmit",
37
- "sanity": "bash scripts/build-limits.sh --gate bash -c 'bun run codegen && bun run lint:sanity && bun run format:check && bun run type:check && bun run build && bun run build-storybook'",
37
+ "test": "bun test ./tests",
38
+ "sanity": "bash scripts/build-limits.sh --gate bash -c 'bun run codegen && bun run lint:sanity && bun run format:check && bun run type:check && bun run test && bun run build && bun run build-storybook'",
38
39
  "storybook": "storybook dev -p 6006",
39
40
  "build-storybook": "storybook build",
40
41
  "test:storybook": "test-storybook",
@@ -48,7 +49,7 @@
48
49
  },
49
50
  "peerDependencies": {
50
51
  "@apollo/client": "^4.0.11",
51
- "@burdenoff/fe-libs": "2026.912.4",
52
+ "@burdenoff/fe-libs": "2026.916.1",
52
53
  "@burdenoff/fluidgrids-fe-sdk": "2026.731.4",
53
54
  "@iconify/react": "^6.0.2",
54
55
  "@monaco-editor/react": "^4.7.0",
@@ -83,7 +84,7 @@
83
84
  "zustand": "^5.0.3"
84
85
  },
85
86
  "devDependencies": {
86
- "@burdenoff/fe-libs": "2026.912.4",
87
+ "@burdenoff/fe-libs": "2026.916.1",
87
88
  "@burdenoff/fluidgrids-fe-sdk": "2026.731.4",
88
89
  "@eslint/js": "^10.0.1",
89
90
  "@graphql-codegen/add": "^5.0.0",