@tangle-network/agent-app 0.45.23 → 0.45.25

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.
Files changed (61) hide show
  1. package/dist/{DesignCanvas-ZG7DJS7C.js → DesignCanvas-3RRPVPCP.js} +2 -2
  2. package/dist/{DesignCanvasEditor-SLFMHOII.js → DesignCanvasEditor-KAMMEX7E.js} +4 -4
  3. package/dist/{TimelineEditor-6PV42FLG.js → TimelineEditor-IIBVCAZV.js} +2 -2
  4. package/dist/VaultPane-MUWGYVJ7.js +7 -0
  5. package/dist/assistant/index.d.ts +11 -0
  6. package/dist/assistant/index.js +89 -56
  7. package/dist/assistant/index.js.map +1 -1
  8. package/dist/chat-react/index.js +4 -1
  9. package/dist/chat-react/index.js.map +1 -1
  10. package/dist/chat-routes/index.d.ts +57 -6
  11. package/dist/chat-routes/index.js +225 -17
  12. package/dist/chat-routes/index.js.map +1 -1
  13. package/dist/{chunk-MLACJ6DB.js → chunk-7O4FZAFP.js} +10 -10
  14. package/dist/chunk-7O4FZAFP.js.map +1 -0
  15. package/dist/{chunk-OASVAPRL.js → chunk-AWPK7ZDS.js} +3 -3
  16. package/dist/{chunk-YTMKRL3L.js → chunk-HZ4WCA37.js} +3 -2
  17. package/dist/chunk-HZ4WCA37.js.map +1 -0
  18. package/dist/{chunk-FVJP2RTC.js → chunk-JJJEO4ND.js} +91 -36
  19. package/dist/chunk-JJJEO4ND.js.map +1 -0
  20. package/dist/{chunk-7PFFFRVU.js → chunk-KOMP6NDW.js} +172 -43
  21. package/dist/chunk-KOMP6NDW.js.map +1 -0
  22. package/dist/{chunk-TZO3SOUL.js → chunk-LSQR6HM6.js} +49 -33
  23. package/dist/chunk-LSQR6HM6.js.map +1 -0
  24. package/dist/{chunk-3IHYJIDJ.js → chunk-NC2V63EL.js} +15 -15
  25. package/dist/chunk-NC2V63EL.js.map +1 -0
  26. package/dist/{chunk-73CQJOIU.js → chunk-OYGM5WLD.js} +5 -5
  27. package/dist/chunk-OYGM5WLD.js.map +1 -0
  28. package/dist/chunk-YIQHOAHN.js +340 -0
  29. package/dist/chunk-YIQHOAHN.js.map +1 -0
  30. package/dist/design-canvas-react/index.d.ts +2 -2
  31. package/dist/design-canvas-react/index.js +9 -9
  32. package/dist/design-canvas-react/index.js.map +1 -1
  33. package/dist/design-canvas-react/lazy.js +1 -1
  34. package/dist/sequences-react/index.js +2 -2
  35. package/dist/theme/index.d.ts +27 -1
  36. package/dist/theme/index.js +1 -1
  37. package/dist/theme/tailwind-preset.d.ts +25 -0
  38. package/dist/theme/tailwind-preset.js +55 -1
  39. package/dist/theme/tailwind-preset.js.map +1 -1
  40. package/dist/theme/tokens.css +298 -40
  41. package/dist/vault/index.js +1 -1
  42. package/dist/vault/lazy.js +1 -1
  43. package/dist/web-react/async/index.js +19 -307
  44. package/dist/web-react/async/index.js.map +1 -1
  45. package/dist/web-react/index.d.ts +73 -6
  46. package/dist/web-react/index.js +2 -2
  47. package/dist/work-product-react/index.js +1 -1
  48. package/package.json +1 -1
  49. package/dist/VaultPane-HFH734K6.js +0 -7
  50. package/dist/chunk-3IHYJIDJ.js.map +0 -1
  51. package/dist/chunk-73CQJOIU.js.map +0 -1
  52. package/dist/chunk-7PFFFRVU.js.map +0 -1
  53. package/dist/chunk-FVJP2RTC.js.map +0 -1
  54. package/dist/chunk-MLACJ6DB.js.map +0 -1
  55. package/dist/chunk-TZO3SOUL.js.map +0 -1
  56. package/dist/chunk-YTMKRL3L.js.map +0 -1
  57. /package/dist/{DesignCanvas-ZG7DJS7C.js.map → DesignCanvas-3RRPVPCP.js.map} +0 -0
  58. /package/dist/{DesignCanvasEditor-SLFMHOII.js.map → DesignCanvasEditor-KAMMEX7E.js.map} +0 -0
  59. /package/dist/{TimelineEditor-6PV42FLG.js.map → TimelineEditor-IIBVCAZV.js.map} +0 -0
  60. /package/dist/{VaultPane-HFH734K6.js.map → VaultPane-MUWGYVJ7.js.map} +0 -0
  61. /package/dist/{chunk-OASVAPRL.js.map → chunk-AWPK7ZDS.js.map} +0 -0
