@burdenoff/microfe-bigconsole 2026.912.4 → 2026.916.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"createSandboxAssistantTransport.js","names":[],"sources":["../../../src/bigconsole/assistant/createSandboxAssistantTransport.ts"],"sourcesContent":["/**\n * BigConsole adapter for the shared fe-libs AssistantWidget.\n *\n * The floating widget (fe-libs, Layer 1) is backend-agnostic — it calls an\n * injected `AssistantTransport`. This hook builds a transport that drives the\n * sandbox AI assistant (combined `assistant` mode = docs Q&A + api-calls): it\n * provisions/reuses a sandbox, opens a session, dispatches the prompt async,\n * then polls for the streamed answer. The agent introspects the GraphQL schema\n * and performs API calls on the user's behalf using their workspace token,\n * contextual to the current screen (via `gatherPageContext`).\n *\n * This is a faithful port of microfe-vibecontrols'\n * `services/createSandboxAssistantTransport.ts`; the only product-specific\n * difference lives in `assistantApi.createAssistantSandbox`\n * (`AI_ASSISTANT_PRODUCT=bigconsole`).\n */\n\nimport { useCallback, useMemo, useRef } from 'react';\nimport { useAuthToken } from '@burdenoff/fe-libs/shared/providers/shell';\nimport {\n createAssistantSandbox,\n createAssistantSession,\n extendAssistantSandboxTTL,\n findExistingSandbox,\n getAssistantMessages,\n sendAssistantPromptAsync,\n waitForAssistantServiceReady,\n waitForSandboxReady,\n} from './assistantApi';\nimport { buildAttachmentBlock } from './attachmentExtract';\nimport { useAssistantRunStore } from './assistantRunStore';\nimport {\n type AssistantConversationSummary,\n type AssistantHistoryTurnMessage,\n deleteAssistantConversation,\n getAssistantConversationMessages,\n listAssistantConversations,\n saveAssistantTurn,\n} from './conversationHistoryApi';\nimport { gatherPageContext } from './pageContext';\nimport type { AssistantMode, AssistantRawMessage, AssistantRawMessagePart, AssistantSandboxAuthContext } from './types';\n\n/**\n * Locally-defined mirror of the fe-libs `AssistantTransport` contract.\n *\n * Intentionally NOT imported from `@burdenoff/fe-libs`: microfe's tsconfig maps\n * `@burdenoff/fe-libs/*` to fe-libs *source*, so vite-plugin-dts would rewrite a\n * cross-package type used in this hook's public signature to a broken\n * source-relative path in the emitted `.d.ts`. Structural typing makes this\n * shape assignable to fe-libs' `AssistantTransport` at the call site\n * (bigconsole-app's AppShell), which is where compatibility is enforced.\n */\ninterface AssistantSendArgs {\n prompt: string;\n /** Files attached via the widget's upload button (fe-libs carries the raw\n * File[]; we extract + fold a capped preview into the agent prompt here). */\n attachments?: File[];\n onProgress: (partialText: string) => void;\n signal: AbortSignal;\n}\n\n/** Mirror of fe-libs' `AssistantWidgetMessage` (see note above on why). */\ninterface AssistantHistoryMessage {\n id: string;\n role: 'user' | 'assistant';\n content: string;\n pending?: boolean;\n error?: boolean;\n}\n\n/** Mirror of fe-libs' `AssistantSessionSummary` (see note above on why). */\ninterface AssistantSessionSummaryLocal {\n id: string;\n title: string;\n updatedAt?: number;\n active?: boolean;\n}\n\nexport interface AssistantTransport {\n sendPrompt: (args: AssistantSendArgs) => Promise<{ text: string }>;\n loadHistory: () => Promise<AssistantHistoryMessage[]>;\n listSessions: () => Promise<AssistantSessionSummaryLocal[]>;\n newSession: () => Promise<void>;\n deleteSession: (sessionId: string) => Promise<void>;\n selectSession: (sessionId: string) => Promise<AssistantHistoryMessage[]>;\n}\n\n// BigConsole still boots the manually-tagged ACA image\n// `alpha-delegated-auth-v11` for the assistant sandbox. The historical\n// platform notes show that this image line reliably supports `api-calls`, while\n// the combined `assistant` mode depends on newer image contracts that are not\n// yet guaranteed on this tag. Use `api-calls` here so the assistant can execute\n// workspace GraphQL operations end-to-end right now. Once the underlying image\n// line is rebuilt and verified for combined mode, this can be switched back.\nconst MODE: AssistantMode = 'api-calls';\n// How long the UI will follow a single turn.\n//\n// This was 180s, which was SHORTER THAN THE WORK. A full \"create a school\n// attendance dashboard\" build — datasink → dashboard → parser → widget, each a\n// separate gateway call preceded by a model round-trip — measured 229s in prod.\n// So the agent finished, the dashboard genuinely existed, and the user was still\n// shown \"the assistant timed out\". That is worse than cosmetic: people retry and\n// end up with duplicate dashboards.\n//\n// 7 minutes covers the observed worst case with headroom. It costs nothing on\n// fast turns (we stop the moment the turn reports done), and the backend keeps\n// pace — the sandbox TTL is extended every TTL_EXTEND_INTERVAL_MS.\nconst STREAM_BUDGET_MS = 420_000;\nconst POLL_INTERVAL_MS = 1500;\nconst TTL_EXTEND_INTERVAL_MS = 30_000;\n// Raw agent messages per restore. The agent emits one message per internal\n// step, so a handful of turns is already dozens of messages — this is a cap on\n// the RAW fetch, not on the number of restored turns.\nconst HISTORY_MESSAGE_LIMIT = 200;\n/** Chats shown in History. Titles come from the store, so listing is one query. */\nconst SESSION_LIST_LIMIT = 25;\n/**\n * Cap on the transcript replayed into a resumed agent session. Long enough to\n * carry the ids and decisions that make \"that dashboard\" resolvable, short\n * enough not to crowd out the actual prompt.\n */\nconst REPLAY_MAX_CHARS = 6000;\n\nconst SANDBOX_ID_KEY = 'bc-assistant-sandbox-id';\nconst SESSION_ID_KEY = 'bc-assistant-session-id';\n/** The durable chat. This is the identity History lists. */\nconst CONVERSATION_ID_KEY = 'bc-assistant-conversation-id';\n\nfunction readStoredId(key: string): string | null {\n try {\n return window.sessionStorage.getItem(key);\n } catch {\n // sessionStorage unavailable (private mode) — degrade to a fresh session.\n return null;\n }\n}\n\nfunction writeStoredId(key: string, id: string | null): void {\n try {\n if (id) window.sessionStorage.setItem(key, id);\n else window.sessionStorage.removeItem(key);\n } catch {\n // Non-fatal: we simply lose cross-reload continuity.\n }\n}\n\n// ── Context helpers (mirror vibecontrols' resolution) ────────────────\n\nfunction getProfileContextValue(key: 'workspaceId' | 'organizationId'): string {\n try {\n const activeContextKey =\n key === 'workspaceId' ? 'burdenoff-active-context-workspace' : 'burdenoff-active-context-organization';\n const activeContextValue = localStorage.getItem(activeContextKey);\n if (activeContextValue) return activeContextValue;\n\n const activeProfileId = sessionStorage.getItem('bf-active-profile');\n if (!activeProfileId) return '';\n const raw = localStorage.getItem(`bf-p-${activeProfileId}-context`);\n if (!raw) return '';\n const context = JSON.parse(raw) as { workspaceId?: string; organizationId?: string };\n return context[key] ?? '';\n } catch {\n return '';\n }\n}\n\nfunction getWorkspaceId(fallback: string | null): string {\n const params = new URLSearchParams(window.location.search);\n return params.get('workspace') ?? getProfileContextValue('workspaceId') ?? fallback ?? '';\n}\n\nfunction getOrganizationId(): string {\n const params = new URLSearchParams(window.location.search);\n return params.get('org') ?? getProfileContextValue('organizationId');\n}\n\n// ── Message-progress helpers (pure; mirror vibecontrols) ─────────────\n\nfunction getMessageRole(message: AssistantRawMessage): string | undefined {\n // Check nested format first, then flat format\n return message.info?.role ?? message.role;\n}\n\nfunction getRawMessageCreatedAt(message: AssistantRawMessage): number {\n // Check nested format first (epoch ms), then flat format (ISO string or epoch ms)\n const nested = message.info?.time?.created;\n if (nested !== undefined) return nested;\n const flat = message.createdAt;\n if (flat === undefined) return 0;\n // If it's a string (ISO), parse it; otherwise treat as epoch ms\n if (typeof flat === 'string') {\n const parsed = Date.parse(flat);\n return isNaN(parsed) ? 0 : parsed;\n }\n return flat;\n}\n\nfunction getMessageCompleted(message: AssistantRawMessage): number | undefined {\n // Check nested format first, then flat format\n const nested = message.info?.time?.completed;\n if (nested !== undefined) return nested;\n const flat = message.completedAt;\n if (flat === undefined) return undefined;\n if (typeof flat === 'string') {\n const parsed = Date.parse(flat);\n return isNaN(parsed) ? undefined : parsed;\n }\n return flat;\n}\n\nfunction getAssistantText(message: AssistantRawMessage): string {\n // Check parts format first (nested), then flat content\n const parts = message.parts ?? [];\n const textFromParts = (parts ?? [])\n .filter((part) => part.type === 'text' && typeof part.text === 'string')\n .map((part) => part.text?.trim() ?? '')\n .filter(Boolean)\n .join('\\n');\n if (textFromParts) return textFromParts;\n // Fallback to flat content field\n return typeof message.content === 'string' ? message.content.trim() : '';\n}\n\n// Friendly, human-readable labels for the agent's tools so the progress line\n// reads like \"Searching the schema…\" instead of \"Running: bash\". The agent sets\n// a `description` on every bash call (e.g. \"Search for sales-related types in\n// workspace schema\") and a todo list on todowrite — surface those directly.\nconst TOOL_LABELS: Record<string, string> = {\n bash: 'Running a command',\n webfetch: 'Fetching a page',\n 'file.read': 'Reading files',\n 'file.write': 'Writing files',\n 'file.edit': 'Editing files',\n 'file.find.text': 'Searching the code',\n 'file.find.file': 'Looking for files',\n todowrite: 'Planning the steps',\n todoread: 'Reviewing the plan',\n};\n\nfunction asRecord(value: unknown): Record<string, unknown> | undefined {\n return typeof value === 'object' && value !== null ? (value as Record<string, unknown>) : undefined;\n}\n\n/** Best-effort human summary of what a single tool part is doing right now. */\nfunction describeToolPart(part: AssistantRawMessagePart): string {\n const tool = part.tool ?? 'tool';\n const input = asRecord(part.state?.input);\n\n // bash carries a plain-English `description` of the step — the best signal.\n const description = input?.description;\n if (typeof description === 'string' && description.trim()) return description.trim();\n\n // todowrite carries the todo list — surface the item being worked on.\n const todos = input?.todos;\n if (Array.isArray(todos)) {\n const active = todos.find((todo) => asRecord(todo)?.status === 'in_progress') ?? todos[0];\n const content = asRecord(active)?.content;\n if (typeof content === 'string' && content.trim()) return content.trim();\n }\n\n return TOOL_LABELS[tool] ?? `Running ${tool}`;\n}\n\nfunction getToolProgress(message: AssistantRawMessage): string[] {\n const parts = message.parts ?? [];\n return parts\n .filter((part) => part.type === 'tool' && part.tool)\n .map((part) => {\n const status = part.state?.status ?? 'running';\n const label = describeToolPart(part);\n if (status === 'completed') return `✓ ${label}`;\n if (status === 'failed') return `⚠ ${label}`;\n return `⏳ ${label}…`;\n });\n}\n\nfunction buildProgress(\n messages: AssistantRawMessage[],\n sinceMs: number,\n previousContent?: string,\n previousContentAtMs?: number\n): { content: string; done: boolean } {\n const relevant = messages\n .filter((message) => getMessageRole(message) === 'assistant' && getRawMessageCreatedAt(message) >= sinceMs)\n .sort((left, right) => getRawMessageCreatedAt(left) - getRawMessageCreatedAt(right));\n\n // Debug: log message filtering when no relevant messages found\n if (relevant.length === 0 && messages.length > 0) {\n console.log('[BigConsole-Assistant] buildProgress: no relevant messages', {\n totalMessages: messages.length,\n sinceMs,\n messageRoles: messages.map((m) => getMessageRole(m)),\n messageTimestamps: messages.map((m) => getRawMessageCreatedAt(m)),\n });\n }\n\n let content: string;\n let done: boolean;\n\n if (relevant.length === 0) {\n content = '';\n // No usable assistant content for THIS turn yet. Separate \"still thinking\"\n // from \"responded but unreadable\", so a slow reasoning model is never\n // mistaken for a dead runtime:\n //\n // - No assistant message exists AT ALL: the agent is still starting up, or\n // gpt-5.6-terra (a reasoning model) is still thinking before its first\n // token. Time-to-first-message routinely exceeds the old 15s window,\n // especially with a large system prompt — which declared the turn\n // done-and-empty and surfaced \"the assistant runtime did not respond\"\n // even though the backend was healthy. NEVER give up here; let the outer\n // turn budget (STREAM_BUDGET_MS) decide, exactly like the running-tool\n // guard in the branch below.\n //\n // - An assistant message exists but none maps to this turn (timestamp skew\n // / role mismatch): the turn may really be over but unreadable. Keep a\n // staleness fallback — but give reasoning models ample room (90s, not\n // 15s) so a slow first token is never read as a stalled turn.\n const anyAssistantMessage = messages.some((message) => getMessageRole(message) === 'assistant');\n const emptyForMs =\n previousContent === content && previousContentAtMs !== undefined ? Date.now() - previousContentAtMs : 0;\n // Skew case (a message exists but is unreadable): 90s is plenty.\n // Nothing-at-all case (slow reasoning first token): wait 150s before calling\n // it a genuine no-show — a safe upper bound for time-to-first-token that\n // still fails a truly dead runtime (unbooted sandbox / quota) in ~2.5 min\n // instead of the old 15s that tripped healthy reasoning turns.\n done = anyAssistantMessage ? emptyForMs >= 90_000 : emptyForMs >= 150_000;\n } else {\n const latest = relevant[relevant.length - 1]!;\n\n // ACCUMULATE the run, don't just show its last line.\n //\n // The agent emits a message per step, and it now narrates each one and prints\n // a link the moment a create lands (\"✅ Data sink created — [Open …](/…)\").\n // Showing only the newest message threw all of that away a second later: the\n // user saw a lone \"Thinking…\" and none of the links they were promised. Join\n // the whole run instead, so the panel reads as a live account of what is\n // happening and every link stays on screen.\n const narration = relevant.map(getAssistantText).filter(Boolean);\n const toolProgress = getToolProgress(latest);\n content = [...narration, ...toolProgress].join('\\n\\n');\n\n const officiallyDone = Boolean(getMessageCompleted(latest)) && content.length > 0;\n\n // A tool that is still running is proof the turn is alive, so never let the\n // staleness fallback fire underneath it. A single gateway call can sit on the\n // same \"⏳ Creating the data sink…\" line for far longer than the old 15s\n // window, which would have declared the turn finished mid-build.\n const hasRunningTool = (latest.parts ?? []).some(\n (part) => part.type === 'tool' && part.state?.status !== 'completed' && part.state?.status !== 'failed'\n );\n\n const staleDone =\n !officiallyDone &&\n !hasRunningTool &&\n previousContent === content &&\n previousContentAtMs !== undefined &&\n Date.now() - previousContentAtMs >= 45_000;\n\n done = officiallyDone || staleDone;\n }\n\n return { content, done };\n}\n\nfunction sanitize(response: string): string {\n return response\n .replace(/(Authorization\\s*:\\s*Bearer\\s+)[^\\s\\n]+/gi, '$1[REDACTED]')\n .replace(/(X-Workspace-Authorization\\s*:\\s*Bearer\\s+)[^\\s\\n]+/gi, '$1[REDACTED]')\n .replace(/\\beyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9._-]+\\.[A-Za-z0-9._-]+\\b/g, '[REDACTED_JWT]')\n .replace(/\\bsk-ant-[A-Za-z0-9-]+\\b/g, '[REDACTED_API_KEY]');\n}\n\nfunction isRecoverable(error: unknown): boolean {\n const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();\n if (\n message.includes('rate limit exceeded') ||\n message.includes('unauthorized') ||\n message.includes('k8s api error 401')\n ) {\n return false;\n }\n return [\n 'sandbox not found',\n 'sandbox is not running',\n 'sandbox service not available yet',\n 'sandbox failed',\n 'sandbox startup timed out',\n 'assistant service did not become healthy',\n 'proxy error: 404',\n 'proxy error: 502',\n 'proxy error: 503',\n 'unable to connect',\n 'image pull',\n 'container failed',\n 'bootstrap failed',\n // Transient network errors from the browser fetch — gateway CORS preflight\n // failures, mid-stream resets, and Cloudflare 524s all surface as\n // \"Failed to fetch\" via TypeError. They're worth retrying on a clean\n // runtime since the underlying sandbox state is unaffected.\n 'failed to fetch',\n 'cf-proxy timeout',\n // Subgraph returned 500 with a generic message — gateway returns this as\n // a GraphQL error rather than an HTTP error. The actual underlying cause\n // (e.g., transient Prisma timeout) is recoverable, but a fresh sandbox\n // may be needed.\n 'unexpected error',\n // An empty turn (\"the assistant runtime did not respond\") is most often a\n // stale/dead session or sandbox reference — a prompt to a session whose\n // sandbox has been recycled persists nothing rather than erroring. Treat it\n // as recoverable so the retry drops the warm refs and mints a fresh\n // sandbox+session; a genuinely down runtime simply empties again on attempt 2\n // and then surfaces to the user. (attempt-gated to a single retry upstream.)\n 'the assistant runtime did not respond',\n ].some((fragment) => message.includes(fragment));\n}\n\n/**\n * Returns a memoized `AssistantTransport` wired to the BigConsole sandbox\n * assistant agent. The sandbox + session are cached in refs so follow-up turns\n * reuse the warm environment for the lifetime of the host shell.\n */\nexport function useSandboxAssistantTransport(): AssistantTransport {\n const { getAccessToken, getWorkspaceToken, userId, workspaceId: ctxWorkspaceId } = useAuthToken();\n\n // Rehydrate the sandbox + session ids persisted by the previous page\n // lifecycle. These were being WRITTEN to sessionStorage but never read back,\n // so every reload silently opened a brand-new agent session: the chat looked\n // empty AND the agent genuinely lost the conversation (it could no longer\n // resolve \"that datasink\" / \"the dashboard you just made\").\n //\n // Restoring both together is what makes history real rather than cosmetic —\n // the transcript we replay into the UI is the same session the agent will\n // keep reasoning over. A stale/expired sandbox is not a problem: `sendPrompt`\n // already treats that as recoverable, drops the refs, and retries clean.\n const [initialSandboxId, initialSessionId, initialConversationId] = useMemo(\n () => [readStoredId(SANDBOX_ID_KEY), readStoredId(SESSION_ID_KEY), readStoredId(CONVERSATION_ID_KEY)] as const,\n []\n );\n\n const sandboxIdRef = useRef<string | null>(initialSandboxId);\n const sessionIdRef = useRef<string | null>(initialSessionId);\n const conversationIdRef = useRef<string | null>(initialConversationId);\n /**\n * Transcript to feed the agent on its next prompt.\n *\n * A chat can outlive the agent that produced it: the transcript is durable,\n * the sandbox session is not. Showing the messages while the agent silently\n * remembers nothing is the worst of both worlds — ask it to \"add a widget to\n * that dashboard\" and it has no idea what \"that\" is. So when a chat is\n * resumed after its agent is gone, replay the conversation into its first\n * prompt.\n */\n const replayRef = useRef<string | null>(null);\n\n const persistSandboxId = useCallback((id: string | null) => {\n sandboxIdRef.current = id;\n writeStoredId(SANDBOX_ID_KEY, id);\n }, []);\n\n const persistSessionId = useCallback((id: string | null) => {\n sessionIdRef.current = id;\n writeStoredId(SESSION_ID_KEY, id);\n }, []);\n\n const persistConversationId = useCallback((id: string | null) => {\n conversationIdRef.current = id;\n writeStoredId(CONVERSATION_ID_KEY, id);\n }, []);\n\n const ensureRuntime = useCallback(\n async (\n workspaceId: string,\n authContext: AssistantSandboxAuthContext\n ): Promise<{ sandboxId: string; sessionId: string }> => {\n let sandboxId = sandboxIdRef.current;\n console.log('[BigConsole-Assistant] ensureRuntime start', { sandboxId, workspaceId });\n if (!sandboxId) {\n if (!getAccessToken()) {\n throw new Error('The assistant requires an authenticated session. Please sign in again.');\n }\n console.log('[BigConsole-Assistant] findExistingSandbox called');\n sandboxId = await findExistingSandbox(workspaceId, MODE, authContext);\n console.log('[BigConsole-Assistant] findExistingSandbox result', { sandboxId });\n\n if (sandboxId) {\n try {\n console.log('[BigConsole-Assistant] waitForSandboxReady called (reused)', { sandboxId });\n await waitForSandboxReady(sandboxId, workspaceId, authContext);\n console.log('[BigConsole-Assistant] waitForSandboxReady done (reused)', { sandboxId });\n console.log('[BigConsole-Assistant] waitForAssistantServiceReady called (reused)', { sandboxId });\n await waitForAssistantServiceReady(sandboxId, workspaceId, MODE, authContext);\n console.log('[BigConsole-Assistant] waitForAssistantServiceReady done (reused)', { sandboxId });\n } catch (error) {\n console.warn('[BigConsole-Assistant] existing sandbox unusable, falling back to fresh sandbox', {\n sandboxId,\n error: error instanceof Error ? error.message : String(error),\n });\n sandboxId = null;\n }\n }\n\n if (!sandboxId) {\n console.log('[BigConsole-Assistant] createAssistantSandbox called');\n sandboxId = await createAssistantSandbox(MODE, workspaceId, authContext);\n console.log('[BigConsole-Assistant] createAssistantSandbox result', { sandboxId });\n console.log('[BigConsole-Assistant] waitForSandboxReady called (fresh)', { sandboxId });\n await waitForSandboxReady(sandboxId, workspaceId, authContext);\n console.log('[BigConsole-Assistant] waitForSandboxReady done (fresh)', { sandboxId });\n console.log('[BigConsole-Assistant] waitForAssistantServiceReady called (fresh)', { sandboxId });\n await waitForAssistantServiceReady(sandboxId, workspaceId, MODE, authContext);\n console.log('[BigConsole-Assistant] waitForAssistantServiceReady done (fresh)', { sandboxId });\n }\n\n persistSandboxId(sandboxId);\n }\n\n let sessionId = sessionIdRef.current;\n console.log('[BigConsole-Assistant] session check', { sessionId, sandboxId });\n if (!sessionId) {\n console.log('[BigConsole-Assistant] createAssistantSession called', { sandboxId, workspaceId });\n const result = await createAssistantSession(sandboxId, workspaceId, MODE, authContext);\n sessionId = result.sessionId;\n persistSessionId(sessionId);\n }\n\n return { sandboxId, sessionId };\n },\n [getAccessToken]\n );\n\n const sendPrompt = useCallback(\n async ({\n prompt,\n attachments,\n onProgress,\n signal,\n }: AssistantSendArgs): Promise<{\n text: string;\n }> => {\n const workspaceId = getWorkspaceId(ctxWorkspaceId);\n console.log('[BigConsole-Assistant] sendPrompt called', {\n promptLength: prompt.length,\n workspaceId,\n hasCtxWorkspaceId: !!ctxWorkspaceId,\n authContextKeys: {\n hasAccessToken: !!getAccessToken(),\n hasWorkspaceToken: !!getWorkspaceToken(),\n hasUserId: !!userId,\n },\n locationSearch: window.location.search,\n });\n if (!workspaceId) {\n throw new Error('The assistant needs an active workspace. Open a workspace and try again.');\n }\n const authContext: AssistantSandboxAuthContext = {\n accessToken: getAccessToken(),\n workspaceToken: getWorkspaceToken(),\n userId,\n organizationId: getOrganizationId(),\n };\n console.log('[BigConsole-Assistant] authContext prepared', {\n hasAccessToken: !!authContext.accessToken,\n hasWorkspaceToken: !!authContext.workspaceToken,\n hasUserId: !!authContext.userId,\n hasOrgId: !!authContext.organizationId,\n });\n\n // Drive the live preview panel. The agent has no event stream, so the\n // narration IS the signal: the store parses it into a DataSink → Dashboard\n // → Parser → Widget rail. Fed here rather than in the widget because the\n // host owns this transport, so no fe-libs change is needed.\n const runStore = useAssistantRunStore.getState();\n runStore.startRun(prompt);\n const reportProgress = (partial: string): void => {\n onProgress(partial);\n useAssistantRunStore.getState().applyProgress(partial);\n };\n\n const run = async (attempt: 1 | 2): Promise<{ text: string }> => {\n try {\n console.log('[BigConsole-Assistant] run attempt', attempt);\n const { sandboxId, sessionId } = await ensureRuntime(workspaceId, authContext);\n console.log('[BigConsole-Assistant] ensureRuntime resolved', { sandboxId, sessionId });\n\n // Resuming a chat whose agent session is gone: hand the agent the\n // earlier transcript once, on the first prompt of the resumed chat, so\n // it answers with that context instead of from a blank slate. Consumed\n // on success — never replayed twice into the same session.\n const replay = replayRef.current;\n // Extract any uploaded files (JSON/CSV/Excel/PDF) into a capped text\n // block and fold it into the prompt the agent sees, so it can design a\n // DataSink straight from the pasted rows. The user-facing `prompt`\n // (preview narration, logs) stays clean.\n const attachmentBlock = attachments && attachments.length > 0 ? await buildAttachmentBlock(attachments) : '';\n const promptWithData = attachmentBlock ? `${prompt}\\n\\n${attachmentBlock}` : prompt;\n const agentPrompt = replay ? `${replay}\\n\\n---\\n\\n${promptWithData}` : promptWithData;\n\n const startedAt = Date.now();\n console.log('[BigConsole-Assistant] calling sendAssistantPromptAsync', {\n sandboxId,\n workspaceId,\n sessionId,\n promptLength: agentPrompt.length,\n replayed: Boolean(replay),\n });\n await sendAssistantPromptAsync(\n sandboxId,\n workspaceId,\n sessionId,\n agentPrompt,\n MODE,\n gatherPageContext(),\n authContext\n );\n\n const timeoutAt = Date.now() + STREAM_BUDGET_MS;\n let lastTtlExtensionAt = Date.now();\n let lastContent = '';\n let lastContentChangeAt = Date.now();\n let progress = buildProgress([], startedAt);\n\n while (Date.now() < timeoutAt) {\n if (signal.aborted) throw new Error('Cancelled');\n\n const messages = await getAssistantMessages(sandboxId, workspaceId, sessionId, authContext, 50);\n console.log('[BigConsole-Assistant] poll', {\n elapsedMs: Date.now() - startedAt,\n messageCount: messages.length,\n firstFewRoles: messages.slice(0, 3).map((m) => getMessageRole(m)),\n firstFewTimestamps: messages.slice(0, 3).map((m) => getRawMessageCreatedAt(m)),\n });\n progress = buildProgress(messages, startedAt, lastContent, lastContentChangeAt);\n console.log('[BigConsole-Assistant] progress', {\n contentPreview: progress.content.slice(0, 100),\n done: progress.done,\n lastContentChangeAt: Date.now() - lastContentChangeAt,\n });\n if (progress.content !== lastContent) {\n lastContent = progress.content;\n lastContentChangeAt = Date.now();\n }\n reportProgress(sanitize(progress.content));\n if (progress.done) {\n console.log('[BigConsole-Assistant] progress.done=true, breaking poll loop');\n break;\n }\n\n if (Date.now() - lastTtlExtensionAt > TTL_EXTEND_INTERVAL_MS) {\n await extendAssistantSandboxTTL(sandboxId, workspaceId, 600, authContext).catch(() => undefined);\n lastTtlExtensionAt = Date.now();\n }\n\n await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));\n }\n\n if (!progress.done) {\n // Do NOT say \"please try again\". We stopped watching; the agent did\n // not stop working, and anything it already created is real. Telling\n // people to retry is how you get duplicate dashboards.\n throw new Error(\n 'I stopped waiting for a reply, but I may still be working — anything I already created will be there. Check your dashboards and data sinks before asking again, so you do not end up with duplicates.'\n );\n }\n\n const reply = sanitize(progress.content);\n\n // An empty reply is a FAILED turn, not a successful one.\n //\n // `buildProgress` gives up and reports `done` when the agent has said\n // nothing for 15s, which is what happens when the runtime cannot serve\n // the turn at all (e.g. the sandbox quota is exhausted). Returning that\n // as a success showed the user a blank assistant bubble and — once\n // history became durable — wrote an empty transcript into it, leaving\n // a titled chat with nothing inside. Fail loudly and save nothing.\n if (!reply.trim()) {\n throw new Error(\n 'I could not produce a reply — the assistant runtime did not respond. It may be out of capacity right now. Nothing was changed; please try again shortly.'\n );\n }\n\n replayRef.current = null;\n\n // Persist the completed turn. Failing to save must not fail the turn —\n // the user got their answer, and the work the agent did is already real.\n try {\n const conversation = await saveAssistantTurn(\n {\n conversationId: conversationIdRef.current,\n prompt,\n reply,\n agentSessionId: sessionId,\n },\n workspaceId,\n authContext\n );\n persistConversationId(conversation.id);\n } catch (error) {\n console.warn('[BigConsole-Assistant] could not save turn to history', {\n error: error instanceof Error ? error.message : String(error),\n });\n }\n\n return { text: reply };\n } catch (error) {\n console.error('[BigConsole-Assistant] run error', {\n attempt,\n error: error instanceof Error ? error.message : String(error),\n stack: error instanceof Error ? error.stack : undefined,\n sandboxId: sandboxIdRef.current,\n sessionId: sessionIdRef.current,\n });\n // A stale/expired sandbox or session is recoverable — drop the warm\n // refs and retry once from a clean runtime.\n if (attempt === 1 && isRecoverable(error)) {\n persistSandboxId(null);\n persistSessionId(null);\n return run(2);\n }\n throw error;\n }\n };\n\n try {\n const result = await run(1);\n useAssistantRunStore.getState().finishRun(null);\n return result;\n } catch (error) {\n useAssistantRunStore.getState().finishRun(error instanceof Error ? error.message : String(error));\n throw error;\n }\n },\n [\n ctxWorkspaceId,\n ensureRuntime,\n getAccessToken,\n getWorkspaceToken,\n userId,\n persistSandboxId,\n persistSessionId,\n persistConversationId,\n ]\n );\n\n // ── Durable history (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 to wspace-conversations\n // instead, tagged with the product, so history is durable AND product-scoped —\n // a BigConsole chat can never surface in another product's panel.\n //\n // Two ids, doing different jobs:\n // conversationId — the durable chat. What History lists, and what the widget\n // treats as \"the session\".\n // sessionId — the LIVE agent session inside the sandbox. Ephemeral; a\n // hint stored on the conversation so a still-warm agent can\n // be resumed.\n\n const authFor = useCallback(\n (): AssistantSandboxAuthContext => ({\n accessToken: getAccessToken(),\n workspaceToken: getWorkspaceToken(),\n userId,\n organizationId: getOrganizationId(),\n }),\n [getAccessToken, getWorkspaceToken, userId]\n );\n\n const buildReplay = useCallback((messages: AssistantHistoryMessage[]): string | null => {\n if (messages.length === 0) return null;\n const transcript = messages\n .map((message) => `${message.role === 'user' ? 'User' : 'Assistant'}: ${message.content}`)\n .join('\\n\\n')\n .slice(-REPLAY_MAX_CHARS);\n\n return [\n 'You are resuming an earlier conversation. What follows is what was said in it — treat it as your own memory and continue seamlessly. Do not mention this replay, and do not redo work that was already completed.',\n '--- earlier in this conversation ---',\n transcript,\n '--- end ---',\n ].join('\\n\\n');\n }, []);\n\n const mapConversationMessages = useCallback(\n (messages: AssistantHistoryTurnMessage[]): AssistantHistoryMessage[] =>\n messages.map((message) => ({\n id: message.id,\n role: message.role === 'ASSISTANT' ? ('assistant' as const) : ('user' as const),\n content: sanitize(message.content),\n })),\n []\n );\n\n /**\n * Adopt a conversation: show its transcript, and line the agent up to continue\n * it — resuming the live session when one survives, replaying the transcript\n * when it does not.\n */\n const adoptConversation = useCallback(\n async (\n conversation: AssistantConversationSummary,\n workspaceId: string,\n auth: AssistantSandboxAuthContext\n ): Promise<AssistantHistoryMessage[]> => {\n const raw = await getAssistantConversationMessages(conversation.id, workspaceId, auth, HISTORY_MESSAGE_LIMIT);\n const messages = mapConversationMessages(raw);\n\n persistConversationId(conversation.id);\n\n // Do NOT adopt the stored agentSessionId. That session lives inside an\n // ephemeral sandbox (~600s TTL) and is almost always gone by the time a\n // past conversation is reopened — and a prompt to a dead session does not\n // error, it silently persists nothing, which surfaces as \"the assistant\n // runtime did not respond\". Always start a FRESH opencode session on the\n // current sandbox and replay the transcript so the agent keeps its context.\n // (A mass sandbox recycle — e.g. an image rollout — invalidates every\n // stored session at once, which is exactly when adoption bites hardest.)\n persistSessionId(null);\n replayRef.current = buildReplay(messages);\n\n return messages;\n },\n [mapConversationMessages, persistConversationId, persistSessionId, buildReplay]\n );\n\n const loadHistory = useCallback(async (): Promise<AssistantHistoryMessage[]> => {\n const workspaceId = getWorkspaceId(ctxWorkspaceId);\n if (!workspaceId || !getAccessToken()) return [];\n const auth = authFor();\n\n try {\n const conversations = await listAssistantConversations(workspaceId, auth, SESSION_LIST_LIMIT);\n\n // Reopen the chat the user was in; failing that, their most recent one, so\n // a fresh login lands them back where they left off rather than in a blank\n // chat with their history hidden behind a menu.\n const current = conversationIdRef.current;\n const target = conversations.find((conversation) => conversation.id === current) ?? conversations[0];\n if (!target) return [];\n\n return await adoptConversation(target, workspaceId, auth);\n } catch (error) {\n console.warn('[BigConsole-Assistant] could not restore history', {\n error: error instanceof Error ? error.message : String(error),\n });\n return [];\n }\n }, [ctxWorkspaceId, getAccessToken, authFor, adoptConversation]);\n\n const listSessions = useCallback(async (): Promise<AssistantSessionSummaryLocal[]> => {\n const workspaceId = getWorkspaceId(ctxWorkspaceId);\n if (!workspaceId || !getAccessToken()) return [];\n\n try {\n const conversations = await listAssistantConversations(workspaceId, authFor(), SESSION_LIST_LIMIT);\n // Titles come from the store, so listing is ONE round-trip — no per-chat\n // probing, which is what used to make opening History feel slow.\n return conversations.map((conversation) => ({\n id: conversation.id,\n title: conversation.title?.trim() || 'New chat',\n updatedAt: Date.parse(conversation.updatedAt) || undefined,\n active: conversation.id === conversationIdRef.current,\n }));\n } catch {\n return [];\n }\n }, [ctxWorkspaceId, getAccessToken, authFor]);\n\n /**\n * Start a new chat — instantly, and with no backend call.\n *\n * Both ids are simply detached: the agent session is created lazily on the next\n * prompt, and the conversation row by the first saveAssistantTurn. Nothing to\n * wait for, and no empty conversations left behind for chats nobody used.\n */\n const newSession = useCallback(async (): Promise<void> => {\n persistConversationId(null);\n persistSessionId(null);\n replayRef.current = null;\n return Promise.resolve();\n }, [persistConversationId, persistSessionId]);\n\n const deleteSession = useCallback(\n async (conversationId: string): Promise<void> => {\n const workspaceId = getWorkspaceId(ctxWorkspaceId);\n if (!workspaceId) return;\n\n await deleteAssistantConversation(conversationId, workspaceId, authFor());\n\n // Deleting the chat you are looking at leaves you in a fresh one.\n if (conversationIdRef.current === conversationId) {\n persistConversationId(null);\n persistSessionId(null);\n replayRef.current = null;\n }\n },\n [ctxWorkspaceId, authFor, persistConversationId, persistSessionId]\n );\n\n const selectSession = useCallback(\n async (conversationId: string): Promise<AssistantHistoryMessage[]> => {\n const workspaceId = getWorkspaceId(ctxWorkspaceId);\n if (!workspaceId) return [];\n const auth = authFor();\n\n const conversations = await listAssistantConversations(workspaceId, auth, SESSION_LIST_LIMIT);\n const target = conversations.find((conversation) => conversation.id === conversationId);\n if (!target) return [];\n\n return adoptConversation(target, workspaceId, auth);\n },\n [ctxWorkspaceId, authFor, adoptConversation]\n );\n\n return useMemo<AssistantTransport>(\n () => ({ sendPrompt, loadHistory, listSessions, newSession, deleteSession, selectSession }),\n [sendPrompt, loadHistory, listSessions, newSession, deleteSession, selectSession]\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"createSandboxAssistantTransport.js","names":[],"sources":["../../../src/bigconsole/assistant/createSandboxAssistantTransport.ts"],"sourcesContent":["/**\n * BigConsole adapter for the shared fe-libs AssistantWidget.\n *\n * The floating widget (fe-libs, Layer 1) is backend-agnostic — it calls an\n * injected `AssistantTransport`. This hook builds a transport that drives the\n * sandbox AI assistant (combined `assistant` mode = docs Q&A + api-calls): it\n * provisions/reuses a sandbox, opens a session, dispatches the prompt async,\n * then polls for the streamed answer. The agent introspects the GraphQL schema\n * and performs API calls on the user's behalf using their workspace token,\n * contextual to the current screen (via `gatherPageContext`).\n *\n * This is a faithful port of microfe-vibecontrols'\n * `services/createSandboxAssistantTransport.ts`; the only product-specific\n * difference lives in `assistantApi.createAssistantSandbox`\n * (`AI_ASSISTANT_PRODUCT=bigconsole`).\n */\n\nimport { useCallback, useMemo, useRef } from 'react';\nimport { useAuthToken } from '@burdenoff/fe-libs/shared/providers/shell';\nimport {\n createAssistantSandbox,\n createAssistantSession,\n extendAssistantSandboxTTL,\n findExistingSandbox,\n getAssistantMessages,\n isCachedAssistantSandboxReusable,\n sendAssistantPromptAsync,\n waitForAssistantServiceReady,\n waitForSandboxReady,\n} from './assistantApi';\nimport {\n ASSISTANT_SANDBOX_ID_KEY,\n ASSISTANT_SESSION_ID_KEY,\n type AssistantRuntimeApi,\n LEGACY_ASSISTANT_RUNTIME_KEYS,\n resolveAssistantRuntime,\n} from './assistantRuntime';\nimport { buildAttachmentBlock } from './attachmentExtract';\nimport { useAssistantRunStore } from './assistantRunStore';\nimport {\n type AssistantConversationSummary,\n type AssistantHistoryTurnMessage,\n deleteAssistantConversation,\n getAssistantConversationMessages,\n listAssistantConversations,\n saveAssistantTurn,\n} from './conversationHistoryApi';\nimport { gatherPageContext } from './pageContext';\nimport type { AssistantMode, AssistantRawMessage, AssistantRawMessagePart, AssistantSandboxAuthContext } from './types';\n\n/**\n * Locally-defined mirror of the fe-libs `AssistantTransport` contract.\n *\n * Intentionally NOT imported from `@burdenoff/fe-libs`: microfe's tsconfig maps\n * `@burdenoff/fe-libs/*` to fe-libs *source*, so vite-plugin-dts would rewrite a\n * cross-package type used in this hook's public signature to a broken\n * source-relative path in the emitted `.d.ts`. Structural typing makes this\n * shape assignable to fe-libs' `AssistantTransport` at the call site\n * (bigconsole-app's AppShell), which is where compatibility is enforced.\n */\ninterface AssistantSendArgs {\n prompt: string;\n /** Files attached via the widget's upload button (fe-libs carries the raw\n * File[]; we extract + fold a capped preview into the agent prompt here). */\n attachments?: File[];\n onProgress: (partialText: string) => void;\n signal: AbortSignal;\n}\n\n/** Mirror of fe-libs' `AssistantWidgetMessage` (see note above on why). */\ninterface AssistantHistoryMessage {\n id: string;\n role: 'user' | 'assistant';\n content: string;\n pending?: boolean;\n error?: boolean;\n}\n\n/** Mirror of fe-libs' `AssistantSessionSummary` (see note above on why). */\ninterface AssistantSessionSummaryLocal {\n id: string;\n title: string;\n updatedAt?: number;\n active?: boolean;\n}\n\nexport interface AssistantTransport {\n sendPrompt: (args: AssistantSendArgs) => Promise<{ text: string }>;\n loadHistory: () => Promise<AssistantHistoryMessage[]>;\n listSessions: () => Promise<AssistantSessionSummaryLocal[]>;\n newSession: () => Promise<void>;\n deleteSession: (sessionId: string) => Promise<void>;\n selectSession: (sessionId: string) => Promise<AssistantHistoryMessage[]>;\n}\n\n// BigConsole still boots the manually-tagged ACA image\n// `alpha-delegated-auth-v11` for the assistant sandbox. The historical\n// platform notes show that this image line reliably supports `api-calls`, while\n// the combined `assistant` mode depends on newer image contracts that are not\n// yet guaranteed on this tag. Use `api-calls` here so the assistant can execute\n// workspace GraphQL operations end-to-end right now. Once the underlying image\n// line is rebuilt and verified for combined mode, this can be switched back.\nconst MODE: AssistantMode = 'api-calls';\n// How long the UI will follow a single turn.\n//\n// This was 180s, which was SHORTER THAN THE WORK. A full \"create a school\n// attendance dashboard\" build — datasink → dashboard → parser → widget, each a\n// separate gateway call preceded by a model round-trip — measured 229s in prod.\n// So the agent finished, the dashboard genuinely existed, and the user was still\n// shown \"the assistant timed out\". That is worse than cosmetic: people retry and\n// end up with duplicate dashboards.\n//\n// 7 minutes covers the observed worst case with headroom. It costs nothing on\n// fast turns (we stop the moment the turn reports done), and the backend keeps\n// pace — the sandbox TTL is extended every TTL_EXTEND_INTERVAL_MS.\nconst STREAM_BUDGET_MS = 420_000;\nconst POLL_INTERVAL_MS = 1500;\nconst TTL_EXTEND_INTERVAL_MS = 30_000;\n// Raw agent messages per restore. The agent emits one message per internal\n// step, so a handful of turns is already dozens of messages — this is a cap on\n// the RAW fetch, not on the number of restored turns.\nconst HISTORY_MESSAGE_LIMIT = 200;\n/** Chats shown in History. Titles come from the store, so listing is one query. */\nconst SESSION_LIST_LIMIT = 25;\n/**\n * Cap on the transcript replayed into a resumed agent session. Long enough to\n * carry the ids and decisions that make \"that dashboard\" resolvable, short\n * enough not to crowd out the actual prompt.\n */\nconst REPLAY_MAX_CHARS = 6000;\n\n/**\n * The durable chat. This is the identity History lists. Unversioned on purpose:\n * it names a product-scoped wspace-conversations row, not a sandbox, so it was\n * never subject to the cross-product adoption the runtime keys were.\n */\nconst CONVERSATION_ID_KEY = 'bc-assistant-conversation-id';\n\nconst RUNTIME_API: AssistantRuntimeApi = {\n isCachedAssistantSandboxReusable,\n findExistingSandbox,\n createAssistantSandbox,\n waitForSandboxReady,\n waitForAssistantServiceReady,\n createAssistantSession,\n};\n\nfunction readStoredId(key: string): string | null {\n try {\n return window.sessionStorage.getItem(key);\n } catch {\n // sessionStorage unavailable (private mode) — degrade to a fresh session.\n return null;\n }\n}\n\nfunction writeStoredId(key: string, id: string | null): void {\n try {\n if (id) window.sessionStorage.setItem(key, id);\n else window.sessionStorage.removeItem(key);\n } catch {\n // Non-fatal: we simply lose cross-reload continuity.\n }\n}\n\n// ── Context helpers (mirror vibecontrols' resolution) ────────────────\n\nfunction getProfileContextValue(key: 'workspaceId' | 'organizationId'): string {\n try {\n const activeContextKey =\n key === 'workspaceId' ? 'burdenoff-active-context-workspace' : 'burdenoff-active-context-organization';\n const activeContextValue = localStorage.getItem(activeContextKey);\n if (activeContextValue) return activeContextValue;\n\n const activeProfileId = sessionStorage.getItem('bf-active-profile');\n if (!activeProfileId) return '';\n const raw = localStorage.getItem(`bf-p-${activeProfileId}-context`);\n if (!raw) return '';\n const context = JSON.parse(raw) as { workspaceId?: string; organizationId?: string };\n return context[key] ?? '';\n } catch {\n return '';\n }\n}\n\nfunction getWorkspaceId(fallback: string | null): string {\n const params = new URLSearchParams(window.location.search);\n return params.get('workspace') ?? getProfileContextValue('workspaceId') ?? fallback ?? '';\n}\n\nfunction getOrganizationId(): string {\n const params = new URLSearchParams(window.location.search);\n return params.get('org') ?? getProfileContextValue('organizationId');\n}\n\n// ── Message-progress helpers (pure; mirror vibecontrols) ─────────────\n\nfunction getMessageRole(message: AssistantRawMessage): string | undefined {\n // Check nested format first, then flat format\n return message.info?.role ?? message.role;\n}\n\nfunction getRawMessageCreatedAt(message: AssistantRawMessage): number {\n // Check nested format first (epoch ms), then flat format (ISO string or epoch ms)\n const nested = message.info?.time?.created;\n if (nested !== undefined) return nested;\n const flat = message.createdAt;\n if (flat === undefined) return 0;\n // If it's a string (ISO), parse it; otherwise treat as epoch ms\n if (typeof flat === 'string') {\n const parsed = Date.parse(flat);\n return isNaN(parsed) ? 0 : parsed;\n }\n return flat;\n}\n\nfunction getMessageCompleted(message: AssistantRawMessage): number | undefined {\n // Check nested format first, then flat format\n const nested = message.info?.time?.completed;\n if (nested !== undefined) return nested;\n const flat = message.completedAt;\n if (flat === undefined) return undefined;\n if (typeof flat === 'string') {\n const parsed = Date.parse(flat);\n return isNaN(parsed) ? undefined : parsed;\n }\n return flat;\n}\n\nfunction getAssistantText(message: AssistantRawMessage): string {\n // Check parts format first (nested), then flat content\n const parts = message.parts ?? [];\n const textFromParts = (parts ?? [])\n .filter((part) => part.type === 'text' && typeof part.text === 'string')\n .map((part) => part.text?.trim() ?? '')\n .filter(Boolean)\n .join('\\n');\n if (textFromParts) return textFromParts;\n // Fallback to flat content field\n return typeof message.content === 'string' ? message.content.trim() : '';\n}\n\n// Friendly, human-readable labels for the agent's tools so the progress line\n// reads like \"Searching the schema…\" instead of \"Running: bash\". The agent sets\n// a `description` on every bash call (e.g. \"Search for sales-related types in\n// workspace schema\") and a todo list on todowrite — surface those directly.\nconst TOOL_LABELS: Record<string, string> = {\n bash: 'Running a command',\n webfetch: 'Fetching a page',\n 'file.read': 'Reading files',\n 'file.write': 'Writing files',\n 'file.edit': 'Editing files',\n 'file.find.text': 'Searching the code',\n 'file.find.file': 'Looking for files',\n todowrite: 'Planning the steps',\n todoread: 'Reviewing the plan',\n};\n\nfunction asRecord(value: unknown): Record<string, unknown> | undefined {\n return typeof value === 'object' && value !== null ? (value as Record<string, unknown>) : undefined;\n}\n\n/** Best-effort human summary of what a single tool part is doing right now. */\nfunction describeToolPart(part: AssistantRawMessagePart): string {\n const tool = part.tool ?? 'tool';\n const input = asRecord(part.state?.input);\n\n // bash carries a plain-English `description` of the step — the best signal.\n const description = input?.description;\n if (typeof description === 'string' && description.trim()) return description.trim();\n\n // todowrite carries the todo list — surface the item being worked on.\n const todos = input?.todos;\n if (Array.isArray(todos)) {\n const active = todos.find((todo) => asRecord(todo)?.status === 'in_progress') ?? todos[0];\n const content = asRecord(active)?.content;\n if (typeof content === 'string' && content.trim()) return content.trim();\n }\n\n return TOOL_LABELS[tool] ?? `Running ${tool}`;\n}\n\nfunction getToolProgress(message: AssistantRawMessage): string[] {\n const parts = message.parts ?? [];\n return parts\n .filter((part) => part.type === 'tool' && part.tool)\n .map((part) => {\n const status = part.state?.status ?? 'running';\n const label = describeToolPart(part);\n if (status === 'completed') return `✓ ${label}`;\n if (status === 'failed') return `⚠ ${label}`;\n return `⏳ ${label}…`;\n });\n}\n\nfunction buildProgress(\n messages: AssistantRawMessage[],\n sinceMs: number,\n previousContent?: string,\n previousContentAtMs?: number\n): { content: string; done: boolean } {\n const relevant = messages\n .filter((message) => getMessageRole(message) === 'assistant' && getRawMessageCreatedAt(message) >= sinceMs)\n .sort((left, right) => getRawMessageCreatedAt(left) - getRawMessageCreatedAt(right));\n\n // Debug: log message filtering when no relevant messages found\n if (relevant.length === 0 && messages.length > 0) {\n console.log('[BigConsole-Assistant] buildProgress: no relevant messages', {\n totalMessages: messages.length,\n sinceMs,\n messageRoles: messages.map((m) => getMessageRole(m)),\n messageTimestamps: messages.map((m) => getRawMessageCreatedAt(m)),\n });\n }\n\n let content: string;\n let done: boolean;\n\n if (relevant.length === 0) {\n content = '';\n // No usable assistant content for THIS turn yet. Separate \"still thinking\"\n // from \"responded but unreadable\", so a slow reasoning model is never\n // mistaken for a dead runtime:\n //\n // - No assistant message exists AT ALL: the agent is still starting up, or\n // gpt-5.6-terra (a reasoning model) is still thinking before its first\n // token. Time-to-first-message routinely exceeds the old 15s window,\n // especially with a large system prompt — which declared the turn\n // done-and-empty and surfaced \"the assistant runtime did not respond\"\n // even though the backend was healthy. NEVER give up here; let the outer\n // turn budget (STREAM_BUDGET_MS) decide, exactly like the running-tool\n // guard in the branch below.\n //\n // - An assistant message exists but none maps to this turn (timestamp skew\n // / role mismatch): the turn may really be over but unreadable. Keep a\n // staleness fallback — but give reasoning models ample room (90s, not\n // 15s) so a slow first token is never read as a stalled turn.\n const anyAssistantMessage = messages.some((message) => getMessageRole(message) === 'assistant');\n const emptyForMs =\n previousContent === content && previousContentAtMs !== undefined ? Date.now() - previousContentAtMs : 0;\n // Skew case (a message exists but is unreadable): 90s is plenty.\n // Nothing-at-all case (slow reasoning first token): wait 150s before calling\n // it a genuine no-show — a safe upper bound for time-to-first-token that\n // still fails a truly dead runtime (unbooted sandbox / quota) in ~2.5 min\n // instead of the old 15s that tripped healthy reasoning turns.\n done = anyAssistantMessage ? emptyForMs >= 90_000 : emptyForMs >= 150_000;\n } else {\n const latest = relevant[relevant.length - 1]!;\n\n // ACCUMULATE the run, don't just show its last line.\n //\n // The agent emits a message per step, and it now narrates each one and prints\n // a link the moment a create lands (\"✅ Data sink created — [Open …](/…)\").\n // Showing only the newest message threw all of that away a second later: the\n // user saw a lone \"Thinking…\" and none of the links they were promised. Join\n // the whole run instead, so the panel reads as a live account of what is\n // happening and every link stays on screen.\n const narration = relevant.map(getAssistantText).filter(Boolean);\n const toolProgress = getToolProgress(latest);\n content = [...narration, ...toolProgress].join('\\n\\n');\n\n const officiallyDone = Boolean(getMessageCompleted(latest)) && content.length > 0;\n\n // A tool that is still running is proof the turn is alive, so never let the\n // staleness fallback fire underneath it. A single gateway call can sit on the\n // same \"⏳ Creating the data sink…\" line for far longer than the old 15s\n // window, which would have declared the turn finished mid-build.\n const hasRunningTool = (latest.parts ?? []).some(\n (part) => part.type === 'tool' && part.state?.status !== 'completed' && part.state?.status !== 'failed'\n );\n\n const staleDone =\n !officiallyDone &&\n !hasRunningTool &&\n previousContent === content &&\n previousContentAtMs !== undefined &&\n Date.now() - previousContentAtMs >= 45_000;\n\n done = officiallyDone || staleDone;\n }\n\n return { content, done };\n}\n\nfunction sanitize(response: string): string {\n return response\n .replace(/(Authorization\\s*:\\s*Bearer\\s+)[^\\s\\n]+/gi, '$1[REDACTED]')\n .replace(/(X-Workspace-Authorization\\s*:\\s*Bearer\\s+)[^\\s\\n]+/gi, '$1[REDACTED]')\n .replace(/\\beyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9._-]+\\.[A-Za-z0-9._-]+\\b/g, '[REDACTED_JWT]')\n .replace(/\\bsk-ant-[A-Za-z0-9-]+\\b/g, '[REDACTED_API_KEY]');\n}\n\nfunction isRecoverable(error: unknown): boolean {\n const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();\n if (\n message.includes('rate limit exceeded') ||\n message.includes('unauthorized') ||\n message.includes('k8s api error 401')\n ) {\n return false;\n }\n return [\n 'sandbox not found',\n 'sandbox is not running',\n 'sandbox service not available yet',\n 'sandbox failed',\n 'sandbox startup timed out',\n 'assistant service did not become healthy',\n 'proxy error: 404',\n 'proxy error: 502',\n 'proxy error: 503',\n 'unable to connect',\n 'image pull',\n 'container failed',\n 'bootstrap failed',\n // Transient network errors from the browser fetch — gateway CORS preflight\n // failures, mid-stream resets, and Cloudflare 524s all surface as\n // \"Failed to fetch\" via TypeError. They're worth retrying on a clean\n // runtime since the underlying sandbox state is unaffected.\n 'failed to fetch',\n 'cf-proxy timeout',\n // Subgraph returned 500 with a generic message — gateway returns this as\n // a GraphQL error rather than an HTTP error. The actual underlying cause\n // (e.g., transient Prisma timeout) is recoverable, but a fresh sandbox\n // may be needed.\n 'unexpected error',\n // An empty turn (\"the assistant runtime did not respond\") is most often a\n // stale/dead session or sandbox reference — a prompt to a session whose\n // sandbox has been recycled persists nothing rather than erroring. Treat it\n // as recoverable so the retry drops the warm refs and mints a fresh\n // sandbox+session; a genuinely down runtime simply empties again on attempt 2\n // and then surfaces to the user. (attempt-gated to a single retry upstream.)\n 'the assistant runtime did not respond',\n ].some((fragment) => message.includes(fragment));\n}\n\n/**\n * Returns a memoized `AssistantTransport` wired to the BigConsole sandbox\n * assistant agent. The sandbox + session are cached in refs so follow-up turns\n * reuse the warm environment for the lifetime of the host shell.\n */\nexport function useSandboxAssistantTransport(): AssistantTransport {\n const { getAccessToken, getWorkspaceToken, userId, workspaceId: ctxWorkspaceId } = useAuthToken();\n\n // Rehydrate the sandbox + session ids persisted by the previous page\n // lifecycle. These were being WRITTEN to sessionStorage but never read back,\n // so every reload silently opened a brand-new agent session: the chat looked\n // empty AND the agent genuinely lost the conversation (it could no longer\n // resolve \"that datasink\" / \"the dashboard you just made\").\n //\n // Restoring both together is what makes history real rather than cosmetic —\n // the transcript we replay into the UI is the same session the agent will\n // keep reasoning over. A rehydrated sandbox id is NOT trusted as-is: before its\n // first use `resolveAssistantRuntime` re-fetches it and holds it to the reuse\n // predicate (product stamp, owner, confirmWrites, status), discarding it — and\n // its session — if it fails. A stale/expired sandbox is discarded the same way.\n const [initialSandboxId, initialSessionId, initialConversationId] = useMemo(() => {\n // Pre-BOFF-7331 builds wrote unversioned keys and may have cached another\n // product's sandbox in them. Never read them; clear them.\n for (const legacyKey of LEGACY_ASSISTANT_RUNTIME_KEYS) writeStoredId(legacyKey, null);\n return [\n readStoredId(ASSISTANT_SANDBOX_ID_KEY),\n readStoredId(ASSISTANT_SESSION_ID_KEY),\n readStoredId(CONVERSATION_ID_KEY),\n ] as const;\n }, []);\n\n const sandboxIdRef = useRef<string | null>(initialSandboxId);\n const sessionIdRef = useRef<string | null>(initialSessionId);\n const conversationIdRef = useRef<string | null>(initialConversationId);\n /** Memory only: which sandbox (for which user) this page lifecycle has proven drivable. */\n const verifiedSandboxKeyRef = useRef<string | null>(null);\n /**\n * Transcript to feed the agent on its next prompt.\n *\n * A chat can outlive the agent that produced it: the transcript is durable,\n * the sandbox session is not. Showing the messages while the agent silently\n * remembers nothing is the worst of both worlds — ask it to \"add a widget to\n * that dashboard\" and it has no idea what \"that\" is. So when a chat is\n * resumed after its agent is gone, replay the conversation into its first\n * prompt.\n */\n const replayRef = useRef<string | null>(null);\n\n const persistSandboxId = useCallback((id: string | null) => {\n sandboxIdRef.current = id;\n writeStoredId(ASSISTANT_SANDBOX_ID_KEY, id);\n }, []);\n\n const persistSessionId = useCallback((id: string | null) => {\n sessionIdRef.current = id;\n writeStoredId(ASSISTANT_SESSION_ID_KEY, id);\n }, []);\n\n const persistConversationId = useCallback((id: string | null) => {\n conversationIdRef.current = id;\n writeStoredId(CONVERSATION_ID_KEY, id);\n }, []);\n\n const ensureRuntime = useCallback(\n async (\n workspaceId: string,\n authContext: AssistantSandboxAuthContext\n ): Promise<{ sandboxId: string; sessionId: string }> =>\n resolveAssistantRuntime({\n workspaceId,\n mode: MODE,\n authContext,\n api: RUNTIME_API,\n hasAccessToken: () => !!getAccessToken(),\n store: {\n getSandboxId: () => sandboxIdRef.current,\n setSandboxId: persistSandboxId,\n getSessionId: () => sessionIdRef.current,\n setSessionId: persistSessionId,\n getVerifiedSandboxKey: () => verifiedSandboxKeyRef.current,\n setVerifiedSandboxKey: (key) => {\n verifiedSandboxKeyRef.current = key;\n },\n },\n }),\n [getAccessToken, persistSandboxId, persistSessionId]\n );\n\n const sendPrompt = useCallback(\n async ({\n prompt,\n attachments,\n onProgress,\n signal,\n }: AssistantSendArgs): Promise<{\n text: string;\n }> => {\n const workspaceId = getWorkspaceId(ctxWorkspaceId);\n console.log('[BigConsole-Assistant] sendPrompt called', {\n promptLength: prompt.length,\n workspaceId,\n hasCtxWorkspaceId: !!ctxWorkspaceId,\n authContextKeys: {\n hasAccessToken: !!getAccessToken(),\n hasWorkspaceToken: !!getWorkspaceToken(),\n hasUserId: !!userId,\n },\n locationSearch: window.location.search,\n });\n if (!workspaceId) {\n throw new Error('The assistant needs an active workspace. Open a workspace and try again.');\n }\n const authContext: AssistantSandboxAuthContext = {\n accessToken: getAccessToken(),\n workspaceToken: getWorkspaceToken(),\n userId,\n organizationId: getOrganizationId(),\n };\n console.log('[BigConsole-Assistant] authContext prepared', {\n hasAccessToken: !!authContext.accessToken,\n hasWorkspaceToken: !!authContext.workspaceToken,\n hasUserId: !!authContext.userId,\n hasOrgId: !!authContext.organizationId,\n });\n\n // Drive the live preview panel. The agent has no event stream, so the\n // narration IS the signal: the store parses it into a DataSink → Dashboard\n // → Parser → Widget rail. Fed here rather than in the widget because the\n // host owns this transport, so no fe-libs change is needed.\n const runStore = useAssistantRunStore.getState();\n runStore.startRun(prompt);\n const reportProgress = (partial: string): void => {\n onProgress(partial);\n useAssistantRunStore.getState().applyProgress(partial);\n };\n\n const run = async (attempt: 1 | 2): Promise<{ text: string }> => {\n try {\n console.log('[BigConsole-Assistant] run attempt', attempt);\n const { sandboxId, sessionId } = await ensureRuntime(workspaceId, authContext);\n console.log('[BigConsole-Assistant] ensureRuntime resolved', { sandboxId, sessionId });\n\n // Resuming a chat whose agent session is gone: hand the agent the\n // earlier transcript once, on the first prompt of the resumed chat, so\n // it answers with that context instead of from a blank slate. Consumed\n // on success — never replayed twice into the same session.\n const replay = replayRef.current;\n // Extract any uploaded files (JSON/CSV/Excel/PDF) into a capped text\n // block and fold it into the prompt the agent sees, so it can design a\n // DataSink straight from the pasted rows. The user-facing `prompt`\n // (preview narration, logs) stays clean.\n const attachmentBlock = attachments && attachments.length > 0 ? await buildAttachmentBlock(attachments) : '';\n const promptWithData = attachmentBlock ? `${prompt}\\n\\n${attachmentBlock}` : prompt;\n const agentPrompt = replay ? `${replay}\\n\\n---\\n\\n${promptWithData}` : promptWithData;\n\n const startedAt = Date.now();\n console.log('[BigConsole-Assistant] calling sendAssistantPromptAsync', {\n sandboxId,\n workspaceId,\n sessionId,\n promptLength: agentPrompt.length,\n replayed: Boolean(replay),\n });\n await sendAssistantPromptAsync(\n sandboxId,\n workspaceId,\n sessionId,\n agentPrompt,\n MODE,\n gatherPageContext(),\n authContext\n );\n\n const timeoutAt = Date.now() + STREAM_BUDGET_MS;\n let lastTtlExtensionAt = Date.now();\n let lastContent = '';\n let lastContentChangeAt = Date.now();\n let progress = buildProgress([], startedAt);\n\n while (Date.now() < timeoutAt) {\n if (signal.aborted) throw new Error('Cancelled');\n\n const messages = await getAssistantMessages(sandboxId, workspaceId, sessionId, authContext, 50);\n console.log('[BigConsole-Assistant] poll', {\n elapsedMs: Date.now() - startedAt,\n messageCount: messages.length,\n firstFewRoles: messages.slice(0, 3).map((m) => getMessageRole(m)),\n firstFewTimestamps: messages.slice(0, 3).map((m) => getRawMessageCreatedAt(m)),\n });\n progress = buildProgress(messages, startedAt, lastContent, lastContentChangeAt);\n console.log('[BigConsole-Assistant] progress', {\n contentPreview: progress.content.slice(0, 100),\n done: progress.done,\n lastContentChangeAt: Date.now() - lastContentChangeAt,\n });\n if (progress.content !== lastContent) {\n lastContent = progress.content;\n lastContentChangeAt = Date.now();\n }\n reportProgress(sanitize(progress.content));\n if (progress.done) {\n console.log('[BigConsole-Assistant] progress.done=true, breaking poll loop');\n break;\n }\n\n if (Date.now() - lastTtlExtensionAt > TTL_EXTEND_INTERVAL_MS) {\n await extendAssistantSandboxTTL(sandboxId, workspaceId, 600, authContext).catch(() => undefined);\n lastTtlExtensionAt = Date.now();\n }\n\n await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));\n }\n\n if (!progress.done) {\n // Do NOT say \"please try again\". We stopped watching; the agent did\n // not stop working, and anything it already created is real. Telling\n // people to retry is how you get duplicate dashboards.\n throw new Error(\n 'I stopped waiting for a reply, but I may still be working — anything I already created will be there. Check your dashboards and data sinks before asking again, so you do not end up with duplicates.'\n );\n }\n\n const reply = sanitize(progress.content);\n\n // An empty reply is a FAILED turn, not a successful one.\n //\n // `buildProgress` gives up and reports `done` when the agent has said\n // nothing for 15s, which is what happens when the runtime cannot serve\n // the turn at all (e.g. the sandbox quota is exhausted). Returning that\n // as a success showed the user a blank assistant bubble and — once\n // history became durable — wrote an empty transcript into it, leaving\n // a titled chat with nothing inside. Fail loudly and save nothing.\n if (!reply.trim()) {\n throw new Error(\n 'I could not produce a reply — the assistant runtime did not respond. It may be out of capacity right now. Nothing was changed; please try again shortly.'\n );\n }\n\n replayRef.current = null;\n\n // Persist the completed turn. Failing to save must not fail the turn —\n // the user got their answer, and the work the agent did is already real.\n try {\n const conversation = await saveAssistantTurn(\n {\n conversationId: conversationIdRef.current,\n prompt,\n reply,\n agentSessionId: sessionId,\n },\n workspaceId,\n authContext\n );\n persistConversationId(conversation.id);\n } catch (error) {\n console.warn('[BigConsole-Assistant] could not save turn to history', {\n error: error instanceof Error ? error.message : String(error),\n });\n }\n\n return { text: reply };\n } catch (error) {\n console.error('[BigConsole-Assistant] run error', {\n attempt,\n error: error instanceof Error ? error.message : String(error),\n stack: error instanceof Error ? error.stack : undefined,\n sandboxId: sandboxIdRef.current,\n sessionId: sessionIdRef.current,\n });\n // A stale/expired sandbox or session is recoverable — drop the warm\n // refs and retry once from a clean runtime.\n if (attempt === 1 && isRecoverable(error)) {\n persistSandboxId(null);\n persistSessionId(null);\n return run(2);\n }\n throw error;\n }\n };\n\n try {\n const result = await run(1);\n useAssistantRunStore.getState().finishRun(null);\n return result;\n } catch (error) {\n useAssistantRunStore.getState().finishRun(error instanceof Error ? error.message : String(error));\n throw error;\n }\n },\n [\n ctxWorkspaceId,\n ensureRuntime,\n getAccessToken,\n getWorkspaceToken,\n userId,\n persistSandboxId,\n persistSessionId,\n persistConversationId,\n ]\n );\n\n // ── Durable history (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 to wspace-conversations\n // instead, tagged with the product, so history is durable AND product-scoped —\n // a BigConsole chat can never surface in another product's panel.\n //\n // Two ids, doing different jobs:\n // conversationId — the durable chat. What History lists, and what the widget\n // treats as \"the session\".\n // sessionId — the LIVE agent session inside the sandbox. Ephemeral; a\n // hint stored on the conversation so a still-warm agent can\n // be resumed.\n\n const authFor = useCallback(\n (): AssistantSandboxAuthContext => ({\n accessToken: getAccessToken(),\n workspaceToken: getWorkspaceToken(),\n userId,\n organizationId: getOrganizationId(),\n }),\n [getAccessToken, getWorkspaceToken, userId]\n );\n\n const buildReplay = useCallback((messages: AssistantHistoryMessage[]): string | null => {\n if (messages.length === 0) return null;\n const transcript = messages\n .map((message) => `${message.role === 'user' ? 'User' : 'Assistant'}: ${message.content}`)\n .join('\\n\\n')\n .slice(-REPLAY_MAX_CHARS);\n\n return [\n 'You are resuming an earlier conversation. What follows is what was said in it — treat it as your own memory and continue seamlessly. Do not mention this replay, and do not redo work that was already completed.',\n '--- earlier in this conversation ---',\n transcript,\n '--- end ---',\n ].join('\\n\\n');\n }, []);\n\n const mapConversationMessages = useCallback(\n (messages: AssistantHistoryTurnMessage[]): AssistantHistoryMessage[] =>\n messages.map((message) => ({\n id: message.id,\n role: message.role === 'ASSISTANT' ? ('assistant' as const) : ('user' as const),\n content: sanitize(message.content),\n })),\n []\n );\n\n /**\n * Adopt a conversation: show its transcript, and line the agent up to continue\n * it — resuming the live session when one survives, replaying the transcript\n * when it does not.\n */\n const adoptConversation = useCallback(\n async (\n conversation: AssistantConversationSummary,\n workspaceId: string,\n auth: AssistantSandboxAuthContext\n ): Promise<AssistantHistoryMessage[]> => {\n const raw = await getAssistantConversationMessages(conversation.id, workspaceId, auth, HISTORY_MESSAGE_LIMIT);\n const messages = mapConversationMessages(raw);\n\n persistConversationId(conversation.id);\n\n // Do NOT adopt the stored agentSessionId. That session lives inside an\n // ephemeral sandbox (~600s TTL) and is almost always gone by the time a\n // past conversation is reopened — and a prompt to a dead session does not\n // error, it silently persists nothing, which surfaces as \"the assistant\n // runtime did not respond\". Always start a FRESH opencode session on the\n // current sandbox and replay the transcript so the agent keeps its context.\n // (A mass sandbox recycle — e.g. an image rollout — invalidates every\n // stored session at once, which is exactly when adoption bites hardest.)\n persistSessionId(null);\n replayRef.current = buildReplay(messages);\n\n return messages;\n },\n [mapConversationMessages, persistConversationId, persistSessionId, buildReplay]\n );\n\n const loadHistory = useCallback(async (): Promise<AssistantHistoryMessage[]> => {\n const workspaceId = getWorkspaceId(ctxWorkspaceId);\n if (!workspaceId || !getAccessToken()) return [];\n const auth = authFor();\n\n try {\n const conversations = await listAssistantConversations(workspaceId, auth, SESSION_LIST_LIMIT);\n\n // Reopen the chat the user was in; failing that, their most recent one, so\n // a fresh login lands them back where they left off rather than in a blank\n // chat with their history hidden behind a menu.\n const current = conversationIdRef.current;\n const target = conversations.find((conversation) => conversation.id === current) ?? conversations[0];\n if (!target) return [];\n\n return await adoptConversation(target, workspaceId, auth);\n } catch (error) {\n console.warn('[BigConsole-Assistant] could not restore history', {\n error: error instanceof Error ? error.message : String(error),\n });\n return [];\n }\n }, [ctxWorkspaceId, getAccessToken, authFor, adoptConversation]);\n\n const listSessions = useCallback(async (): Promise<AssistantSessionSummaryLocal[]> => {\n const workspaceId = getWorkspaceId(ctxWorkspaceId);\n if (!workspaceId || !getAccessToken()) return [];\n\n try {\n const conversations = await listAssistantConversations(workspaceId, authFor(), SESSION_LIST_LIMIT);\n // Titles come from the store, so listing is ONE round-trip — no per-chat\n // probing, which is what used to make opening History feel slow.\n return conversations.map((conversation) => ({\n id: conversation.id,\n title: conversation.title?.trim() || 'New chat',\n updatedAt: Date.parse(conversation.updatedAt) || undefined,\n active: conversation.id === conversationIdRef.current,\n }));\n } catch {\n return [];\n }\n }, [ctxWorkspaceId, getAccessToken, authFor]);\n\n /**\n * Start a new chat — instantly, and with no backend call.\n *\n * Both ids are simply detached: the agent session is created lazily on the next\n * prompt, and the conversation row by the first saveAssistantTurn. Nothing to\n * wait for, and no empty conversations left behind for chats nobody used.\n */\n const newSession = useCallback(async (): Promise<void> => {\n persistConversationId(null);\n persistSessionId(null);\n replayRef.current = null;\n return Promise.resolve();\n }, [persistConversationId, persistSessionId]);\n\n const deleteSession = useCallback(\n async (conversationId: string): Promise<void> => {\n const workspaceId = getWorkspaceId(ctxWorkspaceId);\n if (!workspaceId) return;\n\n await deleteAssistantConversation(conversationId, workspaceId, authFor());\n\n // Deleting the chat you are looking at leaves you in a fresh one.\n if (conversationIdRef.current === conversationId) {\n persistConversationId(null);\n persistSessionId(null);\n replayRef.current = null;\n }\n },\n [ctxWorkspaceId, authFor, persistConversationId, persistSessionId]\n );\n\n const selectSession = useCallback(\n async (conversationId: string): Promise<AssistantHistoryMessage[]> => {\n const workspaceId = getWorkspaceId(ctxWorkspaceId);\n if (!workspaceId) return [];\n const auth = authFor();\n\n const conversations = await listAssistantConversations(workspaceId, auth, SESSION_LIST_LIMIT);\n const target = conversations.find((conversation) => conversation.id === conversationId);\n if (!target) return [];\n\n return adoptConversation(target, workspaceId, auth);\n },\n [ctxWorkspaceId, authFor, adoptConversation]\n );\n\n return useMemo<AssistantTransport>(\n () => ({ sendPrompt, loadHistory, listSessions, newSession, deleteSession, selectSession }),\n [sendPrompt, loadHistory, listSessions, newSession, deleteSession, selectSession]\n );\n}\n"],"mappings":"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"}
@@ -1,115 +1,177 @@
1
- import e from "../datasink/DataSinkTableViewer.js";
2
- import t from "../../hooks/useWidgetOperations.js";
3
- import n from "../../hooks/useDataSinkOperations.js";
4
- import r from "../../hooks/useParserOperations.js";
5
- import i from "../widgets/WidgetWrapper.js";
6
- import a from "../DrilldownDashboardRenderer.js";
7
- import { useEffect as o, useRef as s, useState as c } from "react";
8
- import { Spinner as l } from "@burdenoff/fe-libs/ui";
9
- import { jsx as u, jsxs as d } from "react/jsx-runtime";
1
+ import { GetParserDocument as e, ListWidgetsByDashboardDocument as t } from "../../../generated/wspace-operations.js";
2
+ import n from "../datasink/DataSinkTableViewer.js";
3
+ import { normalizeWidget as r } from "../../hooks/useWidgetOperations.js";
4
+ import i from "../../hooks/useDataSinkOperations.js";
5
+ import a from "../widgets/WidgetWrapper.js";
6
+ import o from "../DrilldownDashboardRenderer.js";
7
+ import { useEffect as s, useRef as c, useState as l } from "react";
8
+ import { useApolloClient as u } from "@apollo/client/react";
9
+ import { Spinner as d } from "@burdenoff/fe-libs/ui";
10
+ import { jsx as f, jsxs as p } from "react/jsx-runtime";
10
11
  //#region src/bigconsole/components/preview/AiPreviewContent.tsx
11
- var f = 4e3, p = 6e4, m = "flex min-h-[6rem] min-w-0 items-center justify-center gap-2 rounded-card bg-bg-sunken px-3 py-6 text-center text-sm text-text-muted";
12
- function h({ label: e }) {
13
- return /* @__PURE__ */ d("div", {
14
- className: m,
15
- children: [/* @__PURE__ */ u(l, { className: "size-4 shrink-0" }), e]
12
+ var m = 4e3, h = 6e4, g = "flex min-h-[6rem] min-w-0 items-center justify-center gap-2 rounded-card bg-bg-sunken px-3 py-6 text-center text-sm text-text-muted";
13
+ function _({ label: e, testId: t }) {
14
+ return /* @__PURE__ */ p("div", {
15
+ className: g,
16
+ "data-testid": t,
17
+ children: [/* @__PURE__ */ f(d, { className: "size-4 shrink-0" }), e]
16
18
  });
17
19
  }
18
- function g({ text: e }) {
19
- return /* @__PURE__ */ u("p", {
20
- className: m,
20
+ function v({ text: e, testId: t }) {
21
+ return /* @__PURE__ */ f("p", {
22
+ className: g,
23
+ "data-testid": t,
21
24
  children: e
22
25
  });
23
26
  }
24
- function _(e, t, n) {
25
- let [r, i] = c(null), [a, l] = c(!0), u = s(e);
26
- return u.current = e, o(() => {
27
- if (!t) {
28
- l(!1);
29
- return;
30
- }
27
+ function y({ text: e, testId: t }) {
28
+ return /* @__PURE__ */ f("p", {
29
+ className: `${g} text-status-error-text`,
30
+ "data-testid": t,
31
+ children: e
32
+ });
33
+ }
34
+ function b(e, t, n) {
35
+ let [r, i] = l(null), [a, o] = l(!!t), [u, d] = l(null), [f, p] = l(t), g = c(e);
36
+ return g.current = e, t !== f && (p(t), i(null), d(null), o(!!t)), s(() => {
37
+ if (!t) return;
31
38
  let e = !1, r = Date.now(), a = async () => {
32
- let t = await u.current();
33
- e || (t != null && i(t), l(!1));
39
+ try {
40
+ let t = await g.current();
41
+ if (e) return;
42
+ t != null && i(t), d(null);
43
+ } catch (t) {
44
+ if (e) return;
45
+ d(t instanceof Error ? t : /* @__PURE__ */ Error("Failed to load preview content"));
46
+ } finally {
47
+ e || o(!1);
48
+ }
34
49
  };
35
50
  a();
36
- let o = window.setInterval(() => {
37
- !n || Date.now() - r > p || typeof document < "u" && document.visibilityState !== "visible" || a();
38
- }, f);
51
+ let s = window.setInterval(() => {
52
+ !n || Date.now() - r > h || typeof document < "u" && document.visibilityState !== "visible" || a();
53
+ }, m);
39
54
  return () => {
40
- e = !0, window.clearInterval(o);
55
+ e = !0, window.clearInterval(s);
41
56
  };
42
57
  }, [t, n]), {
43
58
  data: r,
44
- loading: a
59
+ loading: a,
60
+ error: u
45
61
  };
46
62
  }
47
- function v({ id: t, live: r }) {
48
- let { getDataSinkData: i } = n({ skipFetch: !0 }), { data: a, loading: o } = _(async () => {
49
- let e = await i({ id: t }, { limit: 50 });
50
- return e ? e.raw : null;
51
- }, t, r);
52
- return t ? o ? /* @__PURE__ */ u(h, { label: "Loading the data sink’s data…" }) : /* @__PURE__ */ u("div", {
63
+ function x({ id: e, live: t }) {
64
+ let { getDataSinkData: r } = i({ skipFetch: !0 }), { data: a, loading: o, error: s } = b(async () => {
65
+ let t = await r({ id: e }, { limit: 50 });
66
+ return t ? t.raw : null;
67
+ }, e, t);
68
+ return e ? s ? /* @__PURE__ */ f(y, {
69
+ text: "Could not load the data sink's rows.",
70
+ testId: "ai-preview-datasink-error"
71
+ }) : o ? /* @__PURE__ */ f(_, {
72
+ label: "Loading the data sink’s data…",
73
+ testId: "ai-preview-datasink-loading"
74
+ }) : /* @__PURE__ */ f("div", {
53
75
  "data-testid": "ai-preview-content-datasink",
54
76
  className: "min-w-0",
55
- children: /* @__PURE__ */ u(e, {
77
+ children: /* @__PURE__ */ f(n, {
56
78
  data: a,
57
79
  maxHeight: "300px"
58
80
  })
59
- }) : /* @__PURE__ */ u(g, { text: "Waiting for the data sink to be created…" });
81
+ }) : /* @__PURE__ */ f(v, {
82
+ text: "Waiting for the data sink to be created…",
83
+ testId: "ai-preview-datasink-waiting"
84
+ });
60
85
  }
61
- function y({ dashboardId: e }) {
62
- return e ? /* @__PURE__ */ u("div", {
86
+ function S({ dashboardId: e }) {
87
+ return e ? /* @__PURE__ */ f("div", {
63
88
  "data-testid": "ai-preview-content-dashboard",
64
89
  className: "min-w-0",
65
- children: /* @__PURE__ */ u(a, {
90
+ children: /* @__PURE__ */ f(o, {
66
91
  dashboardId: e,
67
92
  params: {}
68
93
  })
69
- }) : /* @__PURE__ */ u(g, { text: "Waiting for the dashboard to be created…" });
94
+ }) : /* @__PURE__ */ f(v, {
95
+ text: "Waiting for the dashboard to be created…",
96
+ testId: "ai-preview-dashboard-waiting"
97
+ });
70
98
  }
71
- function b({ parserId: t, live: n }) {
72
- let { executeParser: i } = r({ skipFetch: !0 }), { data: a, loading: o } = _(async () => {
73
- let e = await i(t ?? "");
74
- return e ? e.output : null;
75
- }, t, n);
76
- return t ? o ? /* @__PURE__ */ u(h, { label: "Running the parser…" }) : /* @__PURE__ */ u("div", {
99
+ function C({ parserId: t, live: r }) {
100
+ let a = u(), { getDataSinkData: o } = i({ skipFetch: !0 }), { data: s, loading: c, error: l } = b(async () => {
101
+ if (!t) return null;
102
+ let n = (await a.query({
103
+ query: e,
104
+ variables: { id: t },
105
+ fetchPolicy: "network-only"
106
+ })).data?.parser?.outputKey;
107
+ if (!n) return null;
108
+ let r = await o({ key: n }, { limit: 50 });
109
+ return r ? r.raw : null;
110
+ }, t, r);
111
+ return t ? l ? /* @__PURE__ */ f(y, {
112
+ text: "Could not read the parser's output.",
113
+ testId: "ai-preview-parser-error"
114
+ }) : c ? /* @__PURE__ */ f(_, {
115
+ label: "Loading the parser’s output…",
116
+ testId: "ai-preview-parser-loading"
117
+ }) : /* @__PURE__ */ f("div", {
77
118
  "data-testid": "ai-preview-content-parser",
78
119
  className: "min-w-0",
79
- children: /* @__PURE__ */ u(e, {
80
- data: a,
120
+ children: /* @__PURE__ */ f(n, {
121
+ data: s,
81
122
  maxHeight: "280px"
82
123
  })
83
- }) : /* @__PURE__ */ u(g, { text: "No parser in this build — the rows were already shaped." });
124
+ }) : /* @__PURE__ */ f(v, {
125
+ text: "No parser in this build — the rows were already shaped.",
126
+ testId: "ai-preview-parser-waiting"
127
+ });
84
128
  }
85
- function x(e) {
129
+ function w(e) {
86
130
  let t = e.toUpperCase();
87
131
  return t.includes("METRIC") || t.includes("KPI") || t.includes("CARD") || t.includes("STAT") ? "min(160px, 40vh)" : t.includes("TABLE") || t.includes("LIST") ? "min(280px, 50vh)" : "min(320px, 50vh)";
88
132
  }
89
- function S({ dashboardId: e, live: n }) {
90
- let { widgets: r, loading: a, refetch: c } = t(void 0, e, { skipFetch: !e }), l = s(c);
91
- return l.current = c, o(() => {
92
- if (!e) return;
93
- let t = Date.now(), r = window.setInterval(() => {
94
- !n || Date.now() - t > p || typeof document < "u" && document.visibilityState !== "visible" || l.current();
95
- }, f);
96
- return () => window.clearInterval(r);
97
- }, [e, n]), e ? a && r.length === 0 ? /* @__PURE__ */ u(h, { label: "Loading widgets…" }) : r.length === 0 ? /* @__PURE__ */ u(g, { text: "No widgets on this dashboard yet." }) : /* @__PURE__ */ u("div", {
133
+ function T(e, n) {
134
+ let i = u(), { data: a, loading: o, error: s } = b(async () => e ? ((await i.query({
135
+ query: t,
136
+ variables: { dashboardId: e },
137
+ fetchPolicy: "network-only"
138
+ })).data?.listWidgetsByDashboard || []).map(r) : null, e, n);
139
+ return {
140
+ widgets: a ?? [],
141
+ loading: o,
142
+ error: s
143
+ };
144
+ }
145
+ function E({ dashboardId: e, live: t }) {
146
+ let { widgets: n, loading: r, error: i } = T(e, t);
147
+ return e ? i ? /* @__PURE__ */ f(y, {
148
+ text: "Could not load this dashboard's widgets.",
149
+ testId: "ai-preview-widget-error"
150
+ }) : r && n.length === 0 ? /* @__PURE__ */ f(_, {
151
+ label: "Loading widgets…",
152
+ testId: "ai-preview-widget-loading"
153
+ }) : n.length === 0 ? /* @__PURE__ */ f(v, {
154
+ text: "No widgets on this dashboard yet.",
155
+ testId: "ai-preview-widget-empty"
156
+ }) : /* @__PURE__ */ f("div", {
98
157
  "data-testid": "ai-preview-content-widget",
99
158
  className: "flex min-w-0 flex-col gap-list-gap",
100
- children: r.map((e) => /* @__PURE__ */ u("div", {
159
+ children: n.map((e) => /* @__PURE__ */ f("div", {
101
160
  className: "min-w-0 overflow-hidden rounded-card border border-[var(--color-border-subtle)] bg-bg-canvas",
102
- style: { height: x(e.type) },
103
- children: /* @__PURE__ */ u(i, {
161
+ style: { height: w(e.type) },
162
+ children: /* @__PURE__ */ f(a, {
104
163
  widget: e,
105
164
  isLoading: !1,
106
165
  error: null,
107
166
  filterValues: {}
108
167
  })
109
168
  }, e.id))
110
- }) : /* @__PURE__ */ u(g, { text: "Waiting for the dashboard — widgets live on it." });
169
+ }) : /* @__PURE__ */ f(v, {
170
+ text: "Waiting for the dashboard — widgets live on it.",
171
+ testId: "ai-preview-widget-waiting"
172
+ });
111
173
  }
112
174
  //#endregion
113
- export { y as DashboardContent, v as DataSinkContent, b as ParserContent, S as WidgetsContent };
175
+ export { S as DashboardContent, x as DataSinkContent, C as ParserContent, E as WidgetsContent };
114
176
 
115
177
  //# sourceMappingURL=AiPreviewContent.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"AiPreviewContent.js","names":[],"sources":["../../../../src/bigconsole/components/preview/AiPreviewContent.tsx"],"sourcesContent":["/**\n * Live content renderers for the AI preview tabs.\n *\n * Each tab shows the REAL content of the entity the assistant just built —\n * the data sink's rows, the live dashboard, the parser's transformed output,\n * the rendered widget charts — not a list with an Open button. They reuse the\n * same components the real pages use (DataSinkTableViewer, DrilldownDashboard-\n * Renderer, WidgetWrapper) so what you see in the preview is what you get.\n *\n * These require Apollo + the BigConsole providers, which is why they live only\n * in the full AiPreviewPanel (mounted inside BigConsoleRoot), never in the\n * store-only app-shell mini panel.\n */\nimport { useEffect, useRef, useState } from 'react';\nimport { Spinner } from '@burdenoff/fe-libs/ui';\n\nimport { DataSinkTableViewer } from '../datasink/DataSinkTableViewer';\nimport { DrilldownDashboardRenderer } from '../DrilldownDashboardRenderer';\nimport { WidgetWrapper } from '../widgets/WidgetWrapper';\nimport { useDataSinkOperations } from '../../hooks/useDataSinkOperations';\nimport { useParserOperations } from '../../hooks/useParserOperations';\nimport { useWidgetOperations } from '../../hooks/useWidgetOperations';\n\n/** While the build is live, re-pull content on this cadence so it fills in. */\nconst CONTENT_POLL_MS = 4000;\n/** Stop the live re-pull after this long even if the turn is still running. */\nconst CONTENT_POLL_WINDOW_MS = 60_000;\n\n/**\n * Loading and waiting states share one recessed, height-reserved block so the\n * panel does not jump every time a tab fills in — previously each tab had its\n * own bare paragraph and its own height.\n */\nconst PLACEHOLDER_BOX =\n 'flex min-h-[6rem] min-w-0 items-center justify-center gap-2 rounded-card bg-bg-sunken px-3 py-6 text-center text-sm text-text-muted';\n\nfunction CenteredSpinner({ label }: { label: string }) {\n return (\n <div className={PLACEHOLDER_BOX}>\n <Spinner className=\"size-4 shrink-0\" />\n {label}\n </div>\n );\n}\n\nfunction Waiting({ text }: { text: string }) {\n return <p className={PLACEHOLDER_BOX}>{text}</p>;\n}\n\n/**\n * Fetch-on-mount, then re-fetch every CONTENT_POLL_MS while `live`, bounded to\n * CONTENT_POLL_WINDOW_MS. Returns { data, loading, done }. `fetcher` must be\n * stable-ish; we hold it in a ref so the effect keys only on `key`/`live`.\n */\nfunction useLiveContent<T>(fetcher: () => Promise<T | null>, key: string | undefined, live: boolean) {\n const [data, setData] = useState<T | null>(null);\n const [loading, setLoading] = useState(true);\n const fetcherRef = useRef(fetcher);\n fetcherRef.current = fetcher;\n\n useEffect(() => {\n if (!key) {\n setLoading(false);\n return;\n }\n let cancelled = false;\n const startedAt = Date.now();\n const pull = async () => {\n const result = await fetcherRef.current();\n if (cancelled) return;\n if (result !== null && result !== undefined) setData(result);\n setLoading(false);\n };\n void pull();\n const intervalId = window.setInterval(() => {\n if (!live || Date.now() - startedAt > CONTENT_POLL_WINDOW_MS) return;\n if (typeof document !== 'undefined' && document.visibilityState !== 'visible') return;\n void pull();\n }, CONTENT_POLL_MS);\n return () => {\n cancelled = true;\n window.clearInterval(intervalId);\n };\n }, [key, live]);\n\n return { data, loading };\n}\n\nexport function DataSinkContent({ id, live }: { id?: string; live: boolean }) {\n const { getDataSinkData } = useDataSinkOperations({ skipFetch: true });\n const { data, loading } = useLiveContent(\n async () => {\n const res = await getDataSinkData({ id }, { limit: 50 });\n return res ? res.raw : null;\n },\n id,\n live\n );\n\n if (!id) return <Waiting text=\"Waiting for the data sink to be created…\" />;\n if (loading) return <CenteredSpinner label=\"Loading the data sink’s data…\" />;\n return (\n <div data-testid=\"ai-preview-content-datasink\" className=\"min-w-0\">\n <DataSinkTableViewer data={data} maxHeight=\"300px\" />\n </div>\n );\n}\n\nexport function DashboardContent({ dashboardId }: { dashboardId?: string }) {\n if (!dashboardId) return <Waiting text=\"Waiting for the dashboard to be created…\" />;\n // No inner `max-h + overflow-y-auto`: the panel already owns a vertical\n // scroll, and nesting a second one put two scrollbars in ~440px of space.\n return (\n <div data-testid=\"ai-preview-content-dashboard\" className=\"min-w-0\">\n <DrilldownDashboardRenderer dashboardId={dashboardId} params={{}} />\n </div>\n );\n}\n\nexport function ParserContent({ parserId, live }: { parserId?: string; live: boolean }) {\n const { executeParser } = useParserOperations({ skipFetch: true });\n const { data, loading } = useLiveContent(\n async () => {\n const res = await executeParser(parserId ?? '');\n return res ? res.output : null;\n },\n parserId,\n live\n );\n\n if (!parserId) return <Waiting text=\"No parser in this build — the rows were already shaped.\" />;\n if (loading) return <CenteredSpinner label=\"Running the parser…\" />;\n // The \"Parser output (transformed rows)\" caption used to sit here; it now\n // lives on the section header, so the tab does not label itself twice.\n return (\n <div data-testid=\"ai-preview-content-parser\" className=\"min-w-0\">\n <DataSinkTableViewer data={data} maxHeight=\"280px\" />\n </div>\n );\n}\n\n/**\n * Recharts needs a definite height or it renders 0px tall, but a hard 320px box\n * eats a short viewport whole. `min(px, vh)` keeps the definite height Recharts\n * needs while letting the box shrink on small screens.\n */\nfunction widgetBoxHeight(type: string): string {\n const t = type.toUpperCase();\n if (t.includes('METRIC') || t.includes('KPI') || t.includes('CARD') || t.includes('STAT')) return 'min(160px, 40vh)';\n if (t.includes('TABLE') || t.includes('LIST')) return 'min(280px, 50vh)';\n return 'min(320px, 50vh)';\n}\n\nexport function WidgetsContent({ dashboardId, live }: { dashboardId?: string; live: boolean }) {\n const { widgets, loading, refetch } = useWidgetOperations(undefined, dashboardId, {\n skipFetch: !dashboardId,\n });\n\n const refetchRef = useRef(refetch);\n refetchRef.current = refetch;\n useEffect(() => {\n if (!dashboardId) return;\n const startedAt = Date.now();\n const intervalId = window.setInterval(() => {\n if (!live || Date.now() - startedAt > CONTENT_POLL_WINDOW_MS) return;\n if (typeof document !== 'undefined' && document.visibilityState !== 'visible') return;\n void refetchRef.current();\n }, CONTENT_POLL_MS);\n return () => window.clearInterval(intervalId);\n }, [dashboardId, live]);\n\n if (!dashboardId) return <Waiting text=\"Waiting for the dashboard — widgets live on it.\" />;\n if (loading && widgets.length === 0) return <CenteredSpinner label=\"Loading widgets…\" />;\n if (widgets.length === 0) return <Waiting text=\"No widgets on this dashboard yet.\" />;\n\n return (\n <div data-testid=\"ai-preview-content-widget\" className=\"flex min-w-0 flex-col gap-list-gap\">\n {widgets.map((widget) => (\n <div\n key={widget.id}\n // `border-border-subtle` is registered as a frozen light hex in this\n // shell, so the box kept a light border in dark mode.\n className=\"min-w-0 overflow-hidden rounded-card border border-[var(--color-border-subtle)] bg-bg-canvas\"\n style={{ height: widgetBoxHeight(widget.type) }}\n >\n <WidgetWrapper widget={widget} isLoading={false} error={null} filterValues={{}} />\n </div>\n ))}\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"AiPreviewContent.js","names":[],"sources":["../../../../src/bigconsole/components/preview/AiPreviewContent.tsx"],"sourcesContent":["/**\n * Live content renderers for the AI preview tabs.\n *\n * Each tab shows the REAL content of the entity the assistant just built —\n * the data sink's rows, the live dashboard, the parser's transformed output,\n * the rendered widget charts — not a list with an Open button. They reuse the\n * same components the real pages use (DataSinkTableViewer, DrilldownDashboard-\n * Renderer, WidgetWrapper) so what you see in the preview is what you get.\n *\n * These require Apollo + the BigConsole providers, which is why they live only\n * in the full AiPreviewPanel (mounted inside BigConsoleRoot), never in the\n * store-only app-shell mini panel.\n *\n * ── BC0 (BOFF-7333; BOFF-7270, BOFF-7271) ────────────────────────────────────\n * A PREVIEW MUST OBSERVE, NEVER MUTATE, AND NEVER PUBLISH.\n *\n * Three defects made this panel change the system it was previewing:\n *\n * 1. The Parser tab polled `executeParser` — a MUTATION — every 4s for 60s,\n * re-running the transform server-side up to ~15 times per turn. It now\n * reads the parser (query), follows its `outputKey`, and reads that sink\n * read-only. Zero mutations.\n * 2. The Widgets tab went through `useWidgetOperations`, which publishes into\n * the GLOBAL widget store — so opening the preview overwrote the widgets an\n * already-open dashboard was rendering. It now holds widgets in local state.\n * 3. `useLiveContent` never reset on key change, so the PREVIOUS entity's rows\n * stayed on screen as though current, and an un-caught rejection left the\n * tab spinning forever with no error surface.\n *\n * ★ Deliberately NOT changed: the other `executeParser` callers\n * (`ParserViewPage`, `CustomWidgetBuilderSheet`, `PropertiesPanel`). Those are\n * user-initiated \"run this parser\" actions where executing IS the intent. Only\n * the passive preview had no business mutating.\n */\nimport { useEffect, useRef, useState } from 'react';\nimport { useApolloClient } from '@apollo/client/react';\nimport { Spinner } from '@burdenoff/fe-libs/ui';\n\nimport { DataSinkTableViewer } from '../datasink/DataSinkTableViewer';\nimport { DrilldownDashboardRenderer } from '../DrilldownDashboardRenderer';\nimport { WidgetWrapper } from '../widgets/WidgetWrapper';\nimport { useDataSinkOperations } from '../../hooks/useDataSinkOperations';\nimport { normalizeWidget } from '../../hooks/useWidgetOperations';\nimport { GetParserDocument, ListWidgetsByDashboardDocument } from '../../../generated/wspace-operations';\nimport type { Parser, Widget } from '../../types';\n\n/** While the build is live, re-pull content on this cadence so it fills in. */\nconst CONTENT_POLL_MS = 4000;\n/** Stop the live re-pull after this long even if the turn is still running. */\nconst CONTENT_POLL_WINDOW_MS = 60_000;\n\n/**\n * Loading and waiting states share one recessed, height-reserved block so the\n * panel does not jump every time a tab fills in — previously each tab had its\n * own bare paragraph and its own height.\n */\nconst PLACEHOLDER_BOX =\n 'flex min-h-[6rem] min-w-0 items-center justify-center gap-2 rounded-card bg-bg-sunken px-3 py-6 text-center text-sm text-text-muted';\n\nfunction CenteredSpinner({ label, testId }: { label: string; testId?: string }) {\n return (\n <div className={PLACEHOLDER_BOX} data-testid={testId}>\n <Spinner className=\"size-4 shrink-0\" />\n {label}\n </div>\n );\n}\n\nfunction Waiting({ text, testId }: { text: string; testId?: string }) {\n return (\n <p className={PLACEHOLDER_BOX} data-testid={testId}>\n {text}\n </p>\n );\n}\n\n/**\n * ★ BC0: the tab had no error surface at all. An un-caught rejection inside the\n * poller left `loading` true forever, so a failed read was indistinguishable\n * from a slow one — and it surfaced as an unhandled promise rejection.\n */\nfunction ErrorBox({ text, testId }: { text: string; testId?: string }) {\n return (\n <p className={`${PLACEHOLDER_BOX} text-status-error-text`} data-testid={testId}>\n {text}\n </p>\n );\n}\n\n/**\n * Fetch-on-mount, then re-fetch every CONTENT_POLL_MS while `live`, bounded to\n * CONTENT_POLL_WINDOW_MS. Returns { data, loading, error }. `fetcher` must be\n * stable-ish; we hold it in a ref so the effect keys only on `key`/`live`.\n *\n * ★★ BC0: RESETS ON KEY CHANGE. Previously `data` survived a key change, so\n * switching to a different entity rendered the PREVIOUS entity's rows as though\n * they were the new one's — with `loading` already false, nothing said otherwise.\n */\nfunction useLiveContent<T>(fetcher: () => Promise<T | null>, key: string | undefined, live: boolean) {\n const [data, setData] = useState<T | null>(null);\n const [loading, setLoading] = useState(Boolean(key));\n const [error, setError] = useState<Error | null>(null);\n const [renderedKey, setRenderedKey] = useState(key);\n const fetcherRef = useRef(fetcher);\n fetcherRef.current = fetcher;\n\n // ★★ The reset happens DURING RENDER, not in an effect. An effect-based reset\n // commits one frame carrying the PREVIOUS entity's rows before clearing them —\n // which is the very defect being fixed here. Adjusting state while rendering is\n // React's supported pattern for \"a prop changed, discard the derived state\": it\n // re-runs this component immediately, before anything reaches the DOM, so the\n // stale rows are never painted. A key going undefined clears the render too.\n if (key !== renderedKey) {\n setRenderedKey(key);\n setData(null);\n setError(null);\n setLoading(Boolean(key));\n }\n\n useEffect(() => {\n if (!key) return;\n\n let cancelled = false;\n const startedAt = Date.now();\n const pull = async () => {\n try {\n const result = await fetcherRef.current();\n if (cancelled) return;\n if (result !== null && result !== undefined) setData(result);\n setError(null);\n } catch (err) {\n if (cancelled) return;\n // A later successful poll clears this, so a transient blip does not\n // strand the tab on an error.\n setError(err instanceof Error ? err : new Error('Failed to load preview content'));\n } finally {\n if (!cancelled) setLoading(false);\n }\n };\n void pull();\n const intervalId = window.setInterval(() => {\n if (!live || Date.now() - startedAt > CONTENT_POLL_WINDOW_MS) return;\n if (typeof document !== 'undefined' && document.visibilityState !== 'visible') return;\n void pull();\n }, CONTENT_POLL_MS);\n return () => {\n cancelled = true;\n window.clearInterval(intervalId);\n };\n }, [key, live]);\n\n return { data, loading, error };\n}\n\nexport function DataSinkContent({ id, live }: { id?: string; live: boolean }) {\n const { getDataSinkData } = useDataSinkOperations({ skipFetch: true });\n const { data, loading, error } = useLiveContent(\n async () => {\n const res = await getDataSinkData({ id }, { limit: 50 });\n return res ? res.raw : null;\n },\n id,\n live\n );\n\n if (!id) return <Waiting text=\"Waiting for the data sink to be created…\" testId=\"ai-preview-datasink-waiting\" />;\n if (error) return <ErrorBox text=\"Could not load the data sink's rows.\" testId=\"ai-preview-datasink-error\" />;\n if (loading) return <CenteredSpinner label=\"Loading the data sink’s data…\" testId=\"ai-preview-datasink-loading\" />;\n return (\n <div data-testid=\"ai-preview-content-datasink\" className=\"min-w-0\">\n <DataSinkTableViewer data={data} maxHeight=\"300px\" />\n </div>\n );\n}\n\nexport function DashboardContent({ dashboardId }: { dashboardId?: string }) {\n if (!dashboardId)\n return <Waiting text=\"Waiting for the dashboard to be created…\" testId=\"ai-preview-dashboard-waiting\" />;\n // No inner `max-h + overflow-y-auto`: the panel already owns a vertical\n // scroll, and nesting a second one put two scrollbars in ~440px of space.\n return (\n <div data-testid=\"ai-preview-content-dashboard\" className=\"min-w-0\">\n <DrilldownDashboardRenderer dashboardId={dashboardId} params={{}} />\n </div>\n );\n}\n\n/**\n * ★★★ BC0 — READ-ONLY. This tab used to call `executeParser`, a mutation, on\n * every poll: ~15 server-side re-executions per turn purely to look at output.\n *\n * A parser writes its transformed rows into a data sink named by its own\n * `outputKey` (`ParserCoreFields`), and `getDataSinkData` accepts `{ key }`. So\n * the output is fully readable without executing anything: read the parser,\n * follow `outputKey`, read the sink.\n *\n * ★ Apollo is queried directly rather than via `useParserOperations.fetchParser`,\n * because that helper calls `addParser()` and would publish into the global\n * parser store — trading defect 2's leak for the same leak one store over.\n */\nexport function ParserContent({ parserId, live }: { parserId?: string; live: boolean }) {\n const client = useApolloClient();\n const { getDataSinkData } = useDataSinkOperations({ skipFetch: true });\n const { data, loading, error } = useLiveContent(\n async () => {\n if (!parserId) return null;\n const res = await client.query<{ parser?: Parser | null }>({\n query: GetParserDocument,\n variables: { id: parserId },\n fetchPolicy: 'network-only',\n });\n const outputKey = res.data?.parser?.outputKey;\n if (!outputKey) return null;\n const sink = await getDataSinkData({ key: outputKey }, { limit: 50 });\n return sink ? sink.raw : null;\n },\n parserId,\n live\n );\n\n if (!parserId)\n return (\n <Waiting text=\"No parser in this build — the rows were already shaped.\" testId=\"ai-preview-parser-waiting\" />\n );\n if (error) return <ErrorBox text=\"Could not read the parser's output.\" testId=\"ai-preview-parser-error\" />;\n // \"Running the parser…\" would now be a lie — nothing is executed.\n if (loading) return <CenteredSpinner label=\"Loading the parser’s output…\" testId=\"ai-preview-parser-loading\" />;\n // The \"Parser output (transformed rows)\" caption used to sit here; it now\n // lives on the section header, so the tab does not label itself twice.\n return (\n <div data-testid=\"ai-preview-content-parser\" className=\"min-w-0\">\n <DataSinkTableViewer data={data} maxHeight=\"280px\" />\n </div>\n );\n}\n\n/**\n * Recharts needs a definite height or it renders 0px tall, but a hard 320px box\n * eats a short viewport whole. `min(px, vh)` keeps the definite height Recharts\n * needs while letting the box shrink on small screens.\n */\nfunction widgetBoxHeight(type: string): string {\n const t = type.toUpperCase();\n if (t.includes('METRIC') || t.includes('KPI') || t.includes('CARD') || t.includes('STAT')) return 'min(160px, 40vh)';\n if (t.includes('TABLE') || t.includes('LIST')) return 'min(280px, 50vh)';\n return 'min(320px, 50vh)';\n}\n\n/**\n * ★★★ BC0 — COMPONENT-LOCAL widgets.\n *\n * `useWidgetOperations` publishes every fetch into the GLOBAL widget store, so\n * merely opening this preview replaced the widget set an already-open dashboard\n * was rendering. The query and the normalisation are reused — `normalizeWidget`\n * is exported from that hook precisely so the two paths cannot drift — but the\n * result stays here, in this component's state.\n *\n * It also inherits `useLiveContent`'s bounded window and visibility check,\n * replacing the bespoke interval this tab used to run.\n */\nfunction usePreviewWidgets(dashboardId: string | undefined, live: boolean) {\n const client = useApolloClient();\n const { data, loading, error } = useLiveContent<Widget[]>(\n async () => {\n if (!dashboardId) return null;\n const result = await client.query<Record<string, unknown>>({\n query: ListWidgetsByDashboardDocument,\n variables: { dashboardId },\n fetchPolicy: 'network-only',\n });\n const list = (result.data?.listWidgetsByDashboard as Record<string, unknown>[]) || [];\n return list.map(normalizeWidget);\n },\n dashboardId,\n live\n );\n return { widgets: data ?? [], loading, error };\n}\n\nexport function WidgetsContent({ dashboardId, live }: { dashboardId?: string; live: boolean }) {\n const { widgets, loading, error } = usePreviewWidgets(dashboardId, live);\n\n if (!dashboardId)\n return <Waiting text=\"Waiting for the dashboard — widgets live on it.\" testId=\"ai-preview-widget-waiting\" />;\n if (error) return <ErrorBox text=\"Could not load this dashboard's widgets.\" testId=\"ai-preview-widget-error\" />;\n if (loading && widgets.length === 0)\n return <CenteredSpinner label=\"Loading widgets…\" testId=\"ai-preview-widget-loading\" />;\n if (widgets.length === 0)\n return <Waiting text=\"No widgets on this dashboard yet.\" testId=\"ai-preview-widget-empty\" />;\n\n return (\n <div data-testid=\"ai-preview-content-widget\" className=\"flex min-w-0 flex-col gap-list-gap\">\n {widgets.map((widget) => (\n <div\n key={widget.id}\n // `border-border-subtle` is registered as a frozen light hex in this\n // shell, so the box kept a light border in dark mode.\n className=\"min-w-0 overflow-hidden rounded-card border border-[var(--color-border-subtle)] bg-bg-canvas\"\n style={{ height: widgetBoxHeight(widget.type) }}\n >\n <WidgetWrapper widget={widget} isLoading={false} error={null} filterValues={{}} />\n </div>\n ))}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -278,6 +278,6 @@ function v(l, v, y = {}) {
278
278
  };
279
279
  }
280
280
  //#endregion
281
- export { v as default };
281
+ export { v as default, _ as normalizeWidget };
282
282
 
283
283
  //# sourceMappingURL=useWidgetOperations.js.map