@tangle-network/agent-app 0.45.27 → 0.45.29

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 (76) hide show
  1. package/dist/{DesignCanvas-3RRPVPCP.js → DesignCanvas-RSWQCXM3.js} +2 -2
  2. package/dist/{DesignCanvasEditor-KAMMEX7E.js → DesignCanvasEditor-XZMGIST7.js} +4 -4
  3. package/dist/assistant/index.d.ts +63 -39
  4. package/dist/assistant/index.js +23 -5
  5. package/dist/assistant/index.js.map +1 -1
  6. package/dist/chat-react/index.d.ts +25 -2
  7. package/dist/chat-react/index.js.map +1 -1
  8. package/dist/chat-routes/index.js +1 -1
  9. package/dist/{chunk-ODYE4A7L.js → chunk-3I27SDU3.js} +142 -58
  10. package/dist/chunk-3I27SDU3.js.map +1 -0
  11. package/dist/chunk-C3CAYGGQ.js +18 -0
  12. package/dist/chunk-C3CAYGGQ.js.map +1 -0
  13. package/dist/chunk-EMLGWEHV.js +33 -0
  14. package/dist/chunk-EMLGWEHV.js.map +1 -0
  15. package/dist/{chunk-XEA5EG6P.js → chunk-EPZYYWCT.js} +8 -30
  16. package/dist/chunk-EPZYYWCT.js.map +1 -0
  17. package/dist/{chunk-NC2V63EL.js → chunk-FNM7JYME.js} +88 -21
  18. package/dist/chunk-FNM7JYME.js.map +1 -0
  19. package/dist/{chunk-WD2B6HGY.js → chunk-K4RA5T3A.js} +11 -32
  20. package/dist/chunk-K4RA5T3A.js.map +1 -0
  21. package/dist/{chunk-4QVG4WJQ.js → chunk-OYTMILRK.js} +10 -3
  22. package/dist/chunk-OYTMILRK.js.map +1 -0
  23. package/dist/{chunk-OYGM5WLD.js → chunk-PURHUAOR.js} +52 -15
  24. package/dist/chunk-PURHUAOR.js.map +1 -0
  25. package/dist/{chunk-AWPK7ZDS.js → chunk-QGOU3VXH.js} +3 -3
  26. package/dist/{chunk-RT5RKAFX.js → chunk-QGSVF6DZ.js} +8 -18
  27. package/dist/chunk-QGSVF6DZ.js.map +1 -0
  28. package/dist/{chunk-OTLEYHOG.js → chunk-RRH23CGS.js} +24 -4
  29. package/dist/chunk-RRH23CGS.js.map +1 -0
  30. package/dist/{chunk-N3G3FSM4.js → chunk-YXA3U4JZ.js} +9 -47
  31. package/dist/chunk-YXA3U4JZ.js.map +1 -0
  32. package/dist/design-canvas-react/engine.d.ts +15 -1
  33. package/dist/design-canvas-react/engine.js +6 -2
  34. package/dist/design-canvas-react/index.d.ts +13 -1
  35. package/dist/design-canvas-react/index.js +13 -8
  36. package/dist/design-canvas-react/index.js.map +1 -1
  37. package/dist/design-canvas-react/lazy.js +1 -1
  38. package/dist/legibility/cli.js +14 -8
  39. package/dist/legibility/cli.js.map +1 -1
  40. package/dist/legibility/index.js +2 -1
  41. package/dist/peer-floors/cli.js +14 -8
  42. package/dist/peer-floors/cli.js.map +1 -1
  43. package/dist/preflight/cli.js +4 -2
  44. package/dist/preflight/cli.js.map +1 -1
  45. package/dist/{run-tEsZUhAf.d.ts → run-D80hoLKh.d.ts} +1 -1
  46. package/dist/sandbox/index.d.ts +11 -1
  47. package/dist/sandbox/index.js +1 -1
  48. package/dist/signoff/cli.d.ts +2 -2
  49. package/dist/signoff/cli.js +6 -18
  50. package/dist/signoff/cli.js.map +1 -1
  51. package/dist/signoff/index.d.ts +4 -420
  52. package/dist/signoff/index.js +3 -55
  53. package/dist/signoff/proof-cli.js +11 -6
  54. package/dist/signoff/proof-cli.js.map +1 -1
  55. package/dist/signoff/proof.d.ts +70 -271
  56. package/dist/signoff/proof.js +3 -73
  57. package/dist/signoff/proof.js.map +1 -1
  58. package/dist/theme-contract/cli.js +6 -2
  59. package/dist/theme-contract/cli.js.map +1 -1
  60. package/dist/theme-contract/index.js +2 -1
  61. package/dist/{types-U7Nz-txa.d.ts → types-Bd6uW9vQ.d.ts} +1 -1
  62. package/dist/web-react/index.d.ts +51 -44
  63. package/dist/web-react/index.js +1 -1
  64. package/package.json +14 -3
  65. package/dist/chunk-4QVG4WJQ.js.map +0 -1
  66. package/dist/chunk-N3G3FSM4.js.map +0 -1
  67. package/dist/chunk-NC2V63EL.js.map +0 -1
  68. package/dist/chunk-ODYE4A7L.js.map +0 -1
  69. package/dist/chunk-OTLEYHOG.js.map +0 -1
  70. package/dist/chunk-OYGM5WLD.js.map +0 -1
  71. package/dist/chunk-RT5RKAFX.js.map +0 -1
  72. package/dist/chunk-WD2B6HGY.js.map +0 -1
  73. package/dist/chunk-XEA5EG6P.js.map +0 -1
  74. /package/dist/{DesignCanvas-3RRPVPCP.js.map → DesignCanvas-RSWQCXM3.js.map} +0 -0
  75. /package/dist/{DesignCanvasEditor-KAMMEX7E.js.map → DesignCanvasEditor-XZMGIST7.js.map} +0 -0
  76. /package/dist/{chunk-AWPK7ZDS.js.map → chunk-QGOU3VXH.js.map} +0 -0