@@ -144,7 +144,10 @@ function ComposerModeControls({ planMode }) {
144
144
  onClick: () => planMode.setEnabled(!planMode.enabled),
145
145
  title: "Plan mode: the agent proposes a plan you approve before it executes",
146
146
  className: cn(
147
- "inline-flex h-7 items-center gap-1 rounded-full border px-2.5 text-xs transition-colors",
147
+ // Inset ring: this chip sits in a composer row that clips its overflow,
148
+ // so an outward ring loses three of its four sides. Only the offset is
149
+ // overridden — width and colour stay with the tokens.
150
+ "inline-flex h-7 items-center gap-1 rounded-full border px-2.5 text-xs transition-colors focus-visible:[outline-offset:-2px]",
148
151
  planMode.enabled ? "border-primary/50 bg-primary/10 text-primary" : "border-border bg-transparent text-muted-foreground hover:text-foreground",
149
152
  planMode.saving && "opacity-60"
150
153
  ),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/chat-react/composer-agent-controls.tsx","../../src/chat-react/entry-composer.tsx","../../src/chat-react/composer-mode-controls.tsx"],"sourcesContent":["import { useCallback, useMemo, useRef } from 'react'\nimport {\n AgentSessionControls,\n type AgentSessionProfileControl,\n} from '@tangle-network/sandbox-ui/chat'\nimport { canonicalModelId } from '@tangle-network/sandbox-ui/dashboard'\nimport type { ModelInfo } from '@tangle-network/sandbox-ui/dashboard'\n// Browser-safe: model-catalog.ts imports nothing. Taken from the source module\n// rather than the `/catalog` subpath so this stays one build graph.\nimport { isChatCapableModel, type RouterModel } from '../runtime/model-catalog'\nimport type { HarnessType, ReasoningEffort, ReasoningLevel } from './types'\n\n/**\n * The model half of the composer's agent identity. `models` is the catalog the\n * picker renders; `value` may be a bare or a canonical (provider-prefixed) id —\n * the adapter maps it to canonical form for the picker and hands the canonical\n * id back to `onChange`, so a product that stores bare ids only has to accept\n * canonical ones in its setter.\n */\nexport interface ComposerModelSelection {\n value: string\n onChange: (next: string) => void\n models: ModelInfo[]\n loading?: boolean\n disabled?: boolean\n}\n\nexport interface ComposerHarnessSelection {\n value: HarnessType\n onChange: (next: HarnessType) => void\n /** Restrict the offered backends. Omit to take the context default. */\n available?: ReadonlyArray<HarnessType>\n disabled?: boolean\n /**\n * A thread's sidecar session is pinned to one harness at its first message,\n * so once the thread has messages the picker locks. With `onNewChat` the\n * locked chip becomes the informative lock (explains the binding + a button\n * to start a fresh session on another backend).\n */\n locked?: boolean\n lockReason?: string\n onNewChat?: () => void\n}\n\nexport interface ComposerEffortSelection {\n value: ReasoningLevel\n onChange: (next: ReasoningLevel) => void\n /**\n * Depths this product's backend actually applies. Omit for the default set.\n * `auto` is NOT a member: the picker adds that sentinel itself, so listing it\n * here would offer it twice.\n */\n available?: ReadonlyArray<ReasoningEffort>\n disabled?: boolean\n}\n\n/**\n * A selectable named bundle of persona and tool groups. The product resolves\n * the selected id to the runtime AgentProfile before creating or continuing a\n * session; the composer only needs the display catalog and persistence hooks.\n */\nexport type ComposerProfileSelection = AgentSessionProfileControl\n\nexport interface ComposerAgentControlsProps {\n model: ComposerModelSelection\n /** Omit on a router-backed composer: a direct model call has no CLI backend,\n * so the picker would be inert. Model + effort still render. */\n harness?: ComposerHarnessSelection\n /** Universal named agent profile: persona + tool groups, independent of backend. */\n profile?: ComposerProfileSelection\n effort?: ComposerEffortSelection\n className?: string\n /**\n * Which surface the controls live on. `'chat'` (the default) drops the\n * shell-only `cli-base` backend — it has no conversational agent — and tells\n * the shared control to use its chat-only default set. It also drops models\n * that cannot serve a text turn (image / TTS / embedding / audio-in) from\n * the model picker. `'all'` keeps every backend and every model for non-chat\n * / scheduled surfaces.\n */\n context?: 'chat' | 'all'\n /**\n * Trigger layout. `'responsive'` (the default) lays the pickers out in a\n * row on wider screens and uses a compact gear on narrow screens. `'combined'`\n * collapses the pickers behind one\n * labeled trigger reading the current selection (`harness · model · effort`).\n * `'gear'` uses an anonymous gear icon instead of the label; `'inline'` lays\n * the pickers out in a row. `'responsive'` uses the labeled trigger on wider\n * screens and the compact gear on narrow screens so Send stays reachable.\n */\n layout?: 'inline' | 'gear' | 'combined' | 'responsive'\n /**\n * Where the pickers open. `'down'` pins them downward for a composer floating\n * in open space (the centered entry surface) so a tall menu can't flip up over\n * the heading. Defaults to `'auto'` (collision-aware).\n */\n menuPlacement?: 'auto' | 'down'\n /**\n * Restrict the model catalog to what the selected harness can run instead of\n * letting an incompatible model snap the harness. ON by default: a workspace\n * harness is sandbox-bound, and a silent swap tears down and recreates the\n * sandbox. Turn OFF only on a model-first surface with no bound sandbox.\n */\n filterModelsToHarness?: boolean\n}\n\n/**\n * The chat copilot must never offer `cli-base`: it is a shell-only backend with\n * no conversational agent. sandbox-ui's `context='chat'` drops it from the\n * default set, but an explicit `available` list (a plan-filtered one may include\n * `cli-base`) overrides that trim. So strip it here at the data layer whenever\n * the surface is chat — no `cli-base` ever reaches render.\n */\nfunction harnessesForContext(\n available: ReadonlyArray<HarnessType> | undefined,\n context: 'chat' | 'all',\n): ReadonlyArray<HarnessType> | undefined {\n if (!available) return available\n if (context !== 'chat') return available\n return available.filter((h) => h !== 'cli-base')\n}\n\n/**\n * The model-side twin of the harness trim above. Products hand the picker the\n * router's raw `/v1/models`, which lists every routeable endpoint — image\n * generators, TTS voices, embedding models, audio-in transcription. None of\n * them can serve a chat turn, and on the live catalogue that is 35 of 504\n * entries a user could pick and get a broken turn from.\n *\n * `context: 'all'` keeps the full list for non-chat surfaces.\n *\n * The selected model is never filtered out, whatever its modalities say: a\n * product may deliberately pin something exotic, and a picker whose own value\n * is missing from its list renders as blank.\n *\n * That escape hatch has to recognise BOTH spellings of the id. A product stores\n * whatever `onChange` handed it, and `onChange` hands back the canonical\n * provider-prefixed form — so a selection pinned before this trim existed is\n * stored as `openai/gpt-image-1` while the catalog entry's own `id` is the bare\n * `gpt-image-1`. Matching only the bare form drops exactly the model the guard\n * exists to keep, and the picker renders unset, which reads as data loss.\n */\nfunction modelsForContext(\n models: ModelInfo[],\n selectedId: string,\n context: 'chat' | 'all',\n): ModelInfo[] {\n if (context !== 'chat') return models\n return models.filter(\n (m) =>\n m.id === selectedId ||\n canonicalModelId(m) === selectedId ||\n isChatCapableModel(m as unknown as RouterModel),\n )\n}\n\n/**\n * The composer's agent-identity control: profile, backend, model, and\n * reasoning effort, over sandbox-ui's `AgentSessionControls`.\n *\n * This is an ADAPTER, not a second implementation. sandbox-ui owns the control\n * and the harness↔model coherence policy (the catalog is filtered to the\n * selected harness, the effort re-clamps to what the harness/model pair\n * supports, and a harness that ignores a picker is marked with its dead control\n * disabled). What lives here is the app-shell wiring every product otherwise\n * re-derives:\n *\n * - MODEL-ID BOUNDARY. Products store bare ids (what a chat send body expects)\n * while the picker matches on canonical provider-prefixed ids. The canonical\n * form goes in; the canonical id comes back out to `onChange`.\n * - HARNESS-SNAP SUPPRESSION. The shared control snaps the model whenever the\n * harness changes, calling the model `onChange` synchronously with a default\n * compatible model. A product that remembers a per-harness pick must ignore\n * that snap, or it both discards the remembered pick and persists it under\n * the wrong harness's key. The guard is armed for exactly the synchronous\n * window of a harness change and disarmed on the next microtask, so it can\n * never stay stuck — a later genuine pick is never swallowed, even when the\n * harness change was a no-op.\n * - CHAT-CONTEXT TRIM. On a chat surface, `cli-base` never reaches the harness\n * picker and a non-chat model never reaches the model picker. Products feed\n * the picker the router's raw model list, where 35 of 504 entries (image,\n * TTS, embeddings, audio-in transcription) cannot serve a chat turn at all.\n *\n * Behavioral modes (a plan toggle) are NOT agent identity and do not belong\n * here — they dock on the other side of the composer via `controls`.\n */\nexport function ComposerAgentControls({\n model,\n harness,\n profile,\n effort,\n className,\n context = 'chat',\n layout = 'responsive',\n menuPlacement,\n filterModelsToHarness = true,\n}: ComposerAgentControlsProps) {\n const canonicalSelected = useMemo(() => {\n const selected = model.models.find((m) => m.id === model.value)\n if (selected) return canonicalModelId(selected)\n // Already canonical, or the catalog has not loaded — pass through unchanged\n // so a provider-prefixed id still matches once models arrive.\n return model.value\n }, [model.value, model.models])\n\n const offeredModels = useMemo(\n () => modelsForContext(model.models, model.value, context),\n [model.models, model.value, context],\n )\n\n const harnessSwitching = useRef(false)\n const harnessOnChange = harness?.onChange\n const onHarnessChange = useCallback(\n (next: HarnessType) => {\n harnessSwitching.current = true\n harnessOnChange?.(next)\n queueMicrotask(() => {\n harnessSwitching.current = false\n })\n },\n [harnessOnChange],\n )\n\n const modelOnChange = model.onChange\n const onModelChange = useCallback(\n (canonical: string) => {\n if (harnessSwitching.current) return\n modelOnChange(canonical)\n },\n [modelOnChange],\n )\n\n const sharedProps = {\n context,\n menuPlacement,\n filterModelsToHarness,\n model: {\n value: canonicalSelected,\n onChange: onModelChange,\n models: offeredModels,\n loading: model.loading,\n disabled: model.disabled,\n },\n harness: harness\n ? {\n value: harness.value,\n onChange: onHarnessChange,\n available: harnessesForContext(harness.available, context),\n disabled: harness.disabled,\n locked: harness.locked,\n lockReason: harness.lockReason,\n onNewChat: harness.onNewChat,\n }\n : undefined,\n profile,\n reasoning: effort\n ? {\n value: effort.value,\n onChange: effort.onChange,\n available: effort.available,\n disabled: effort.disabled,\n }\n : undefined,\n }\n\n if (layout === 'responsive') {\n return (\n <>\n <AgentSessionControls\n {...sharedProps}\n className={className ? `hidden sm:flex ${className}` : 'hidden sm:flex'}\n layout=\"inline\"\n />\n <AgentSessionControls\n {...sharedProps}\n className={className ? `flex sm:hidden ${className}` : 'flex sm:hidden'}\n layout=\"gear\"\n />\n </>\n )\n }\n\n return (\n <AgentSessionControls\n className={className}\n {...sharedProps}\n layout={layout}\n />\n )\n}\n","import { useState, type ReactNode } from 'react'\nimport { AgentComposer } from '@tangle-network/sandbox-ui/chat'\nimport { ATTACHMENT_ACCEPT, useComposerAttachments } from '../web-react/use-composer-attachments'\nimport type { UseFileMentionsResult } from '../web-react/use-file-mentions'\nimport type { ChatAttachmentInput, FileMention } from '../chat-routes/wire'\nimport {\n ComposerAgentControls,\n type ComposerAgentControlsProps,\n} from './composer-agent-controls'\nimport { ComposerModeControls } from './composer-mode-controls'\nimport type { ComposerPlanModeSelection } from './types'\n\nexport interface EntryComposerProps {\n /** The one line above the input. Domain copy — always a product parameter. */\n heading?: ReactNode\n /** Optional supporting line under the heading. */\n subheading?: ReactNode\n placeholder?: string\n initialValue?: string\n sendLabel?: string\n disabled?: boolean\n /**\n * Agent identity (profile/backend/model/effort). Pass it and the control row renders;\n * omit it and the composer ships without one. Omitting is a real choice for a\n * surface with nothing to choose — it should never be an oversight, which is\n * why this is one prop rather than a free-form slot a caller can forget.\n */\n agent?: ComposerAgentControlsProps\n /**\n * Product-specific behavioral controls docked on the LEFT. A mode is an\n * on/off switch, not a value picker, so it sits apart from agent identity.\n * For the standard plan-approval control, prefer `planMode`.\n */\n modes?: ReactNode\n /**\n * Standard plan-approval mode. Pass only when the selected backend supports\n * it; omitted means the control is not rendered.\n */\n planMode?: ComposerPlanModeSelection\n /**\n * Upload endpoint for staged attachments. Omit and the attach affordance is\n * hidden — a composer with no place to put a file must not offer one.\n */\n uploadUrl?: string\n accept?: string\n onAttachmentError?: (reason: string) => void\n onRejectFiles?: Parameters<typeof AgentComposer>[0]['onRejectFiles']\n /**\n * `@`-file mentions. The hook itself is shared (`useFileMentions`), but the\n * cold-box policy around it — whether pressing `@` is worth starting a\n * sandbox — is a per-surface product call, so the result is injected rather\n * than created here.\n */\n mentions?: UseFileMentionsResult\n mentionPopoverClassName?: string\n /** Rendered under the composer: suggestion pills, an onboarding nudge, a\n * disclaimer. All domain copy. */\n footer?: ReactNode\n /** Block submit until a persisted selection has resolved against the catalog,\n * so the first turn cannot go out under the wrong model. Defaults to true. */\n ready?: boolean\n onSubmit: (\n prompt: string,\n attachments: ChatAttachmentInput[],\n mentions: FileMention[],\n ) => void\n className?: string\n composerClassName?: string\n /** Max width of the centered column. Defaults to the fleet's 820px. */\n maxWidth?: number\n}\n\n/**\n * `EntryComposer` — the centered \"what do you want to work on?\" surface a\n * product shows before a conversation exists (a new thread, an empty session,\n * a workspace overview).\n *\n * It exists because three products each grew their own, and they drifted into\n * three different capability sets rather than three different looks: one lost\n * the attach button, one lost the model and effort pickers entirely, one\n * hand-rolled a `<textarea>` and wired pickers from three separate packages\n * into one row. The composer, the pickers, the attachment queue and the mention\n * index were all ALREADY shared — what was not shared was the assembly, so\n * every product re-derived which controls an entry surface gets, and each\n * re-derivation dropped a different one.\n *\n * Mechanism lives here: layout, the attachment queue, the mention wiring, the\n * submit gate (nothing sends while an upload is pending or failed, or before\n * the model selection has resolved). Domain stays a parameter: `heading`,\n * `placeholder`, `footer` (suggestion pills, disclaimers) and the selections\n * themselves.\n *\n * The docked in-thread composer is NOT this component — it renders\n * `AgentComposer` directly with the same {@link ComposerAgentControls}, because\n * a docked composer has a transcript above it and no hero layout.\n */\nexport function EntryComposer({\n heading,\n subheading,\n placeholder,\n initialValue = '',\n sendLabel,\n disabled,\n agent,\n modes,\n planMode,\n uploadUrl,\n accept = ATTACHMENT_ACCEPT,\n onAttachmentError,\n onRejectFiles,\n mentions,\n mentionPopoverClassName,\n footer,\n ready = true,\n onSubmit,\n className,\n composerClassName,\n maxWidth = 820,\n}: EntryComposerProps) {\n const [value, setValue] = useState(initialValue)\n const attachments = useComposerAttachments({\n uploadUrl,\n enabled: !!uploadUrl,\n onReject: onAttachmentError,\n onError: onAttachmentError,\n })\n\n function submit(prompt: string) {\n if (!ready || disabled) return\n const next = prompt.trim()\n // A pending or failed upload must not send: the turn would reference an\n // attachment the store does not have. Surface why rather than no-op'ing.\n if (attachments.hasPending || attachments.hasError) {\n if (attachments.blockReason) onAttachmentError?.(attachments.blockReason)\n return\n }\n if (!next && attachments.references.length === 0) return\n onSubmit(next, attachments.references, mentions?.mentions ?? [])\n setValue('')\n attachments.clear()\n mentions?.clearMentions()\n }\n\n return (\n <div\n className={\n className ??\n 'relative flex flex-1 flex-col items-center justify-center overflow-hidden bg-background px-5'\n }\n >\n <div className=\"w-full\" style={{ maxWidth }}>\n {heading ? (\n <h2 className=\"mb-4 text-center text-[1.75rem] font-medium tracking-tight text-foreground\">\n {heading}\n </h2>\n ) : null}\n {subheading ? (\n <p className=\"mx-auto mb-9 max-w-md text-center text-sm leading-relaxed text-muted-foreground\">\n {subheading}\n </p>\n ) : (\n heading ? <div className=\"mb-7\" /> : null\n )}\n <AgentComposer\n className={composerClassName ?? 'vt-composer'}\n value={value}\n onChange={setValue}\n onSubmit={() => submit(value)}\n placeholder={placeholder}\n sendLabel={sendLabel}\n disabled={disabled}\n autoFocus\n canSubmitAttachmentsOnly\n accept={accept}\n attachments={attachments.composerFiles}\n onAttach={uploadUrl ? (files) => void attachments.addFiles(files) : undefined}\n onRejectFiles={onRejectFiles}\n onRemoveFile={attachments.removeAttachment}\n onRetryFile={attachments.retry}\n mention={\n mentions\n ? {\n ...mentions.mention,\n // Sidebar tone + hairline so the popover reads as app chrome\n // (the same treatment the picker menus get) instead of the\n // brighter overlay tone the component defaults to.\n popoverClassName:\n mentionPopoverClassName ??\n 'bg-surface-container-low border-[var(--md3-outline-variant)]',\n }\n : undefined\n }\n controls={modes ?? <ComposerModeControls planMode={planMode} />}\n trailing={\n agent ? <ComposerAgentControls menuPlacement=\"down\" {...agent} /> : undefined\n }\n />\n {footer ? <div className=\"mt-7\">{footer}</div> : null}\n </div>\n </div>\n )\n}\n","import { ListChecks } from 'lucide-react'\nimport { cn } from '@tangle-network/sandbox-ui/utils'\nimport type { ComposerPlanModeSelection } from './types'\n\nexport interface ComposerModeControlsProps {\n /**\n * Plan-approval mode. Products pass it only when the selected backend can\n * propose a plan and wait for approval; omitted means nothing renders.\n */\n planMode?: ComposerPlanModeSelection\n}\n\n/**\n * The shared plan-mode toggle for the left side of an agent composer.\n * Plan mode is a behavioral switch, not part of the profile/backend/model/\n * thinking identity controls on the right.\n */\nexport function ComposerModeControls({ planMode }: ComposerModeControlsProps) {\n if (!planMode) return null\n\n return (\n <button\n type=\"button\"\n aria-pressed={planMode.enabled}\n disabled={planMode.saving}\n onClick={() => planMode.setEnabled(!planMode.enabled)}\n title=\"Plan mode: the agent proposes a plan you approve before it executes\"\n className={cn(\n 'inline-flex h-7 items-center gap-1 rounded-full border px-2.5 text-xs transition-colors',\n planMode.enabled\n ? 'border-primary/50 bg-primary/10 text-primary'\n : 'border-border bg-transparent text-muted-foreground hover:text-foreground',\n planMode.saving && 'opacity-60',\n )}\n >\n <ListChecks className=\"h-3.5 w-3.5\" />\n Plan\n </button>\n )\n}\n"],"mappings":";;;;;;;;;;;;AAAA,SAAS,aAAa,SAAS,cAAc;AAC7C;AAAA,EACE;AAAA,OAEK;AACP,SAAS,wBAAwB;AAsQ3B,mBACE,KADF;AA1JN,SAAS,oBACP,WACA,SACwC;AACxC,MAAI,CAAC,UAAW,QAAO;AACvB,MAAI,YAAY,OAAQ,QAAO;AAC/B,SAAO,UAAU,OAAO,CAAC,MAAM,MAAM,UAAU;AACjD;AAsBA,SAAS,iBACP,QACA,YACA,SACa;AACb,MAAI,YAAY,OAAQ,QAAO;AAC/B,SAAO,OAAO;AAAA,IACZ,CAAC,MACC,EAAE,OAAO,cACT,iBAAiB,CAAC,MAAM,cACxB,mBAAmB,CAA2B;AAAA,EAClD;AACF;AAgCO,SAAS,sBAAsB;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,SAAS;AAAA,EACT;AAAA,EACA,wBAAwB;AAC1B,GAA+B;AAC7B,QAAM,oBAAoB,QAAQ,MAAM;AACtC,UAAM,WAAW,MAAM,OAAO,KAAK,CAAC,MAAM,EAAE,OAAO,MAAM,KAAK;AAC9D,QAAI,SAAU,QAAO,iBAAiB,QAAQ;AAG9C,WAAO,MAAM;AAAA,EACf,GAAG,CAAC,MAAM,OAAO,MAAM,MAAM,CAAC;AAE9B,QAAM,gBAAgB;AAAA,IACpB,MAAM,iBAAiB,MAAM,QAAQ,MAAM,OAAO,OAAO;AAAA,IACzD,CAAC,MAAM,QAAQ,MAAM,OAAO,OAAO;AAAA,EACrC;AAEA,QAAM,mBAAmB,OAAO,KAAK;AACrC,QAAM,kBAAkB,SAAS;AACjC,QAAM,kBAAkB;AAAA,IACtB,CAAC,SAAsB;AACrB,uBAAiB,UAAU;AAC3B,wBAAkB,IAAI;AACtB,qBAAe,MAAM;AACnB,yBAAiB,UAAU;AAAA,MAC7B,CAAC;AAAA,IACH;AAAA,IACA,CAAC,eAAe;AAAA,EAClB;AAEA,QAAM,gBAAgB,MAAM;AAC5B,QAAM,gBAAgB;AAAA,IACpB,CAAC,cAAsB;AACrB,UAAI,iBAAiB,QAAS;AAC9B,oBAAc,SAAS;AAAA,IACzB;AAAA,IACA,CAAC,aAAa;AAAA,EAChB;AAEA,QAAM,cAAc;AAAA,IAClB;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO;AAAA,MACL,OAAO;AAAA,MACP,UAAU;AAAA,MACV,QAAQ;AAAA,MACR,SAAS,MAAM;AAAA,MACf,UAAU,MAAM;AAAA,IAClB;AAAA,IACA,SAAS,UACL;AAAA,MACE,OAAO,QAAQ;AAAA,MACf,UAAU;AAAA,MACV,WAAW,oBAAoB,QAAQ,WAAW,OAAO;AAAA,MACzD,UAAU,QAAQ;AAAA,MAClB,QAAQ,QAAQ;AAAA,MAChB,YAAY,QAAQ;AAAA,MACpB,WAAW,QAAQ;AAAA,IACrB,IACA;AAAA,IACJ;AAAA,IACA,WAAW,SACP;AAAA,MACE,OAAO,OAAO;AAAA,MACd,UAAU,OAAO;AAAA,MACjB,WAAW,OAAO;AAAA,MAClB,UAAU,OAAO;AAAA,IACnB,IACA;AAAA,EACN;AAEA,MAAI,WAAW,cAAc;AAC3B,WACE,iCACE;AAAA;AAAA,QAAC;AAAA;AAAA,UACE,GAAG;AAAA,UACJ,WAAW,YAAY,kBAAkB,SAAS,KAAK;AAAA,UACvD,QAAO;AAAA;AAAA,MACT;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACE,GAAG;AAAA,UACJ,WAAW,YAAY,kBAAkB,SAAS,KAAK;AAAA,UACvD,QAAO;AAAA;AAAA,MACT;AAAA,OACF;AAAA,EAEJ;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACC,GAAG;AAAA,MACJ;AAAA;AAAA,EACF;AAEJ;;;ACjSA,SAAS,gBAAgC;AACzC,SAAS,qBAAqB;;;ACD9B,SAAS,kBAAkB;AAC3B,SAAS,UAAU;AAoBf,SAcE,OAAAA,MAdF,QAAAC,aAAA;AAJG,SAAS,qBAAqB,EAAE,SAAS,GAA8B;AAC5E,MAAI,CAAC,SAAU,QAAO;AAEtB,SACE,gBAAAA;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,gBAAc,SAAS;AAAA,MACvB,UAAU,SAAS;AAAA,MACnB,SAAS,MAAM,SAAS,WAAW,CAAC,SAAS,OAAO;AAAA,MACpD,OAAM;AAAA,MACN,WAAW;AAAA,QACT;AAAA,QACA,SAAS,UACL,iDACA;AAAA,QACJ,SAAS,UAAU;AAAA,MACrB;AAAA,MAEA;AAAA,wBAAAD,KAAC,cAAW,WAAU,eAAc;AAAA,QAAE;AAAA;AAAA;AAAA,EAExC;AAEJ;;;AD+GM,SAEI,OAAAE,MAFJ,QAAAC,aAAA;AAtDC,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA,eAAe;AAAA,EACf;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AACb,GAAuB;AACrB,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,YAAY;AAC/C,QAAM,cAAc,uBAAuB;AAAA,IACzC;AAAA,IACA,SAAS,CAAC,CAAC;AAAA,IACX,UAAU;AAAA,IACV,SAAS;AAAA,EACX,CAAC;AAED,WAAS,OAAO,QAAgB;AAC9B,QAAI,CAAC,SAAS,SAAU;AACxB,UAAM,OAAO,OAAO,KAAK;AAGzB,QAAI,YAAY,cAAc,YAAY,UAAU;AAClD,UAAI,YAAY,YAAa,qBAAoB,YAAY,WAAW;AACxE;AAAA,IACF;AACA,QAAI,CAAC,QAAQ,YAAY,WAAW,WAAW,EAAG;AAClD,aAAS,MAAM,YAAY,YAAY,UAAU,YAAY,CAAC,CAAC;AAC/D,aAAS,EAAE;AACX,gBAAY,MAAM;AAClB,cAAU,cAAc;AAAA,EAC1B;AAEA,SACE,gBAAAD;AAAA,IAAC;AAAA;AAAA,MACC,WACE,aACA;AAAA,MAGF,0BAAAC,MAAC,SAAI,WAAU,UAAS,OAAO,EAAE,SAAS,GACvC;AAAA,kBACC,gBAAAD,KAAC,QAAG,WAAU,8EACX,mBACH,IACE;AAAA,QACH,aACC,gBAAAA,KAAC,OAAE,WAAU,mFACV,sBACH,IAEA,UAAU,gBAAAA,KAAC,SAAI,WAAU,QAAO,IAAK;AAAA,QAEvC,gBAAAA;AAAA,UAAC;AAAA;AAAA,YACC,WAAW,qBAAqB;AAAA,YAChC;AAAA,YACA,UAAU;AAAA,YACV,UAAU,MAAM,OAAO,KAAK;AAAA,YAC5B;AAAA,YACA;AAAA,YACA;AAAA,YACA,WAAS;AAAA,YACT,0BAAwB;AAAA,YACxB;AAAA,YACA,aAAa,YAAY;AAAA,YACzB,UAAU,YAAY,CAAC,UAAU,KAAK,YAAY,SAAS,KAAK,IAAI;AAAA,YACpE;AAAA,YACA,cAAc,YAAY;AAAA,YAC1B,aAAa,YAAY;AAAA,YACzB,SACE,WACI;AAAA,cACE,GAAG,SAAS;AAAA;AAAA;AAAA;AAAA,cAIZ,kBACE,2BACA;AAAA,YACJ,IACA;AAAA,YAEN,UAAU,SAAS,gBAAAA,KAAC,wBAAqB,UAAoB;AAAA,YAC7D,UACE,QAAQ,gBAAAA,KAAC,yBAAsB,eAAc,QAAQ,GAAG,OAAO,IAAK;AAAA;AAAA,QAExE;AAAA,QACC,SAAS,gBAAAA,KAAC,SAAI,WAAU,QAAQ,kBAAO,IAAS;AAAA,SACnD;AAAA;AAAA,EACF;AAEJ;","names":["jsx","jsxs","jsx","jsxs"]}
1
+ {"version":3,"sources":["../../src/chat-react/composer-agent-controls.tsx","../../src/chat-react/entry-composer.tsx","../../src/chat-react/composer-mode-controls.tsx"],"sourcesContent":["import { useCallback, useMemo, useRef } from 'react'\nimport {\n AgentSessionControls,\n type AgentSessionProfileControl,\n} from '@tangle-network/sandbox-ui/chat'\nimport { canonicalModelId } from '@tangle-network/sandbox-ui/dashboard'\nimport type { ModelInfo } from '@tangle-network/sandbox-ui/dashboard'\n// Browser-safe: model-catalog.ts imports nothing. Taken from the source module\n// rather than the `/catalog` subpath so this stays one build graph.\nimport { isChatCapableModel, type RouterModel } from '../runtime/model-catalog'\nimport type { HarnessType, ReasoningEffort, ReasoningLevel } from './types'\n\n/**\n * The model half of the composer's agent identity. `models` is the catalog the\n * picker renders; `value` may be a bare or a canonical (provider-prefixed) id —\n * the adapter maps it to canonical form for the picker and hands the canonical\n * id back to `onChange`, so a product that stores bare ids only has to accept\n * canonical ones in its setter.\n */\nexport interface ComposerModelSelection {\n value: string\n onChange: (next: string) => void\n models: ModelInfo[]\n loading?: boolean\n disabled?: boolean\n}\n\nexport interface ComposerHarnessSelection {\n value: HarnessType\n onChange: (next: HarnessType) => void\n /** Restrict the offered backends. Omit to take the context default. */\n available?: ReadonlyArray<HarnessType>\n disabled?: boolean\n /**\n * A thread's sidecar session is pinned to one harness at its first message,\n * so once the thread has messages the picker locks. With `onNewChat` the\n * locked chip becomes the informative lock (explains the binding + a button\n * to start a fresh session on another backend).\n */\n locked?: boolean\n lockReason?: string\n onNewChat?: () => void\n}\n\nexport interface ComposerEffortSelection {\n value: ReasoningLevel\n onChange: (next: ReasoningLevel) => void\n /**\n * Depths this product's backend actually applies. Omit for the default set.\n * `auto` is NOT a member: the picker adds that sentinel itself, so listing it\n * here would offer it twice.\n */\n available?: ReadonlyArray<ReasoningEffort>\n disabled?: boolean\n}\n\n/**\n * A selectable named bundle of persona and tool groups. The product resolves\n * the selected id to the runtime AgentProfile before creating or continuing a\n * session; the composer only needs the display catalog and persistence hooks.\n */\nexport type ComposerProfileSelection = AgentSessionProfileControl\n\nexport interface ComposerAgentControlsProps {\n model: ComposerModelSelection\n /** Omit on a router-backed composer: a direct model call has no CLI backend,\n * so the picker would be inert. Model + effort still render. */\n harness?: ComposerHarnessSelection\n /** Universal named agent profile: persona + tool groups, independent of backend. */\n profile?: ComposerProfileSelection\n effort?: ComposerEffortSelection\n className?: string\n /**\n * Which surface the controls live on. `'chat'` (the default) drops the\n * shell-only `cli-base` backend — it has no conversational agent — and tells\n * the shared control to use its chat-only default set. It also drops models\n * that cannot serve a text turn (image / TTS / embedding / audio-in) from\n * the model picker. `'all'` keeps every backend and every model for non-chat\n * / scheduled surfaces.\n */\n context?: 'chat' | 'all'\n /**\n * Trigger layout. `'responsive'` (the default) lays the pickers out in a\n * row on wider screens and uses a compact gear on narrow screens. `'combined'`\n * collapses the pickers behind one\n * labeled trigger reading the current selection (`harness · model · effort`).\n * `'gear'` uses an anonymous gear icon instead of the label; `'inline'` lays\n * the pickers out in a row. `'responsive'` uses the labeled trigger on wider\n * screens and the compact gear on narrow screens so Send stays reachable.\n */\n layout?: 'inline' | 'gear' | 'combined' | 'responsive'\n /**\n * Where the pickers open. `'down'` pins them downward for a composer floating\n * in open space (the centered entry surface) so a tall menu can't flip up over\n * the heading. Defaults to `'auto'` (collision-aware).\n */\n menuPlacement?: 'auto' | 'down'\n /**\n * Restrict the model catalog to what the selected harness can run instead of\n * letting an incompatible model snap the harness. ON by default: a workspace\n * harness is sandbox-bound, and a silent swap tears down and recreates the\n * sandbox. Turn OFF only on a model-first surface with no bound sandbox.\n */\n filterModelsToHarness?: boolean\n}\n\n/**\n * The chat copilot must never offer `cli-base`: it is a shell-only backend with\n * no conversational agent. sandbox-ui's `context='chat'` drops it from the\n * default set, but an explicit `available` list (a plan-filtered one may include\n * `cli-base`) overrides that trim. So strip it here at the data layer whenever\n * the surface is chat — no `cli-base` ever reaches render.\n */\nfunction harnessesForContext(\n available: ReadonlyArray<HarnessType> | undefined,\n context: 'chat' | 'all',\n): ReadonlyArray<HarnessType> | undefined {\n if (!available) return available\n if (context !== 'chat') return available\n return available.filter((h) => h !== 'cli-base')\n}\n\n/**\n * The model-side twin of the harness trim above. Products hand the picker the\n * router's raw `/v1/models`, which lists every routeable endpoint — image\n * generators, TTS voices, embedding models, audio-in transcription. None of\n * them can serve a chat turn, and on the live catalogue that is 35 of 504\n * entries a user could pick and get a broken turn from.\n *\n * `context: 'all'` keeps the full list for non-chat surfaces.\n *\n * The selected model is never filtered out, whatever its modalities say: a\n * product may deliberately pin something exotic, and a picker whose own value\n * is missing from its list renders as blank.\n *\n * That escape hatch has to recognise BOTH spellings of the id. A product stores\n * whatever `onChange` handed it, and `onChange` hands back the canonical\n * provider-prefixed form — so a selection pinned before this trim existed is\n * stored as `openai/gpt-image-1` while the catalog entry's own `id` is the bare\n * `gpt-image-1`. Matching only the bare form drops exactly the model the guard\n * exists to keep, and the picker renders unset, which reads as data loss.\n */\nfunction modelsForContext(\n models: ModelInfo[],\n selectedId: string,\n context: 'chat' | 'all',\n): ModelInfo[] {\n if (context !== 'chat') return models\n return models.filter(\n (m) =>\n m.id === selectedId ||\n canonicalModelId(m) === selectedId ||\n isChatCapableModel(m as unknown as RouterModel),\n )\n}\n\n/**\n * The composer's agent-identity control: profile, backend, model, and\n * reasoning effort, over sandbox-ui's `AgentSessionControls`.\n *\n * This is an ADAPTER, not a second implementation. sandbox-ui owns the control\n * and the harness↔model coherence policy (the catalog is filtered to the\n * selected harness, the effort re-clamps to what the harness/model pair\n * supports, and a harness that ignores a picker is marked with its dead control\n * disabled). What lives here is the app-shell wiring every product otherwise\n * re-derives:\n *\n * - MODEL-ID BOUNDARY. Products store bare ids (what a chat send body expects)\n * while the picker matches on canonical provider-prefixed ids. The canonical\n * form goes in; the canonical id comes back out to `onChange`.\n * - HARNESS-SNAP SUPPRESSION. The shared control snaps the model whenever the\n * harness changes, calling the model `onChange` synchronously with a default\n * compatible model. A product that remembers a per-harness pick must ignore\n * that snap, or it both discards the remembered pick and persists it under\n * the wrong harness's key. The guard is armed for exactly the synchronous\n * window of a harness change and disarmed on the next microtask, so it can\n * never stay stuck — a later genuine pick is never swallowed, even when the\n * harness change was a no-op.\n * - CHAT-CONTEXT TRIM. On a chat surface, `cli-base` never reaches the harness\n * picker and a non-chat model never reaches the model picker. Products feed\n * the picker the router's raw model list, where 35 of 504 entries (image,\n * TTS, embeddings, audio-in transcription) cannot serve a chat turn at all.\n *\n * Behavioral modes (a plan toggle) are NOT agent identity and do not belong\n * here — they dock on the other side of the composer via `controls`.\n */\nexport function ComposerAgentControls({\n model,\n harness,\n profile,\n effort,\n className,\n context = 'chat',\n layout = 'responsive',\n menuPlacement,\n filterModelsToHarness = true,\n}: ComposerAgentControlsProps) {\n const canonicalSelected = useMemo(() => {\n const selected = model.models.find((m) => m.id === model.value)\n if (selected) return canonicalModelId(selected)\n // Already canonical, or the catalog has not loaded — pass through unchanged\n // so a provider-prefixed id still matches once models arrive.\n return model.value\n }, [model.value, model.models])\n\n const offeredModels = useMemo(\n () => modelsForContext(model.models, model.value, context),\n [model.models, model.value, context],\n )\n\n const harnessSwitching = useRef(false)\n const harnessOnChange = harness?.onChange\n const onHarnessChange = useCallback(\n (next: HarnessType) => {\n harnessSwitching.current = true\n harnessOnChange?.(next)\n queueMicrotask(() => {\n harnessSwitching.current = false\n })\n },\n [harnessOnChange],\n )\n\n const modelOnChange = model.onChange\n const onModelChange = useCallback(\n (canonical: string) => {\n if (harnessSwitching.current) return\n modelOnChange(canonical)\n },\n [modelOnChange],\n )\n\n const sharedProps = {\n context,\n menuPlacement,\n filterModelsToHarness,\n model: {\n value: canonicalSelected,\n onChange: onModelChange,\n models: offeredModels,\n loading: model.loading,\n disabled: model.disabled,\n },\n harness: harness\n ? {\n value: harness.value,\n onChange: onHarnessChange,\n available: harnessesForContext(harness.available, context),\n disabled: harness.disabled,\n locked: harness.locked,\n lockReason: harness.lockReason,\n onNewChat: harness.onNewChat,\n }\n : undefined,\n profile,\n reasoning: effort\n ? {\n value: effort.value,\n onChange: effort.onChange,\n available: effort.available,\n disabled: effort.disabled,\n }\n : undefined,\n }\n\n if (layout === 'responsive') {\n return (\n <>\n <AgentSessionControls\n {...sharedProps}\n className={className ? `hidden sm:flex ${className}` : 'hidden sm:flex'}\n layout=\"inline\"\n />\n <AgentSessionControls\n {...sharedProps}\n className={className ? `flex sm:hidden ${className}` : 'flex sm:hidden'}\n layout=\"gear\"\n />\n </>\n )\n }\n\n return (\n <AgentSessionControls\n className={className}\n {...sharedProps}\n layout={layout}\n />\n )\n}\n","import { useState, type ReactNode } from 'react'\nimport { AgentComposer } from '@tangle-network/sandbox-ui/chat'\nimport { ATTACHMENT_ACCEPT, useComposerAttachments } from '../web-react/use-composer-attachments'\nimport type { UseFileMentionsResult } from '../web-react/use-file-mentions'\nimport type { ChatAttachmentInput, FileMention } from '../chat-routes/wire'\nimport {\n ComposerAgentControls,\n type ComposerAgentControlsProps,\n} from './composer-agent-controls'\nimport { ComposerModeControls } from './composer-mode-controls'\nimport type { ComposerPlanModeSelection } from './types'\n\nexport interface EntryComposerProps {\n /** The one line above the input. Domain copy — always a product parameter. */\n heading?: ReactNode\n /** Optional supporting line under the heading. */\n subheading?: ReactNode\n placeholder?: string\n initialValue?: string\n sendLabel?: string\n disabled?: boolean\n /**\n * Agent identity (profile/backend/model/effort). Pass it and the control row renders;\n * omit it and the composer ships without one. Omitting is a real choice for a\n * surface with nothing to choose — it should never be an oversight, which is\n * why this is one prop rather than a free-form slot a caller can forget.\n */\n agent?: ComposerAgentControlsProps\n /**\n * Product-specific behavioral controls docked on the LEFT. A mode is an\n * on/off switch, not a value picker, so it sits apart from agent identity.\n * For the standard plan-approval control, prefer `planMode`.\n */\n modes?: ReactNode\n /**\n * Standard plan-approval mode. Pass only when the selected backend supports\n * it; omitted means the control is not rendered.\n */\n planMode?: ComposerPlanModeSelection\n /**\n * Upload endpoint for staged attachments. Omit and the attach affordance is\n * hidden — a composer with no place to put a file must not offer one.\n */\n uploadUrl?: string\n accept?: string\n onAttachmentError?: (reason: string) => void\n onRejectFiles?: Parameters<typeof AgentComposer>[0]['onRejectFiles']\n /**\n * `@`-file mentions. The hook itself is shared (`useFileMentions`), but the\n * cold-box policy around it — whether pressing `@` is worth starting a\n * sandbox — is a per-surface product call, so the result is injected rather\n * than created here.\n */\n mentions?: UseFileMentionsResult\n mentionPopoverClassName?: string\n /** Rendered under the composer: suggestion pills, an onboarding nudge, a\n * disclaimer. All domain copy. */\n footer?: ReactNode\n /** Block submit until a persisted selection has resolved against the catalog,\n * so the first turn cannot go out under the wrong model. Defaults to true. */\n ready?: boolean\n onSubmit: (\n prompt: string,\n attachments: ChatAttachmentInput[],\n mentions: FileMention[],\n ) => void\n className?: string\n composerClassName?: string\n /** Max width of the centered column. Defaults to the fleet's 820px. */\n maxWidth?: number\n}\n\n/**\n * `EntryComposer` — the centered \"what do you want to work on?\" surface a\n * product shows before a conversation exists (a new thread, an empty session,\n * a workspace overview).\n *\n * It exists because three products each grew their own, and they drifted into\n * three different capability sets rather than three different looks: one lost\n * the attach button, one lost the model and effort pickers entirely, one\n * hand-rolled a `<textarea>` and wired pickers from three separate packages\n * into one row. The composer, the pickers, the attachment queue and the mention\n * index were all ALREADY shared — what was not shared was the assembly, so\n * every product re-derived which controls an entry surface gets, and each\n * re-derivation dropped a different one.\n *\n * Mechanism lives here: layout, the attachment queue, the mention wiring, the\n * submit gate (nothing sends while an upload is pending or failed, or before\n * the model selection has resolved). Domain stays a parameter: `heading`,\n * `placeholder`, `footer` (suggestion pills, disclaimers) and the selections\n * themselves.\n *\n * The docked in-thread composer is NOT this component — it renders\n * `AgentComposer` directly with the same {@link ComposerAgentControls}, because\n * a docked composer has a transcript above it and no hero layout.\n */\nexport function EntryComposer({\n heading,\n subheading,\n placeholder,\n initialValue = '',\n sendLabel,\n disabled,\n agent,\n modes,\n planMode,\n uploadUrl,\n accept = ATTACHMENT_ACCEPT,\n onAttachmentError,\n onRejectFiles,\n mentions,\n mentionPopoverClassName,\n footer,\n ready = true,\n onSubmit,\n className,\n composerClassName,\n maxWidth = 820,\n}: EntryComposerProps) {\n const [value, setValue] = useState(initialValue)\n const attachments = useComposerAttachments({\n uploadUrl,\n enabled: !!uploadUrl,\n onReject: onAttachmentError,\n onError: onAttachmentError,\n })\n\n function submit(prompt: string) {\n if (!ready || disabled) return\n const next = prompt.trim()\n // A pending or failed upload must not send: the turn would reference an\n // attachment the store does not have. Surface why rather than no-op'ing.\n if (attachments.hasPending || attachments.hasError) {\n if (attachments.blockReason) onAttachmentError?.(attachments.blockReason)\n return\n }\n if (!next && attachments.references.length === 0) return\n onSubmit(next, attachments.references, mentions?.mentions ?? [])\n setValue('')\n attachments.clear()\n mentions?.clearMentions()\n }\n\n return (\n <div\n className={\n className ??\n 'relative flex flex-1 flex-col items-center justify-center overflow-hidden bg-background px-5'\n }\n >\n <div className=\"w-full\" style={{ maxWidth }}>\n {heading ? (\n <h2 className=\"mb-4 text-center text-[1.75rem] font-medium tracking-tight text-foreground\">\n {heading}\n </h2>\n ) : null}\n {subheading ? (\n <p className=\"mx-auto mb-9 max-w-md text-center text-sm leading-relaxed text-muted-foreground\">\n {subheading}\n </p>\n ) : (\n heading ? <div className=\"mb-7\" /> : null\n )}\n <AgentComposer\n className={composerClassName ?? 'vt-composer'}\n value={value}\n onChange={setValue}\n onSubmit={() => submit(value)}\n placeholder={placeholder}\n sendLabel={sendLabel}\n disabled={disabled}\n autoFocus\n canSubmitAttachmentsOnly\n accept={accept}\n attachments={attachments.composerFiles}\n onAttach={uploadUrl ? (files) => void attachments.addFiles(files) : undefined}\n onRejectFiles={onRejectFiles}\n onRemoveFile={attachments.removeAttachment}\n onRetryFile={attachments.retry}\n mention={\n mentions\n ? {\n ...mentions.mention,\n // Sidebar tone + hairline so the popover reads as app chrome\n // (the same treatment the picker menus get) instead of the\n // brighter overlay tone the component defaults to.\n popoverClassName:\n mentionPopoverClassName ??\n 'bg-surface-container-low border-[var(--md3-outline-variant)]',\n }\n : undefined\n }\n controls={modes ?? <ComposerModeControls planMode={planMode} />}\n trailing={\n agent ? <ComposerAgentControls menuPlacement=\"down\" {...agent} /> : undefined\n }\n />\n {footer ? <div className=\"mt-7\">{footer}</div> : null}\n </div>\n </div>\n )\n}\n","import { ListChecks } from 'lucide-react'\nimport { cn } from '@tangle-network/sandbox-ui/utils'\nimport type { ComposerPlanModeSelection } from './types'\n\nexport interface ComposerModeControlsProps {\n /**\n * Plan-approval mode. Products pass it only when the selected backend can\n * propose a plan and wait for approval; omitted means nothing renders.\n */\n planMode?: ComposerPlanModeSelection\n}\n\n/**\n * The shared plan-mode toggle for the left side of an agent composer.\n * Plan mode is a behavioral switch, not part of the profile/backend/model/\n * thinking identity controls on the right.\n */\nexport function ComposerModeControls({ planMode }: ComposerModeControlsProps) {\n if (!planMode) return null\n\n return (\n <button\n type=\"button\"\n aria-pressed={planMode.enabled}\n disabled={planMode.saving}\n onClick={() => planMode.setEnabled(!planMode.enabled)}\n title=\"Plan mode: the agent proposes a plan you approve before it executes\"\n className={cn(\n // Inset ring: this chip sits in a composer row that clips its overflow,\n // so an outward ring loses three of its four sides. Only the offset is\n // overridden — width and colour stay with the tokens.\n 'inline-flex h-7 items-center gap-1 rounded-full border px-2.5 text-xs transition-colors focus-visible:[outline-offset:-2px]',\n planMode.enabled\n ? 'border-primary/50 bg-primary/10 text-primary'\n : 'border-border bg-transparent text-muted-foreground hover:text-foreground',\n planMode.saving && 'opacity-60',\n )}\n >\n <ListChecks className=\"h-3.5 w-3.5\" />\n Plan\n </button>\n )\n}\n"],"mappings":"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","names":["jsx","jsxs","jsx","jsxs"]}
@@ -6,7 +6,7 @@ import { C as ChatMessagePart, a as ChatAttachmentPart, e as ChatMentionPart } f
6
6
  import { InteractionAnswerRoute, InteractionAnswerRouteOptions } from '../interactions/index.js';
7
7
  import { PersistedChatMessageForTurn } from '../stream/index.js';
8
8
  import { T as TurnEventStore } from '../turn-buffer-COMQUwO2.js';
9
- import { M as ModelFailoverAttempt } from '../failover-D-3UXXTb.js';
9
+ import { M as ModelFailoverAttempt, a as ModelFailoverExhaustedError } from '../failover-D-3UXXTb.js';
10
10
  export { D as DEFAULT_STALE_TURN_LOCK_GRACE_MS, a as DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS, R as ReconcileStaleTurnLockOptions, b as ReconcileStaleTurnLockResult, S as StaleTurnLockSandboxProbeResult, c as StaleTurnLockSessionProbeResult, r as reconcileStaleTurnLock } from '../stale-turn-lock-DucQzvXu.js';
11
11
  import { J as JsonRecord } from '../stream-normalizer-DKRxyVlZ.js';
12
12
  import { SandboxInstance } from '@tangle-network/sandbox';
@@ -785,8 +785,8 @@ declare function createChatTurnRoutes<TContext = void>(options: CreateChatTurnRo
785
785
  * platform flake that a same-model re-run recovers (8 hard cases: 7/8
786
786
  * delivered on the first pass, 8/8 with one re-run, at a cost of 1 extra turn
787
787
  * in 9). Default `0`, so the behavior is unchanged unless a product asks.
788
- * - A chain of length 1 costs nothing: one attempt, no extra call, no added
789
- * latency, byte-identical to no failover at all.
788
+ * - A responsive chain of length 1 costs no extra call or latency and remains
789
+ * byte-identical to no failover. A silent chain is now deliberately bounded.
790
790
  */
791
791
 
792
792
  /**
@@ -834,6 +834,10 @@ type OpenModelStream = (args: {
834
834
  model: string;
835
835
  /** 1 for the preferred model, 2 for the first fallback, and so on. */
836
836
  attempt: number;
837
+ /** Aborted when this attempt times out, is abandoned, or finishes. Forward
838
+ * this to `streamSandboxPrompt(..., { signal })` so a blocked transport is
839
+ * cancelled immediately rather than only receiving iterator.return(). */
840
+ signal: AbortSignal;
837
841
  }) => AsyncIterable<unknown> | Promise<AsyncIterable<unknown>>;
838
842
  /** Fired when a model is abandoned and the next one is about to be tried. */
839
843
  interface ModelFallbackInfo {
@@ -846,6 +850,19 @@ interface ModelFailoverStreamOptions {
846
850
  /** Preferred model first, then fallbacks in descending preference. */
847
851
  models: readonly string[];
848
852
  open: OpenModelStream;
853
+ /**
854
+ * Maximum time to open/start one model's event source, including its first
855
+ * iterator pull. Default 120 seconds, matching the sandbox provisioning
856
+ * ceiling so cold infrastructure startup is bounded independently from
857
+ * provider inference.
858
+ */
859
+ openTimeoutMs?: number;
860
+ /**
861
+ * Hard deadline from the source's first lifecycle event to its first
862
+ * answer-bearing event. Default 60 seconds. Processing/lifecycle events do
863
+ * not commit the attempt and do not extend this deadline.
864
+ */
865
+ firstResponseTimeoutMs?: number;
849
866
  /** Override the commit-point rule. Default {@link isCommittingSandboxEvent}. */
850
867
  isCommitting?: (event: unknown) => boolean;
851
868
  onFallback?: (info: ModelFallbackInfo) => void;
@@ -886,6 +903,24 @@ interface ModelFailoverStreamHandle {
886
903
  /** True when the preferred model did not serve — the attributability signal. */
887
904
  usedFallback(): boolean;
888
905
  }
906
+ /** Default ceiling for opening/starting one source through its first event. */
907
+ declare const DEFAULT_MODEL_STREAM_OPEN_TIMEOUT_MS = 120000;
908
+ /** Default ceiling for the first answer-bearing event after the source's first event. */
909
+ declare const DEFAULT_MODEL_FIRST_RESPONSE_TIMEOUT_MS = 60000;
910
+ /** Structured timeout codes surfaced by the producer on final exhaustion. */
911
+ type ModelAttemptTimeoutCode = 'model_stream_open_timeout' | 'provider_first_response_timeout';
912
+ /** Every configured model was exhausted and the final one timed out. */
913
+ declare class ModelFailoverTimeoutError extends ModelFailoverExhaustedError {
914
+ readonly code: ModelAttemptTimeoutCode;
915
+ readonly model: string;
916
+ readonly timeoutMs: number;
917
+ constructor(input: {
918
+ attempts: ModelFailoverAttempt[];
919
+ code: ModelAttemptTimeoutCode;
920
+ model: string;
921
+ timeoutMs: number;
922
+ });
923
+ }
889
924
  /**
890
925
  * Hard ceiling on same-model re-runs. A turn that comes back blank three times
891
926
  * running is not a flake this can retry away, and each pass costs a full
@@ -908,9 +943,11 @@ declare function resolveEmptyTurnRetries(value: number | undefined): number;
908
943
  * Zero added latency on the happy path: the preferred model is opened first and,
909
944
  * the moment it emits anything meaningful, its events flow straight through.
910
945
  *
911
- * @throws ModelFailoverExhaustedError when every model's upstream is down, and
946
+ * @throws ModelFailoverTimeoutError when the final model times out,
947
+ * ModelFailoverExhaustedError when every model's upstream is down, and
912
948
  * re-throws a non-outage error from the FIRST model without walking the
913
- * chain (both behaviors inherited from `runWithModelFailover`).
949
+ * chain (the latter behaviors are inherited from
950
+ * `runWithModelFailover`).
914
951
  */
915
952
  declare function streamWithModelFailover(options: ModelFailoverStreamOptions): ModelFailoverStreamHandle;
916
953
 
@@ -993,6 +1030,14 @@ interface SandboxChatProducerOptions {
993
1030
  /** Opt out of failover while still using {@link openEvents}. `false` collapses
994
1031
  * the chain to `model` alone. */
995
1032
  modelFailover?: false;
1033
+ /** Maximum time to open/start one model event source through its first event.
1034
+ * Default 120 seconds. This is separate from provider first-output latency
1035
+ * so cold sandbox startup is not mislabeled as an inference failure. */
1036
+ openTimeoutMs?: number;
1037
+ /** Hard deadline after the source's first lifecycle event for the first
1038
+ * answer-bearing event. Default 60 seconds. Processing/lifecycle events do
1039
+ * not extend it. */
1040
+ firstResponseTimeoutMs?: number;
996
1041
  /** Fired when a model is abandoned mid-chain (telemetry/alerting). The user
997
1042
  * already sees a transcript notice; this is for the operator. */
998
1043
  onModelFallback?: (info: ModelFallbackInfo) => void;
@@ -1121,6 +1166,12 @@ interface DetachedTurnOptions {
1121
1166
  fallbackModels?: SandboxChatProducerOptions['fallbackModels'];
1122
1167
  /** Opt out of failover while still using {@link openEvents}. */
1123
1168
  modelFailover?: false;
1169
+ /** Maximum time to open/start one model event source through its first event.
1170
+ * Forwarded to the producer; default 120 seconds. */
1171
+ openTimeoutMs?: SandboxChatProducerOptions['openTimeoutMs'];
1172
+ /** Hard deadline after the source's first lifecycle event for the first
1173
+ * answer-bearing event. Forwarded to the producer; default 60 seconds. */
1174
+ firstResponseTimeoutMs?: SandboxChatProducerOptions['firstResponseTimeoutMs'];
1124
1175
  /** Fired when a model is abandoned mid-chain (telemetry/alerting). */
1125
1176
  onModelFallback?: SandboxChatProducerOptions['onModelFallback'];
1126
1177
  /** The PREFERRED model. Recorded on the persisted assistant message + usage
@@ -1787,4 +1838,4 @@ interface PromoteAgentFilePartOptions {
1787
1838
  /** Promote a part of an agent file with optional byte limits and MIME type detection */
1788
1839
  declare function promoteAgentFilePart(options: PromoteAgentFilePartOptions): Promise<PromoteFilePartResult>;
1789
1840
 
1790
- export { type AssistantDraftSnapshot, type AssistantDraftStore, type AssistantDraftWriter, type AssistantDraftWriterOptions, type AssistantRowValues, type AttachmentPathArgs, type AttachmentPathCheck, type AttachmentReadResult, type AttachmentUploadAuthorization, type AttachmentWriteResult, type BuildDispatchPartsInput, ChatAttachmentKind, type ChatRouteDurableProjection, type ChatRouteDurableProjectionLogger, type ChatRouteEvent, type ChatTurnAuthorization, type ChatTurnAuthorizeArgs, type ChatTurnCompleteInput, ChatTurnFilePartInput, type ChatTurnGateResult, type ChatTurnHeartbeat, type ChatTurnInputPatch, type ChatTurnLifecycle, type ChatTurnLifecycleComplete, type ChatTurnLifecycleError, type ChatTurnLifecycleStart, type ChatTurnLock, type ChatTurnLockResult, type ChatTurnMessageStore, type ChatTurnModelAttribution, type ChatTurnModelFailover, ChatTurnPartInput, type ChatTurnProduceArgs, ChatTurnRequestPayload, type ChatTurnRouteProducer, type ChatTurnRoutes, type ChatTurnUsage, type CompletedSandboxTurnSource, type CreateAttachmentUploadRouteOptions, type CreateChatTurnRoutesOptions, type CreateUploadRouteOptions, type DetachedTurnFinal, type DetachedTurnOptions, type DetachedTurnParts, type DetachedTurnResult, type DispatchPartsOutcome, type DraftPersistenceTuning, type DraftStoredMessage, type EmptyTurnRetryInfo, type FilePartPromotionOutcome, MAX_EMPTY_TURN_RETRIES, type ModelFailoverStreamHandle, type ModelFailoverStreamOptions, type ModelFallbackInfo, type OpenModelStream, PROMOTE_MAX_FILE_BYTES, type PromoteAgentFilePartOptions, type PromoteFilePartResult, PromptInputPart, type RawAgentFilePart, type ReadAttachmentFn, type ReadCompletedSandboxTurnOptions, type ReadSandboxMentionFn, type ResolveChatAttachmentsOptions, type ResolveChatAttachmentsResult, type SandboxChatProducerOptions, type SandboxUploadSink, UPLOAD_INLINE_MAX_BYTES, UPLOAD_MAX_FILE_BYTES, type UploadAuthorization, type UploadedChatFile, type WriteAttachmentFn, assistantRowIdForTurn, buildDispatchParts, bytesToBase64, classifyTerminalFailure, createAssistantDraftWriter, createAttachmentUploadRoute, createChatTurnRoutes, createSandboxChatProducer, createUploadRoute, defaultValidateAttachmentPath, isCommittingSandboxEvent, isDraftContentEvent, normalizeChatPromptForSandbox, promoteAgentFilePart, readCompletedSandboxTurn, resolveChatAttachments, resolveEmptyTurnRetries, rowIdOf, runDetachedTurn, sanitizeUploadFilename, sniffMimeFromName, storeSupportsDraftPersistence, streamWithModelFailover, summarizeFailoverReason, withDurableChatProjection };
1841
+ export { type AssistantDraftSnapshot, type AssistantDraftStore, type AssistantDraftWriter, type AssistantDraftWriterOptions, type AssistantRowValues, type AttachmentPathArgs, type AttachmentPathCheck, type AttachmentReadResult, type AttachmentUploadAuthorization, type AttachmentWriteResult, type BuildDispatchPartsInput, ChatAttachmentKind, type ChatRouteDurableProjection, type ChatRouteDurableProjectionLogger, type ChatRouteEvent, type ChatTurnAuthorization, type ChatTurnAuthorizeArgs, type ChatTurnCompleteInput, ChatTurnFilePartInput, type ChatTurnGateResult, type ChatTurnHeartbeat, type ChatTurnInputPatch, type ChatTurnLifecycle, type ChatTurnLifecycleComplete, type ChatTurnLifecycleError, type ChatTurnLifecycleStart, type ChatTurnLock, type ChatTurnLockResult, type ChatTurnMessageStore, type ChatTurnModelAttribution, type ChatTurnModelFailover, ChatTurnPartInput, type ChatTurnProduceArgs, ChatTurnRequestPayload, type ChatTurnRouteProducer, type ChatTurnRoutes, type ChatTurnUsage, type CompletedSandboxTurnSource, type CreateAttachmentUploadRouteOptions, type CreateChatTurnRoutesOptions, type CreateUploadRouteOptions, DEFAULT_MODEL_FIRST_RESPONSE_TIMEOUT_MS, DEFAULT_MODEL_STREAM_OPEN_TIMEOUT_MS, type DetachedTurnFinal, type DetachedTurnOptions, type DetachedTurnParts, type DetachedTurnResult, type DispatchPartsOutcome, type DraftPersistenceTuning, type DraftStoredMessage, type EmptyTurnRetryInfo, type FilePartPromotionOutcome, MAX_EMPTY_TURN_RETRIES, type ModelAttemptTimeoutCode, type ModelFailoverStreamHandle, type ModelFailoverStreamOptions, ModelFailoverTimeoutError, type ModelFallbackInfo, type OpenModelStream, PROMOTE_MAX_FILE_BYTES, type PromoteAgentFilePartOptions, type PromoteFilePartResult, PromptInputPart, type RawAgentFilePart, type ReadAttachmentFn, type ReadCompletedSandboxTurnOptions, type ReadSandboxMentionFn, type ResolveChatAttachmentsOptions, type ResolveChatAttachmentsResult, type SandboxChatProducerOptions, type SandboxUploadSink, UPLOAD_INLINE_MAX_BYTES, UPLOAD_MAX_FILE_BYTES, type UploadAuthorization, type UploadedChatFile, type WriteAttachmentFn, assistantRowIdForTurn, buildDispatchParts, bytesToBase64, classifyTerminalFailure, createAssistantDraftWriter, createAttachmentUploadRoute, createChatTurnRoutes, createSandboxChatProducer, createUploadRoute, defaultValidateAttachmentPath, isCommittingSandboxEvent, isDraftContentEvent, normalizeChatPromptForSandbox, promoteAgentFilePart, readCompletedSandboxTurn, resolveChatAttachments, resolveEmptyTurnRetries, rowIdOf, runDetachedTurn, sanitizeUploadFilename, sniffMimeFromName, storeSupportsDraftPersistence, streamWithModelFailover, summarizeFailoverReason, withDurableChatProjection };
@@ -99,6 +99,7 @@ import {
99
99
  planToPersistedPart
100
100
  } from "../chunk-YJMCRXQQ.js";
101
101
  import {
102
+ ModelFailoverExhaustedError,
102
103
  buildModelChain,
103
104
  isUpstreamUnavailable,
104
105
  readHttpStatusHint,
@@ -883,7 +884,7 @@ function isCommittingSandboxEvent(event) {
883
884
  }
884
885
  return true;
885
886
  }
886
- if (type === "start" || type === "execution.started" || type === "status" || type === "model-processing" || type === "session.created" || type === "session.updated" || type === "session.idle" || type === "step-start" || type === "step-finish" || type === "turn" || type === "warning") {
887
+ if (type === "start" || type === "execution.started" || type === "status" || type === "model-processing" || type === "model.processing" || type === "session.created" || type === "session.updated" || type === "session.idle" || type === "step-start" || type === "step-finish" || type === "turn" || type === "warning") {
887
888
  return false;
888
889
  }
889
890
  return true;
@@ -912,6 +913,44 @@ function classifyTerminalFailure(event) {
912
913
  const code = asString(data?.errorCode) ?? asString(data?.code);
913
914
  return { outage, reason, ...code ? { code } : {} };
914
915
  }
916
+ var DEFAULT_MODEL_STREAM_OPEN_TIMEOUT_MS = 12e4;
917
+ var DEFAULT_MODEL_FIRST_RESPONSE_TIMEOUT_MS = 6e4;
918
+ var ModelFailoverTimeoutError = class extends ModelFailoverExhaustedError {
919
+ code;
920
+ model;
921
+ timeoutMs;
922
+ constructor(input) {
923
+ super(input.attempts);
924
+ this.name = "ModelFailoverTimeoutError";
925
+ this.code = input.code;
926
+ this.model = input.model;
927
+ this.timeoutMs = input.timeoutMs;
928
+ this.message = input.code === "model_stream_open_timeout" ? `Opening the model event source for ${input.model} exceeded ${input.timeoutMs}ms.` : `Model ${input.model} produced no first answer-bearing event within ${input.timeoutMs}ms.`;
929
+ }
930
+ };
931
+ function resolveTimeoutMs(value, fallback, label) {
932
+ if (value === void 0) return fallback;
933
+ if (!Number.isFinite(value) || value <= 0) {
934
+ throw new RangeError(`${label} must be a finite number greater than 0`);
935
+ }
936
+ return Math.max(1, Math.trunc(value));
937
+ }
938
+ var TIMED_OUT = /* @__PURE__ */ Symbol("model-attempt-timed-out");
939
+ function deadlineAfter(timeoutMs) {
940
+ let timer;
941
+ const promise = new Promise((resolve) => {
942
+ timer = setTimeout(() => resolve(TIMED_OUT), timeoutMs);
943
+ });
944
+ return {
945
+ promise,
946
+ clear: () => {
947
+ if (timer !== void 0) {
948
+ clearTimeout(timer);
949
+ timer = void 0;
950
+ }
951
+ }
952
+ };
953
+ }
915
954
  function isEmptyTerminalReceipt(event) {
916
955
  const record = asRecord(event);
917
956
  if (!record) return false;
@@ -940,24 +979,128 @@ async function closeIterator(iterator, log) {
940
979
  function streamWithModelFailover(options) {
941
980
  const committing = options.isCommitting ?? isCommittingSandboxEvent;
942
981
  const emptyTurnRetries = resolveEmptyTurnRetries(options.emptyTurnRetries);
982
+ const openTimeoutMs = resolveTimeoutMs(
983
+ options.openTimeoutMs,
984
+ DEFAULT_MODEL_STREAM_OPEN_TIMEOUT_MS,
985
+ "openTimeoutMs"
986
+ );
987
+ const firstResponseTimeoutMs = resolveTimeoutMs(
988
+ options.firstResponseTimeoutMs,
989
+ DEFAULT_MODEL_FIRST_RESPONSE_TIMEOUT_MS,
990
+ "firstResponseTimeoutMs"
991
+ );
943
992
  let serving;
944
993
  let trail = [];
945
994
  let fellBack = false;
946
995
  let attemptIndex = 0;
996
+ let finalTimeout;
947
997
  const drainOnce = async (model) => {
948
998
  attemptIndex += 1;
949
- const source = await options.open({ model, attempt: attemptIndex });
950
- const iterator = source[Symbol.asyncIterator]();
999
+ const controller = new AbortController();
1000
+ const abort = () => {
1001
+ if (!controller.signal.aborted) controller.abort();
1002
+ };
1003
+ const opening = Promise.resolve().then(() => options.open({
1004
+ model,
1005
+ attempt: attemptIndex,
1006
+ signal: controller.signal
1007
+ }));
1008
+ const openDeadline = deadlineAfter(openTimeoutMs);
1009
+ let opened;
1010
+ try {
1011
+ opened = await Promise.race([opening, openDeadline.promise]);
1012
+ } catch (err) {
1013
+ openDeadline.clear();
1014
+ abort();
1015
+ throw err;
1016
+ }
1017
+ if (opened === TIMED_OUT) {
1018
+ openDeadline.clear();
1019
+ abort();
1020
+ void opening.then(
1021
+ (lateSource) => {
1022
+ try {
1023
+ void closeIterator(lateSource[Symbol.asyncIterator](), options.log);
1024
+ } catch (err) {
1025
+ options.log?.("[chat-routes] closing a late model stream threw", {
1026
+ error: err instanceof Error ? err.message : String(err)
1027
+ });
1028
+ }
1029
+ },
1030
+ (err) => {
1031
+ options.log?.("[chat-routes] abandoned model stream open rejected after timeout", {
1032
+ error: err instanceof Error ? err.message : String(err)
1033
+ });
1034
+ }
1035
+ );
1036
+ return {
1037
+ outcome: {
1038
+ committed: false,
1039
+ error: `Opening the model event source for ${model} exceeded ${openTimeoutMs}ms`,
1040
+ errorCode: "model_stream_open_timeout",
1041
+ timeoutCode: "model_stream_open_timeout"
1042
+ },
1043
+ empty: false
1044
+ };
1045
+ }
1046
+ const source = opened;
1047
+ let iterator;
1048
+ try {
1049
+ iterator = source[Symbol.asyncIterator]();
1050
+ } catch (err) {
1051
+ openDeadline.clear();
1052
+ abort();
1053
+ throw err;
1054
+ }
951
1055
  const buffered = [];
1056
+ let next;
1057
+ try {
1058
+ next = await Promise.race([
1059
+ Promise.resolve(iterator.next()),
1060
+ openDeadline.promise
1061
+ ]);
1062
+ } catch (err) {
1063
+ openDeadline.clear();
1064
+ abort();
1065
+ void closeIterator(iterator, options.log);
1066
+ throw err;
1067
+ }
1068
+ openDeadline.clear();
1069
+ if (next === TIMED_OUT) {
1070
+ abort();
1071
+ void closeIterator(iterator, options.log);
1072
+ return {
1073
+ outcome: {
1074
+ committed: false,
1075
+ error: `Starting the model event source for ${model} exceeded ${openTimeoutMs}ms`,
1076
+ errorCode: "model_stream_open_timeout",
1077
+ timeoutCode: "model_stream_open_timeout"
1078
+ },
1079
+ empty: false
1080
+ };
1081
+ }
1082
+ if (next.done) {
1083
+ abort();
1084
+ return { outcome: { committed: true, buffered, iterator: null, abort }, empty: true };
1085
+ }
1086
+ const firstResponseDeadline = deadlineAfter(firstResponseTimeoutMs);
952
1087
  for (; ; ) {
953
- const next = await iterator.next();
954
- if (next.done) {
955
- return { outcome: { committed: true, buffered, iterator: null }, empty: true };
956
- }
957
1088
  const event = next.value;
958
- const failure = classifyTerminalFailure(event);
1089
+ let failure;
1090
+ let commits = false;
1091
+ try {
1092
+ failure = classifyTerminalFailure(event);
1093
+ commits = !failure && committing(event);
1094
+ } catch (err) {
1095
+ firstResponseDeadline.clear();
1096
+ abort();
1097
+ void closeIterator(iterator, options.log);
1098
+ throw err;
1099
+ }
959
1100
  if (failure?.outage) {
960
- await closeIterator(iterator, options.log);
1101
+ firstResponseDeadline.clear();
1102
+ abort();
1103
+ void closeIterator(iterator, options.log);
961
1104
  return {
962
1105
  outcome: {
963
1106
  committed: false,
@@ -968,18 +1111,65 @@ function streamWithModelFailover(options) {
968
1111
  };
969
1112
  }
970
1113
  buffered.push(event);
971
- if (failure) return { outcome: { committed: true, buffered, iterator }, empty: false };
972
- if (committing(event)) {
973
- return { outcome: { committed: true, buffered, iterator }, empty: isEmptyTerminalReceipt(event) };
1114
+ if (failure) {
1115
+ firstResponseDeadline.clear();
1116
+ return { outcome: { committed: true, buffered, iterator, abort }, empty: false };
1117
+ }
1118
+ if (commits) {
1119
+ firstResponseDeadline.clear();
1120
+ return { outcome: { committed: true, buffered, iterator, abort }, empty: isEmptyTerminalReceipt(event) };
1121
+ }
1122
+ try {
1123
+ next = await Promise.race([
1124
+ Promise.resolve(iterator.next()),
1125
+ firstResponseDeadline.promise
1126
+ ]);
1127
+ } catch (err) {
1128
+ firstResponseDeadline.clear();
1129
+ abort();
1130
+ void closeIterator(iterator, options.log);
1131
+ throw err;
1132
+ }
1133
+ if (next === TIMED_OUT) {
1134
+ firstResponseDeadline.clear();
1135
+ abort();
1136
+ void closeIterator(iterator, options.log);
1137
+ return {
1138
+ outcome: {
1139
+ committed: false,
1140
+ error: `Model ${model} produced no first answer-bearing event within ${firstResponseTimeoutMs}ms`,
1141
+ errorCode: "provider_first_response_timeout",
1142
+ timeoutCode: "provider_first_response_timeout"
1143
+ },
1144
+ empty: false
1145
+ };
1146
+ }
1147
+ if (next.done) {
1148
+ firstResponseDeadline.clear();
1149
+ abort();
1150
+ return { outcome: { committed: true, buffered, iterator: null, abort }, empty: true };
974
1151
  }
975
1152
  }
976
1153
  };
977
1154
  const probe = async (model) => {
1155
+ finalTimeout = void 0;
978
1156
  for (let retry = 0; ; retry += 1) {
979
1157
  const pass = await drainOnce(model);
980
- if (!pass.empty || retry >= emptyTurnRetries) return pass.outcome;
1158
+ if (!pass.empty || retry >= emptyTurnRetries) {
1159
+ if (!pass.outcome.committed && pass.outcome.timeoutCode) {
1160
+ finalTimeout = {
1161
+ code: pass.outcome.timeoutCode,
1162
+ model,
1163
+ timeoutMs: pass.outcome.timeoutCode === "model_stream_open_timeout" ? openTimeoutMs : firstResponseTimeoutMs
1164
+ };
1165
+ } else {
1166
+ finalTimeout = void 0;
1167
+ }
1168
+ return pass.outcome;
1169
+ }
981
1170
  const iterator = pass.outcome.committed ? pass.outcome.iterator : null;
982
- if (iterator) await closeIterator(iterator, options.log);
1171
+ if (pass.outcome.committed) pass.outcome.abort();
1172
+ if (iterator) void closeIterator(iterator, options.log);
983
1173
  const info = { model, retry: retry + 1, remaining: emptyTurnRetries - retry - 1 };
984
1174
  options.log?.("[chat-routes] turn completed with no assistant text; re-running the same model", {
985
1175
  ...info
@@ -1014,11 +1204,20 @@ function streamWithModelFailover(options) {
1014
1204
  } catch (err) {
1015
1205
  const attempts = err?.attempts;
1016
1206
  if (Array.isArray(attempts)) trail = attempts;
1207
+ if (err instanceof ModelFailoverExhaustedError && finalTimeout) {
1208
+ throw new ModelFailoverTimeoutError({
1209
+ attempts: trail,
1210
+ ...finalTimeout
1211
+ });
1212
+ }
1017
1213
  throw err;
1018
1214
  }
1019
1215
  for (const event of handle.buffered) yield event;
1020
1216
  const live = handle.iterator;
1021
- if (!live) return;
1217
+ if (!live) {
1218
+ handle.abort();
1219
+ return;
1220
+ }
1022
1221
  try {
1023
1222
  for (; ; ) {
1024
1223
  const next = await live.next();
@@ -1026,6 +1225,7 @@ function streamWithModelFailover(options) {
1026
1225
  yield next.value;
1027
1226
  }
1028
1227
  } finally {
1228
+ handle.abort();
1029
1229
  await closeIterator(live, options.log);
1030
1230
  }
1031
1231
  })();
@@ -1154,7 +1354,8 @@ function sandboxStreamFailureDiagnostic(error) {
1154
1354
  "Please retry. If it repeats, send these support details to the team."
1155
1355
  ].join("\n\n");
1156
1356
  const failureNote = diagnosticText ? `sandbox-stream: ${message}; ${diagnosticText}` : `sandbox-stream: ${message}`;
1157
- return { userMessage, failureNote };
1357
+ const code = error instanceof ModelFailoverTimeoutError ? error.code : "sandbox.stream_failed";
1358
+ return { userMessage, failureNote, code };
1158
1359
  }
1159
1360
  function createSandboxChatProducer(options) {
1160
1361
  const log = options.log ?? ((message, meta) => console.error(message, meta ?? ""));
@@ -1182,6 +1383,8 @@ function createSandboxChatProducer(options) {
1182
1383
  models: chain,
1183
1384
  open: options.openEvents,
1184
1385
  log,
1386
+ ...options.openTimeoutMs !== void 0 ? { openTimeoutMs: options.openTimeoutMs } : {},
1387
+ ...options.firstResponseTimeoutMs !== void 0 ? { firstResponseTimeoutMs: options.firstResponseTimeoutMs } : {},
1185
1388
  ...options.emptyTurnRetries !== void 0 ? { emptyTurnRetries: options.emptyTurnRetries } : {},
1186
1389
  ...options.onEmptyTurnRetry ? { onEmptyTurnRetry: options.onEmptyTurnRetry } : {},
1187
1390
  onFallback: (info) => {
@@ -1519,7 +1722,7 @@ ${diagnostic.userMessage}` : diagnostic.userMessage;
1519
1722
  type: "error",
1520
1723
  data: {
1521
1724
  message: diagnostic.userMessage,
1522
- code: "sandbox.stream_failed",
1725
+ code: diagnostic.code,
1523
1726
  details: { failureNote: diagnostic.failureNote }
1524
1727
  }
1525
1728
  };
@@ -1712,6 +1915,8 @@ async function runDetachedTurn(opts) {
1712
1915
  model: opts.model,
1713
1916
  ...opts.fallbackModels ? { fallbackModels: opts.fallbackModels } : {},
1714
1917
  ...opts.modelFailover === false ? { modelFailover: false } : {},
1918
+ ...opts.openTimeoutMs !== void 0 ? { openTimeoutMs: opts.openTimeoutMs } : {},
1919
+ ...opts.firstResponseTimeoutMs !== void 0 ? { firstResponseTimeoutMs: opts.firstResponseTimeoutMs } : {},
1715
1920
  ...opts.onModelFallback ? { onModelFallback: opts.onModelFallback } : {},
1716
1921
  isRenderableInteraction: opts.isRenderableInteraction,
1717
1922
  declineInteraction: opts.declineInteraction,
@@ -2670,6 +2875,8 @@ export {
2670
2875
  ATTACHMENT_ACCEPT,
2671
2876
  ATTACHMENT_MAX_COUNT,
2672
2877
  ChatTurnInputError,
2878
+ DEFAULT_MODEL_FIRST_RESPONSE_TIMEOUT_MS,
2879
+ DEFAULT_MODEL_STREAM_OPEN_TIMEOUT_MS,
2673
2880
  DEFAULT_STALE_TURN_LOCK_GRACE_MS,
2674
2881
  DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS,
2675
2882
  DISPATCH_MAX_MEDIA_PARTS,
@@ -2683,6 +2890,7 @@ export {
2683
2890
  MAX_EMPTY_TURN_RETRIES,
2684
2891
  MAX_TEXT_ATTACHMENT_BYTES,
2685
2892
  MENTION_MAX_COUNT,
2893
+ ModelFailoverTimeoutError,
2686
2894
  OOXML_PRESENTATION_MACRO_ENABLED_MIME,
2687
2895
  OOXML_PRESENTATION_MIME,
2688
2896
  OOXML_SNIFFED_MIMES,