@@ -16,6 +16,11 @@ import { b as ChatAttachmentInput, F as FileMention } from '../wire-DOZ-O6hD.js'
16
16
  *
17
17
  * `HarnessType` is agent-interface's enum surfaced through sandbox-ui's picker;
18
18
  * `ReasoningLevel` is sandbox-ui's `auto | ReasoningEffort` union.
19
+ *
20
+ * LEGACY: these spellings serve the deprecated `ComposerAgentControls` adapter.
21
+ * Canonical code speaks `/harness`'s `Harness` union and plain effort-id
22
+ * strings (see `/web-react`'s `EffortPicker`); they are re-exported here only
23
+ * so the legacy adapter's props keep resolving.
19
24
  */
20
25
 
21
26
  interface ComposerPlanModeSelection {
@@ -71,6 +76,11 @@ interface ComposerEffortSelection {
71
76
  * session; the composer only needs the display catalog and persistence hooks.
72
77
  */
73
78
  type ComposerProfileSelection = AgentSessionProfileControl;
79
+ /**
80
+ * @deprecated Part of the legacy sandbox-ui adapter — see
81
+ * {@link ComposerAgentControls}. New code uses `AgentSessionControlsProps`
82
+ * from `@tangle-network/agent-app/web-react`.
83
+ */
74
84
  interface ComposerAgentControlsProps {
75
85
  model: ComposerModelSelection;
76
86
  /** Omit on a router-backed composer: a direct model call has no CLI backend,
@@ -117,8 +127,16 @@ interface ComposerAgentControlsProps {
117
127
  * The composer's agent-identity control: profile, backend, model, and
118
128
  * reasoning effort, over sandbox-ui's `AgentSessionControls`.
119
129
  *
120
- * This is an ADAPTER, not a second implementation. sandbox-ui owns the control
121
- * and the harness↔model coherence policy (the catalog is filtered to the
130
+ * @deprecated Use `AgentSessionControls` from `@tangle-network/agent-app/web-react`
131
+ * the canonical picker cluster (its model menu IS the canonical
132
+ * `ModelPicker`). sandbox-ui's `AgentSessionControls` and `dashboard/ModelPicker`
133
+ * are legacy: deprecated, frozen, and removed at sandbox-ui's next major. This
134
+ * adapter stays exported and fully functional because removing a published
135
+ * symbol is a major, but it renders the OLD design; new code must not adopt
136
+ * it. Props mapping for the migration lives in `docs/ui-picker-canon.md`.
137
+ *
138
+ * This is an ADAPTER, not a second implementation. The harness↔model coherence
139
+ * policy is the shared one (the catalog is filtered to the
122
140
  * selected harness, the effort re-clamps to what the harness/model pair
123
141
  * supports, and a harness that ignores a picker is marked with its dead control
124
142
  * disabled). What lives here is the app-shell wiring every product otherwise
@@ -159,6 +177,11 @@ interface EntryComposerProps {
159
177
  * omit it and the composer ships without one. Omitting is a real choice for a
160
178
  * surface with nothing to choose — it should never be an oversight, which is
161
179
  * why this is one prop rather than a free-form slot a caller can forget.
180
+ *
181
+ * NOTE: this prop routes through the deprecated `ComposerAgentControls`
182
+ * adapter (sandbox-ui's legacy strip). It keeps working until sandbox-ui's
183
+ * next major; the canonical picker cluster is `AgentSessionControls` from
184
+ * `/web-react` (see `docs/ui-picker-canon.md`).
162
185
  */
163
186
  agent?: ComposerAgentControlsProps;
164
187
  /**
@@ -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 // 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"]}
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\n/**\n * @deprecated Part of the legacy sandbox-ui adapter — see\n * {@link ComposerAgentControls}. New code uses `AgentSessionControlsProps`\n * from `@tangle-network/agent-app/web-react`.\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 * @deprecated Use `AgentSessionControls` from `@tangle-network/agent-app/web-react`\n * — the canonical picker cluster (its model menu IS the canonical\n * `ModelPicker`). sandbox-ui's `AgentSessionControls` and `dashboard/ModelPicker`\n * are legacy: deprecated, frozen, and removed at sandbox-ui's next major. This\n * adapter stays exported and fully functional because removing a published\n * symbol is a major, but it renders the OLD design; new code must not adopt\n * it. Props mapping for the migration lives in `docs/ui-picker-canon.md`.\n *\n * This is an ADAPTER, not a second implementation. The harness↔model coherence\n * policy is the shared one (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 * NOTE: this prop routes through the deprecated `ComposerAgentControls`\n * adapter (sandbox-ui's legacy strip). It keeps working until sandbox-ui's\n * next major; the canonical picker cluster is `AgentSessionControls` from\n * `/web-react` (see `docs/ui-picker-canon.md`).\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"]}
@@ -109,7 +109,7 @@ import {
109
109
  flattenHistory,
110
110
  readSandboxBinaryBytes,
111
111
  statSandboxFileSize
112
- } from "../chunk-OTLEYHOG.js";
112
+ } from "../chunk-RRH23CGS.js";
113
113
  import "../chunk-TH7L265V.js";
114
114
  import "../chunk-LWSJK546.js";
115
115
  import "../chunk-P5PIAQVS.js";
@@ -242,9 +242,17 @@ function ModelPicker({ value, onChange, models, loading, renderProviderBadge, re
242
242
  const [query, setQuery] = useState2("");
243
243
  const { containerRef, triggerProps } = usePopover(open, setOpen);
244
244
  const inputRef = useRef2(null);
245
+ const popoverRef = useRef2(null);
245
246
  useEffect2(() => {
246
247
  if (open) inputRef.current?.focus();
247
248
  }, [open]);
249
+ useEffect2(() => {
250
+ if (!open) return;
251
+ const el = popoverRef.current;
252
+ if (!el) return;
253
+ const overflowRight = el.getBoundingClientRect().right - (window.innerWidth - 16);
254
+ el.style.transform = overflowRight > 0 ? `translateX(-${Math.ceil(overflowRight)}px)` : "";
255
+ }, [open]);
248
256
  const selected = models.find((m) => m.id === value);
249
257
  const filtered = useMemo(() => {
250
258
  const q = query.trim().toLowerCase();
@@ -286,7 +294,7 @@ function ModelPicker({ value, onChange, models, loading, renderProviderBadge, re
286
294
  ]
287
295
  }
288
296
  ),
289
- open && /* @__PURE__ */ jsxs2("div", { className: "absolute bottom-full left-0 z-50 mb-2 w-[420px] max-w-[calc(100vw-2rem)] overflow-hidden rounded-xl border border-border bg-card shadow-lg", children: [
297
+ open && /* @__PURE__ */ jsxs2("div", { ref: popoverRef, className: "absolute bottom-full left-0 z-50 mb-2 w-[420px] max-w-[calc(100vw-2rem)] overflow-hidden rounded-xl border border-border bg-card shadow-lg", children: [
290
298
  /* @__PURE__ */ jsx2("div", { className: "border-b border-border px-3 py-2", children: /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2 rounded-lg border border-border bg-background px-3 py-2", children: [
291
299
  /* @__PURE__ */ jsx2(SearchGlyph, { className: "h-3.5 w-3.5 text-muted-foreground" }),
292
300
  /* @__PURE__ */ jsx2(
@@ -307,7 +315,8 @@ function ModelPicker({ value, onChange, models, loading, renderProviderBadge, re
307
315
  filtered.length === 0 && /* @__PURE__ */ jsx2("div", { className: "px-3 py-4 text-center text-sm text-muted-foreground", children: "No models match your search" }),
308
316
  filtered.map((m) => /* @__PURE__ */ jsx2(ModelRow, { model: m, selected: m.id === value, onSelect: () => select(m.id), renderProviderBadge }, m.id))
309
317
  ] }),
310
- !loading && !filtered && /* @__PURE__ */ jsxs2(Fragment, { children: [
318
+ !loading && !filtered && models.length === 0 && /* @__PURE__ */ jsx2("div", { className: "px-3 py-4 text-center text-sm text-muted-foreground", children: "No models available" }),
319
+ !loading && !filtered && models.length > 0 && /* @__PURE__ */ jsxs2(Fragment, { children: [
311
320
  priorityGroup && sections.priority.length > 0 && /* @__PURE__ */ jsxs2(Fragment, { children: [
312
321
  /* @__PURE__ */ jsx2(SectionHeader, { children: priorityGroup.label }),
313
322
  sections.priority.map((m) => /* @__PURE__ */ jsx2(ModelRow, { model: m, selected: m.id === value, onSelect: () => select(m.id), renderProviderBadge }, m.id))
@@ -1629,6 +1638,7 @@ function ChatComposer({
1629
1638
  dropTitle = "Drop files to add context",
1630
1639
  dropDescription = "They attach to your next message.",
1631
1640
  focusShortcut = true,
1641
+ floating = false,
1632
1642
  sendLabel = "Send",
1633
1643
  className
1634
1644
  }) {
@@ -1908,7 +1918,7 @@ function ChatComposer({
1908
1918
  "div",
1909
1919
  {
1910
1920
  "data-testid": "composer-card",
1911
- className: "flex flex-col gap-1.5 rounded-2xl border border-border bg-card px-3 py-2.5 transition focus-within:border-primary/40 focus-within:ring-2 focus-within:ring-primary/15",
1921
+ className: `flex flex-col gap-1.5 rounded-2xl border border-border bg-card px-3 py-2.5 transition focus-within:border-primary/40 focus-within:ring-2 focus-within:ring-primary/15 ${floating ? "shadow-[0_1px_2px_hsl(var(--foreground)/0.05),0_12px_28px_hsl(var(--foreground)/0.07)] dark:shadow-[0_1px_2px_hsl(var(--foreground)/0.14),0_12px_28px_hsl(var(--foreground)/0.22)]" : ""}`,
1912
1922
  children: [
1913
1923
  /* @__PURE__ */ jsx9(
1914
1924
  "textarea",
@@ -1965,7 +1975,14 @@ function ChatComposer({
1965
1975
  }
1966
1976
  )
1967
1977
  ] }),
1968
- /* @__PURE__ */ jsx9("div", { className: "flex min-w-0 flex-1 flex-wrap items-center gap-1.5", children: showInline && controls }),
1978
+ /* @__PURE__ */ jsx9(
1979
+ "div",
1980
+ {
1981
+ "data-testid": "composer-controls",
1982
+ className: "flex min-w-0 flex-1 flex-wrap items-center gap-1.5",
1983
+ children: showInline && controls
1984
+ }
1985
+ ),
1969
1986
  isStreaming ? /* @__PURE__ */ jsxs7(
1970
1987
  "button",
1971
1988
  {
@@ -2773,20 +2790,20 @@ function ActivityRow({
2773
2790
  function AgentActivityPanel({ fetchActivity, renderMissionRef, title = "Agent activity", emptyLabel = "No agent runs yet." }) {
2774
2791
  const [rows, setRows] = useState11([]);
2775
2792
  const [cursor, setCursor] = useState11(void 0);
2776
- const [loading, setLoading] = useState11(false);
2793
+ const [status, setStatus] = useState11("loading");
2777
2794
  const [error, setError] = useState11(null);
2778
2795
  const load = useCallback6(
2779
2796
  async (from) => {
2780
- setLoading(true);
2797
+ setStatus("loading");
2781
2798
  setError(null);
2782
2799
  try {
2783
2800
  const page = await fetchActivity(from);
2784
2801
  setRows((prev) => mergeActivityPages(from === void 0 ? [] : prev, page.items));
2785
2802
  setCursor(page.nextCursor);
2803
+ setStatus("ready");
2786
2804
  } catch (e) {
2787
2805
  setError(e instanceof Error ? e.message : String(e));
2788
- } finally {
2789
- setLoading(false);
2806
+ setStatus("error");
2790
2807
  }
2791
2808
  },
2792
2809
  [fetchActivity]
@@ -2794,6 +2811,7 @@ function AgentActivityPanel({ fetchActivity, renderMissionRef, title = "Agent ac
2794
2811
  useEffect8(() => {
2795
2812
  void load();
2796
2813
  }, [load]);
2814
+ const loading = status === "loading";
2797
2815
  return /* @__PURE__ */ jsxs8("div", { className: "space-y-2", children: [
2798
2816
  /* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-2", children: [
2799
2817
  /* @__PURE__ */ jsx10("h2", { className: "flex-1 text-sm font-semibold", children: title }),
@@ -2809,8 +2827,8 @@ function AgentActivityPanel({ fetchActivity, renderMissionRef, title = "Agent ac
2809
2827
  }
2810
2828
  )
2811
2829
  ] }),
2812
- error && /* @__PURE__ */ jsx10("p", { role: "alert", className: "rounded-md border border-destructive/40 bg-destructive/5 px-3 py-2 text-xs text-destructive", children: error }),
2813
- !error && rows.length === 0 && !loading && /* @__PURE__ */ jsx10("p", { className: "px-1 text-sm text-muted-foreground", children: emptyLabel }),
2830
+ status === "error" && /* @__PURE__ */ jsx10("p", { role: "alert", className: "rounded-md border border-destructive/40 bg-destructive/5 px-3 py-2 text-xs text-destructive", children: error }),
2831
+ status === "ready" && rows.length === 0 && /* @__PURE__ */ jsx10("p", { className: "px-1 text-sm text-muted-foreground", children: emptyLabel }),
2814
2832
  /* @__PURE__ */ jsx10("span", { role: "status", "aria-live": "polite", "aria-busy": loading, className: "sr-only", children: loading ? "Loading activity\u2026" : "" }),
2815
2833
  /* @__PURE__ */ jsx10("div", { className: "space-y-1.5", "aria-busy": loading, children: rows.map((record) => /* @__PURE__ */ jsx10(ActivityRow, { record, renderMissionRef }, record.taskId)) }),
2816
2834
  cursor && /* @__PURE__ */ jsx10(
@@ -4175,6 +4193,7 @@ function SessionRow({
4175
4193
  // src/web-react/record-grid.tsx
4176
4194
  import {
4177
4195
  Fragment as Fragment7,
4196
+ isValidElement,
4178
4197
  useCallback as useCallback9,
4179
4198
  useEffect as useEffect11,
4180
4199
  useId as useId2,
@@ -4511,6 +4530,7 @@ function pruneRecordGridOverlay(rows, overlay) {
4511
4530
 
4512
4531
  // src/web-react/record-grid.tsx
4513
4532
  import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
4533
+ var EMPTY_RECORD_GRID_ROWS = [];
4514
4534
  var CELL_KEY_SEPARATOR = "\0";
4515
4535
  function cellKey(rowId, columnId) {
4516
4536
  return `${rowId}${CELL_KEY_SEPARATOR}${columnId}`;
@@ -4537,22 +4557,21 @@ function groupColumns(columns) {
4537
4557
  }
4538
4558
  var INPUT_CLASS = "w-full rounded-md border border-border bg-background px-2 py-1 text-sm text-foreground outline-none focus:border-primary/60 focus:ring-1 focus:ring-primary/40";
4539
4559
  var BASIS_TONES2 = {
4540
- source: "border-primary/60 text-primary",
4541
- confirmed: "border-success/60 text-success",
4542
- derived: "border-border text-muted-foreground"
4560
+ extracted: "border-primary/60 text-primary",
4561
+ entered: "border-success/60 text-success",
4562
+ computed: "border-border text-muted-foreground",
4563
+ asserted: "border-warning/60 text-warning"
4543
4564
  };
4544
4565
  var BASIS_TITLES = {
4545
- source: "Extracted from a source document",
4546
- confirmed: "Confirmed by a person",
4547
- derived: "Computed from other values"
4566
+ extracted: "Extracted from a source document",
4567
+ entered: "Confirmed by a person",
4568
+ computed: "Computed from other values",
4569
+ asserted: "Agent, unverified \u2014 no source recorded"
4548
4570
  };
4549
4571
  function RecordGrid({
4550
4572
  columns,
4551
- rows,
4552
4573
  caption,
4553
- state = "ready",
4554
- error,
4555
- onRetry,
4574
+ state,
4556
4575
  empty,
4557
4576
  onCreate,
4558
4577
  onUpdate,
@@ -4586,10 +4605,11 @@ function RecordGrid({
4586
4605
  editingRef.current = next;
4587
4606
  setEditingState(next);
4588
4607
  }, []);
4608
+ const callerRows = state.status === "ready" || state.status === "empty" ? state.value : EMPTY_RECORD_GRID_ROWS;
4589
4609
  useEffect11(() => {
4590
- setOverlay((current) => pruneRecordGridOverlay(rows, current));
4591
- }, [rows]);
4592
- const visibleRows = useMemo7(() => projectRecordGridRows(rows, overlay), [rows, overlay]);
4610
+ setOverlay((current) => pruneRecordGridOverlay(callerRows, current));
4611
+ }, [callerRows]);
4612
+ const visibleRows = useMemo7(() => projectRecordGridRows(callerRows, overlay), [callerRows, overlay]);
4593
4613
  const activeFocus = useMemo7(() => {
4594
4614
  if (focus === null) return null;
4595
4615
  if (!visibleRows.some((row) => row.id === focus.rowId)) return null;
@@ -4798,8 +4818,7 @@ function RecordGrid({
4798
4818
  },
4799
4819
  [beginEdit, columns, editing, focusCell, onUpdate, visibleRows]
4800
4820
  );
4801
- const dataState = typeof error === "string" && error !== "" ? "error" : state;
4802
- if (dataState === "loading") {
4821
+ if (state.status === "idle" || state.status === "loading") {
4803
4822
  return /* @__PURE__ */ jsxs12("div", { className: `space-y-3 ${className ?? ""}`, children: [
4804
4823
  toolbar,
4805
4824
  /* @__PURE__ */ jsxs12(
@@ -4820,16 +4839,16 @@ function RecordGrid({
4820
4839
  )
4821
4840
  ] });
4822
4841
  }
4823
- if (dataState === "error") {
4842
+ if (state.status === "error") {
4824
4843
  return /* @__PURE__ */ jsxs12("div", { className: `space-y-3 ${className ?? ""}`, children: [
4825
4844
  toolbar,
4826
4845
  /* @__PURE__ */ jsxs12("div", { role: "alert", className: "rounded-xl border border-destructive/40 bg-destructive/10 px-4 py-4", children: [
4827
- /* @__PURE__ */ jsx14("p", { className: "text-sm font-medium text-destructive", children: typeof error === "string" && error !== "" ? error : `${caption} could not be loaded.` }),
4828
- onRetry && /* @__PURE__ */ jsx14(
4846
+ /* @__PURE__ */ jsx14("p", { className: "text-sm font-medium text-destructive", children: state.message }),
4847
+ /* @__PURE__ */ jsx14(
4829
4848
  "button",
4830
4849
  {
4831
4850
  type: "button",
4832
- onClick: onRetry,
4851
+ onClick: state.retry,
4833
4852
  className: "mt-3 rounded-md border border-destructive/40 px-3 py-1.5 text-xs font-medium text-destructive transition hover:bg-destructive/10",
4834
4853
  children: "Try again"
4835
4854
  }
@@ -4862,7 +4881,15 @@ function RecordGrid({
4862
4881
  /* @__PURE__ */ jsx14("p", { className: "text-sm font-medium text-foreground", children: empty.title }),
4863
4882
  empty.description && /* @__PURE__ */ jsx14("p", { className: "mx-auto mt-1 max-w-md text-sm text-muted-foreground", children: empty.description }),
4864
4883
  /* @__PURE__ */ jsxs12("div", { className: "mt-4 flex flex-wrap items-center justify-center gap-2", children: [
4865
- empty.action,
4884
+ empty.action && (isValidElement(empty.action) ? empty.action : /* @__PURE__ */ jsx14(
4885
+ "button",
4886
+ {
4887
+ type: "button",
4888
+ onClick: empty.action.onClick,
4889
+ className: "rounded-md border border-border px-3 py-1.5 text-sm font-medium text-foreground transition hover:bg-accent/30",
4890
+ children: empty.action.label
4891
+ }
4892
+ )),
4866
4893
  onCreate && /* @__PURE__ */ jsx14(
4867
4894
  "button",
4868
4895
  {
@@ -5158,7 +5185,7 @@ function CellEditor({ column, rowLabel, text, invalid, describedBy, onText, onCo
5158
5185
  );
5159
5186
  }
5160
5187
  function SourceMarker({ panelId, columnHeader, rowLabel, source, open, onToggle }) {
5161
- const basis = source.basis ?? "source";
5188
+ const basis = source.basis ?? "asserted";
5162
5189
  return /* @__PURE__ */ jsxs12("span", { className: "relative inline-flex", children: [
5163
5190
  /* @__PURE__ */ jsx14(
5164
5191
  "button",
@@ -6017,6 +6044,49 @@ function SegmentedBody({
6017
6044
  streaming && segments[lastIndex]?.kind === "tool" && /* @__PURE__ */ jsx16(StreamingCaret, {})
6018
6045
  ] });
6019
6046
  }
6047
+ var QUIET_META_LANE_CLASS = "mt-1 flex h-[18px] items-center gap-2 text-[11px] tracking-wide text-muted-foreground opacity-0 transition-opacity duration-150 group-hover:opacity-100 group-focus-within:opacity-100 motion-reduce:transition-none [@media(hover:none)]:opacity-100";
6048
+ function copyTextOf(msg) {
6049
+ const textRuns = msg.segments?.filter((s) => s.kind === "text") ?? [];
6050
+ if (textRuns.length > 0) return textRuns.map((s) => s.content).join("\n\n");
6051
+ return msg.content;
6052
+ }
6053
+ function CopyMessageButton({ text }) {
6054
+ const [copied, setCopied] = useState16(false);
6055
+ const timerRef = useRef11(null);
6056
+ useEffect12(
6057
+ () => () => {
6058
+ if (timerRef.current !== null) clearTimeout(timerRef.current);
6059
+ },
6060
+ []
6061
+ );
6062
+ const copy = () => {
6063
+ const clipboard = navigator.clipboard;
6064
+ if (!clipboard) return;
6065
+ void clipboard.writeText(text).then(
6066
+ () => {
6067
+ setCopied(true);
6068
+ if (timerRef.current !== null) clearTimeout(timerRef.current);
6069
+ timerRef.current = setTimeout(() => setCopied(false), 1200);
6070
+ },
6071
+ () => {
6072
+ }
6073
+ );
6074
+ };
6075
+ return /* @__PURE__ */ jsx16(
6076
+ "button",
6077
+ {
6078
+ type: "button",
6079
+ onClick: copy,
6080
+ "aria-label": "Copy message",
6081
+ title: "Copy message",
6082
+ className: "rounded p-0.5 text-muted-foreground transition hover:bg-accent/40 hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring",
6083
+ children: copied ? /* @__PURE__ */ jsx16("svg", { className: "h-3.5 w-3.5", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: /* @__PURE__ */ jsx16("polyline", { points: "20 6 9 17 4 12" }) }) : /* @__PURE__ */ jsxs14("svg", { className: "h-3.5 w-3.5", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [
6084
+ /* @__PURE__ */ jsx16("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2" }),
6085
+ /* @__PURE__ */ jsx16("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })
6086
+ ] })
6087
+ }
6088
+ );
6089
+ }
6020
6090
  function AssistantMessageImpl({
6021
6091
  msg,
6022
6092
  streaming,
@@ -6030,7 +6100,8 @@ function AssistantMessageImpl({
6030
6100
  durableCards,
6031
6101
  resolveAttachmentUrl,
6032
6102
  workProductCards,
6033
- messageClassName
6103
+ messageClassName,
6104
+ chrome
6034
6105
  }) {
6035
6106
  const content = useSmoothText(msg.content, streaming);
6036
6107
  const reasoning = useSmoothText(msg.reasoning ?? "", streaming);
@@ -6053,8 +6124,9 @@ function AssistantMessageImpl({
6053
6124
  const thinkingSeconds = useThinkingSeconds(
6054
6125
  streaming && !!reasoning && !hasAnswerText
6055
6126
  );
6056
- return /* @__PURE__ */ jsxs14("div", { className: "mx-auto w-full max-w-3xl px-6 py-3", children: [
6057
- /* @__PURE__ */ jsxs14("div", { className: "mb-1 flex items-baseline gap-2 text-[11px] tracking-wide text-muted-foreground", children: [
6127
+ const quiet = chrome === "quiet";
6128
+ return /* @__PURE__ */ jsxs14("div", { className: `mx-auto w-full max-w-3xl px-6 ${quiet ? "group pb-1 pt-3" : "py-3"}`, children: [
6129
+ !quiet && /* @__PURE__ */ jsxs14("div", { className: "mb-1 flex items-baseline gap-2 text-[11px] tracking-wide text-muted-foreground", children: [
6058
6130
  /* @__PURE__ */ jsx16("span", { className: "font-semibold uppercase", children: agentLabel }),
6059
6131
  msg.modelUsed && /* @__PURE__ */ jsx16("span", { className: "font-mono normal-case", children: msg.modelUsed }),
6060
6132
  formatTokensPerSecond(msg) && /* @__PURE__ */ jsx16("span", { children: formatTokensPerSecond(msg) }),
@@ -6122,7 +6194,13 @@ function AssistantMessageImpl({
6122
6194
  resolveFileUrl: resolveAttachmentUrl,
6123
6195
  justify: "start"
6124
6196
  }
6125
- ) })
6197
+ ) }),
6198
+ quiet && /* @__PURE__ */ jsxs14("div", { "data-testid": "message-meta-lane", className: QUIET_META_LANE_CLASS, children: [
6199
+ /* @__PURE__ */ jsx16(CopyMessageButton, { text: copyTextOf(msg) }),
6200
+ msg.modelUsed && /* @__PURE__ */ jsx16("span", { className: "font-mono", children: msg.modelUsed }),
6201
+ formatTokensPerSecond(msg) && /* @__PURE__ */ jsx16("span", { children: formatTokensPerSecond(msg) }),
6202
+ formatModelCost(msg, models) && /* @__PURE__ */ jsx16("span", { children: formatModelCost(msg, models) })
6203
+ ] })
6126
6204
  ] });
6127
6205
  }
6128
6206
  var AssistantMessage = memo(AssistantMessageImpl);
@@ -6136,10 +6214,10 @@ function useThinkingSeconds(active) {
6136
6214
  }, [active]);
6137
6215
  return seconds;
6138
6216
  }
6139
- function ThinkingRow({ agentLabel }) {
6217
+ function ThinkingRow({ agentLabel, chrome = "labeled" }) {
6140
6218
  const seconds = useThinkingSeconds(true);
6141
6219
  return /* @__PURE__ */ jsxs14("div", { className: "mx-auto w-full max-w-3xl px-6 py-3", children: [
6142
- /* @__PURE__ */ jsx16("p", { className: "mb-1 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: agentLabel }),
6220
+ chrome !== "quiet" && /* @__PURE__ */ jsx16("p", { className: "mb-1 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: agentLabel }),
6143
6221
  /* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-2 text-base text-muted-foreground", children: [
6144
6222
  /* @__PURE__ */ jsx16("svg", { className: "h-4 w-4 animate-spin", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", "aria-hidden": true, children: /* @__PURE__ */ jsx16("path", { d: "M21 12a9 9 0 1 1-6.219-8.56", strokeLinecap: "round" }) }),
6145
6223
  "Thinking",
@@ -6168,6 +6246,7 @@ function StreamErrorRow({ message, onRetry }) {
6168
6246
  function ChatMessages({
6169
6247
  messages,
6170
6248
  messageSize = "default",
6249
+ chrome = "labeled",
6171
6250
  models = [],
6172
6251
  renderMarkdown,
6173
6252
  renderExtras,
@@ -6192,6 +6271,7 @@ function ChatMessages({
6192
6271
  [renderMarkdown]
6193
6272
  );
6194
6273
  const lastIsUser = messages[messages.length - 1]?.role === "user";
6274
+ const quiet = chrome === "quiet";
6195
6275
  if (messages.length === 0 && !loading && !error) {
6196
6276
  const empty = renderEmpty ? renderEmpty() : /* @__PURE__ */ jsx16(ChatEmptyState, { ...emptyState });
6197
6277
  return /* @__PURE__ */ jsxs14(Fragment8, { children: [
@@ -6202,24 +6282,27 @@ function ChatMessages({
6202
6282
  return /* @__PURE__ */ jsxs14(Fragment8, { children: [
6203
6283
  header,
6204
6284
  messages.map(
6205
- (msg) => msg.role === "user" ? /* @__PURE__ */ jsx16("div", { className: "mx-auto w-full max-w-3xl px-6 py-3", children: /* @__PURE__ */ jsxs14("div", { className: "ml-auto w-fit max-w-[85%]", children: [
6206
- /* @__PURE__ */ jsx16("p", { className: "mb-1 text-right text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: userLabel }),
6207
- /* @__PURE__ */ jsx16(
6208
- "div",
6209
- {
6210
- className: `rounded-2xl rounded-tr-md bg-primary/10 px-4 py-2.5 ${messageClassName}`,
6211
- children: /* @__PURE__ */ jsx16("p", { className: "whitespace-pre-wrap", children: msg.content })
6212
- }
6213
- ),
6214
- resolveAttachmentUrl && attachmentPartsFromMessageParts(msg.parts).length > 0 && /* @__PURE__ */ jsx16("div", { className: "mt-1.5", children: /* @__PURE__ */ jsx16(
6215
- MessageAttachments,
6216
- {
6217
- parts: attachmentPartsFromMessageParts(msg.parts),
6218
- resolveFileUrl: resolveAttachmentUrl,
6219
- justify: "end"
6220
- }
6221
- ) })
6222
- ] }) }, msg.id) : /* @__PURE__ */ jsx16(
6285
+ (msg) => msg.role === "user" ? /* @__PURE__ */ jsxs14("div", { className: `mx-auto w-full max-w-3xl px-6 ${quiet ? "group pb-1 pt-3" : "py-3"}`, children: [
6286
+ /* @__PURE__ */ jsxs14("div", { className: `ml-auto w-fit ${quiet ? "max-w-[72%]" : "max-w-[85%]"}`, children: [
6287
+ !quiet && /* @__PURE__ */ jsx16("p", { className: "mb-1 text-right text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: userLabel }),
6288
+ /* @__PURE__ */ jsx16(
6289
+ "div",
6290
+ {
6291
+ className: quiet ? `rounded-2xl bg-[color-mix(in_srgb,hsl(var(--secondary))_65%,hsl(var(--background)))] px-4 py-2.5 ${messageClassName}` : `rounded-2xl rounded-tr-md bg-primary/10 px-4 py-2.5 ${messageClassName}`,
6292
+ children: /* @__PURE__ */ jsx16("p", { className: "whitespace-pre-wrap", children: msg.content })
6293
+ }
6294
+ ),
6295
+ resolveAttachmentUrl && attachmentPartsFromMessageParts(msg.parts).length > 0 && /* @__PURE__ */ jsx16("div", { className: "mt-1.5", children: /* @__PURE__ */ jsx16(
6296
+ MessageAttachments,
6297
+ {
6298
+ parts: attachmentPartsFromMessageParts(msg.parts),
6299
+ resolveFileUrl: resolveAttachmentUrl,
6300
+ justify: "end"
6301
+ }
6302
+ ) })
6303
+ ] }),
6304
+ quiet && /* @__PURE__ */ jsx16("div", { "data-testid": "message-meta-lane", className: `${QUIET_META_LANE_CLASS} justify-end`, children: /* @__PURE__ */ jsx16(CopyMessageButton, { text: msg.content }) })
6305
+ ] }, msg.id) : /* @__PURE__ */ jsx16(
6223
6306
  AssistantMessage,
6224
6307
  {
6225
6308
  msg,
@@ -6234,12 +6317,13 @@ function ChatMessages({
6234
6317
  durableCards,
6235
6318
  resolveAttachmentUrl,
6236
6319
  workProductCards,
6237
- messageClassName
6320
+ messageClassName,
6321
+ chrome
6238
6322
  },
6239
6323
  msg.id
6240
6324
  )
6241
6325
  ),
6242
- loading && lastIsUser && /* @__PURE__ */ jsx16(ThinkingRow, { agentLabel }),
6326
+ loading && lastIsUser && /* @__PURE__ */ jsx16(ThinkingRow, { agentLabel, chrome }),
6243
6327
  error && !loading && /* @__PURE__ */ jsx16(StreamErrorRow, { message: error, onRetry })
6244
6328
  ] });
6245
6329
  }
@@ -6364,4 +6448,4 @@ export {
6364
6448
  useThinkingSeconds,
6365
6449
  ChatMessages
6366
6450
  };
6367
- //# sourceMappingURL=chunk-ODYE4A7L.js.map
6451
+ //# sourceMappingURL=chunk-3I27SDU3.js.map