@parallelworks/ui 0.34.0 → 0.35.0

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 (84) hide show
  1. package/README.md +201 -0
  2. package/dist/ai/components/ChatEmptyState.js +71 -86
  3. package/dist/ai/components/ChatInput.d.ts +11 -3
  4. package/dist/ai/components/ChatInput.js +19 -11
  5. package/dist/ai/components/ChatSidebar.js +12 -130
  6. package/dist/ai/components/ChatStage.d.ts +11 -0
  7. package/dist/ai/components/ChatStage.js +26 -0
  8. package/dist/ai/components/ComposerChrome.d.ts +3 -0
  9. package/dist/ai/components/ComposerChrome.js +21 -2
  10. package/dist/ai/components/conversationActions.d.ts +23 -0
  11. package/dist/ai/components/conversationActions.js +108 -0
  12. package/dist/ai/index.d.ts +4 -3
  13. package/dist/ai/index.js +6 -5
  14. package/dist/ai/strings.d.ts +1 -0
  15. package/dist/ai/strings.js +2 -1
  16. package/dist/ai/ui/ConversationSidebar.d.ts +3 -2
  17. package/dist/ai/ui/ConversationSidebar.js +2 -1
  18. package/dist/ai/ui/sidebar.d.ts +6 -0
  19. package/dist/ai/ui/sidebar.js +10 -1
  20. package/dist/components/Dropdown.js +3 -1
  21. package/dist/components/FilterPill.js +1 -0
  22. package/dist/components/Provider.d.ts +14 -0
  23. package/dist/components/Provider.js +14 -3
  24. package/dist/components/SegmentedControl.d.ts +24 -0
  25. package/dist/components/SegmentedControl.js +69 -0
  26. package/dist/editing.d.ts +32 -0
  27. package/dist/editor/FormCodePanel.js +1 -1
  28. package/dist/engine.d.ts +3 -0
  29. package/dist/form/FieldWrapper.js +1 -1
  30. package/dist/form/Fields/CheckboxGroupField.d.ts +3 -3
  31. package/dist/form/Fields/CheckboxGroupField.js +10 -4
  32. package/dist/form/Fields/RadioField.d.ts +5 -3
  33. package/dist/form/Fields/RadioField.js +25 -17
  34. package/dist/form/Fields/fieldOption.d.ts +12 -0
  35. package/dist/form/Fields/fieldOption.js +17 -0
  36. package/dist/form/Form.d.ts +4 -2
  37. package/dist/form/Form.js +170 -68
  38. package/dist/form/FormLayout.d.ts +7 -0
  39. package/dist/form/FormLayout.js +144 -0
  40. package/dist/form/SpecialTypes/FormikCustomDropdown.js +1 -1
  41. package/dist/form/SpecialTypes/FormikCustomInput.js +7 -0
  42. package/dist/form/Wizard/WizardContainer.d.ts +1 -1
  43. package/dist/form/Wizard/WizardContainer.js +244 -49
  44. package/dist/form/Wizard/WizardNavigation.js +4 -4
  45. package/dist/form/Wizard/WizardStepContent.d.ts +1 -1
  46. package/dist/form/Wizard/WizardStepContent.js +19 -10
  47. package/dist/form/Wizard/WizardStepIndicator.js +35 -7
  48. package/dist/form/Wizard/types.d.ts +19 -6
  49. package/dist/form/Wizard/useWizardState.d.ts +1 -0
  50. package/dist/form/Wizard/useWizardState.js +5 -1
  51. package/dist/form/Wizard/utils.d.ts +7 -0
  52. package/dist/form/Wizard/utils.js +18 -1
  53. package/dist/form/index.d.ts +2 -0
  54. package/dist/form/index.js +4 -2
  55. package/dist/form/layout.d.ts +50 -0
  56. package/dist/form/layout.js +125 -0
  57. package/dist/form/layoutCSS.d.ts +10 -0
  58. package/dist/form/layoutCSS.js +234 -0
  59. package/dist/form/lib.d.ts +2 -0
  60. package/dist/form/lib.js +17 -2
  61. package/dist/form/types/fieldTypes.d.ts +8 -23
  62. package/dist/graph/DependencyGraph.d.ts +20 -1
  63. package/dist/graph/DependencyGraph.js +437 -142
  64. package/dist/graph/MatrixGroup.d.ts +3 -1
  65. package/dist/graph/MatrixGroup.js +31 -28
  66. package/dist/graph/TreeView.js +6 -6
  67. package/dist/graph/gridSpacing.d.ts +7 -0
  68. package/dist/graph/gridSpacing.js +10 -0
  69. package/dist/graph/util.d.ts +2 -0
  70. package/dist/graph/util.js +2 -2
  71. package/dist/index.d.ts +1 -0
  72. package/dist/index.js +2 -1
  73. package/dist/list/ListViewControls.d.ts +15 -0
  74. package/dist/list/ListViewControls.js +9 -3
  75. package/dist/list/index.d.ts +1 -1
  76. package/dist/list/index.js +2 -2
  77. package/dist/styles/base.css +3 -1
  78. package/dist/styles/theme.css +5 -0
  79. package/dist/styles.css +1 -1
  80. package/dist/theme/index.d.ts +2 -0
  81. package/dist/theme/index.js +3 -2
  82. package/package.json +4 -2
  83. package/dist/ai/components/ComposerFrame.d.ts +0 -16
  84. package/dist/ai/components/ComposerFrame.js +0 -33
package/README.md CHANGED
@@ -87,6 +87,18 @@ The engine converts input definitions into the form's field tree, resolves
87
87
  grouping, step labels and log paths. The form and graph throw without one; the
88
88
  editor and log viewer fall back to plain behavior.
89
89
 
90
+ An engine can also carry `editing` (`WorkflowEditing`), the workflow model the
91
+ graph defers to for where jobs go: each job's slot from its `position` or a
92
+ stored layout, a matrix drawn as a run lists its jobs, and needs that wait for
93
+ any one of a matrix's jobs. A host built on a workflow package passes that
94
+ package's functions of the same names:
95
+
96
+ ```tsx
97
+ const engine = { ...createWorkflowEngine(), editing: workflowPackage }
98
+ ```
99
+
100
+ Without `editing`, the graph lays its jobs out by their needs alone.
101
+
90
102
  ## AI chat
91
103
 
92
104
  `@parallelworks/ui/ai` is a chat interface (thread, composer, streaming,
@@ -141,3 +153,192 @@ animations) and streamdown's classes next to the theme contract:
141
153
  ```
142
154
 
143
155
  The prebuilt `@parallelworks/ui/styles.css` already includes both.
156
+
157
+ ## Form layouts
158
+
159
+ `DynamicForm` accepts an optional `$meta.layout` in `formJSONs`. A layout arranges
160
+ existing inputs; it never creates a value or changes an input's name, validation,
161
+ default, or saved-value path. Without a layout, the existing form behavior stays
162
+ unchanged.
163
+
164
+ ```yaml
165
+ $meta:
166
+ layout:
167
+ type: grid
168
+ columns: { base: 1, md: [2, 1] }
169
+ gap: lg
170
+ children:
171
+ - type: field
172
+ field: name
173
+ - type: section
174
+ label: Resources
175
+ children:
176
+ - { type: field, field: queue }
177
+ - { type: field, field: nodes }
178
+ name: { type: string, label: Name }
179
+ queue: { type: dropdown, label: Queue, options: [cpu, gpu] }
180
+ nodes: { type: number, label: Nodes }
181
+ ```
182
+
183
+ Layout nodes can nest:
184
+
185
+ | Type | Properties | Behavior |
186
+ | --- | --- | --- |
187
+ | `field` | `field` | References an input name in the current scope. |
188
+ | `stack` | `children`, optional `gap` | Stacks children vertically. |
189
+ | `grid` | `children`, `columns`, optional `gap`, `align` | Arranges children in equal or weighted columns. |
190
+ | `section` | `children`, `label`, optional `description`, `gap` | Groups controls in a labeled fieldset. |
191
+
192
+ `columns` accepts a count from 1 to 12, or an array of positive relative weights
193
+ such as `[2, 1]`. For responsive layouts, use an object with `base`, `sm`, `md`,
194
+ and/or `lg`. These thresholds refer to the **grid container's width**: 24rem,
195
+ 40rem, and 64rem respectively. An omitted base starts with one column; each
196
+ larger threshold inherits the previous value. A plain count or weight array
197
+ applies at every width. Nested grids measure their own available space.
198
+
199
+ Every node can set `span`, as a count or the same responsive object. A span is
200
+ clamped to the current grid's column count. `gap` accepts `none`, `sm`, `md`
201
+ (default), or `lg`. Labels default to the top within an explicit layout;
202
+ `$meta.labelPosition` still overrides them.
203
+
204
+ A grid defaults to `align: independent`: its children start at the top and their
205
+ contents flow independently. Set `align: rows` when every immediate child is a
206
+ section. Adjacent sections then share a header row and one row per direct child.
207
+ A description, help text, or validation message increases the shared row height,
208
+ so the following rows stay aligned. A nested grid counts as one child row.
209
+ The grid's gap controls shared row spacing. Hidden children retain their row slot
210
+ while a neighboring section uses it; empty sections disappear. When the configured
211
+ grid becomes one column, sections flow vertically and hidden rows collapse.
212
+ Tab order follows each section's document order, including when the sections stack.
213
+ Wrapped field labels still belong to their individual field; row alignment aligns
214
+ field groups, not separate label and control baselines.
215
+
216
+ ```yaml
217
+ type: grid
218
+ columns: { base: 1, md: [2, 1] }
219
+ align: rows
220
+ children:
221
+ - type: section
222
+ label: Resources
223
+ description: Choose the capacity for this run.
224
+ children:
225
+ - { type: field, field: queue }
226
+ - { type: field, field: nodes }
227
+ - type: section
228
+ label: Accounting
229
+ children:
230
+ - { type: field, field: account }
231
+ ```
232
+
233
+ References are local names, not dotted paths. To arrange a group's children,
234
+ put a layout in that group's `items.$meta`; a list uses `template.$meta` and a
235
+ wizard page uses `options.$meta`. Keep `$meta.wizard` on the wizard container
236
+ and place layouts inside its pages. Layout and wizard metadata do not share
237
+ the same scope.
238
+
239
+ Unreferenced inputs render after the layout in schema order. Field visibility
240
+ keeps its existing semantics; empty layout containers disappear, and siblings
241
+ remain in their declared containers. Explicit layouts take precedence over
242
+ field `width` and `anchor-below` hints within that scope. Those hints keep their
243
+ existing behavior when no explicit layout is set.
244
+
245
+ Malformed layouts, unknown references, and duplicate references fall back to
246
+ the existing form layout, showing each input once. Hosts can call
247
+ `resolveFormLayout(layout, fieldNames)` from `@parallelworks/ui/form` to get
248
+ structured `issues` with node paths before saving a layout. Each layout scope is
249
+ limited to 1,024 nodes, 128 children per container, 64 nesting levels below the
250
+ root, and 65,536 total CSS UTF-16 code units. Names and section labels are limited
251
+ to 256 code units; descriptions to 4,096. Oversized layouts fall back to ordinary
252
+ fields. These renderer limits supplement host limits on workflow request size,
253
+ parsing, expression evaluation, and the number of input scopes. Authors can edit
254
+ this metadata in YAML; drag-and-drop layout authoring is a separate consumer
255
+ of the same contract.
256
+
257
+ ### Optional layout CSS
258
+
259
+ Hosts can enable `allowLayoutCSS` on `DynamicForm` (default `false`). Each layout
260
+ node can then supply a `css` **declaration block**:
261
+
262
+ ```yaml
263
+ type: field
264
+ field: account
265
+ css: "max-width: 28rem; padding: 0.5rem;"
266
+ ```
267
+
268
+ Columns, spans, responsive breakpoints, and shared rows remain in the structured
269
+ layout. CSS provides spacing, sizing, alignment, surfaces, and typography. It styles
270
+ the content container of grids/stacks, the complete section including its heading,
271
+ and the wrapper of a field. Section titles accept the node's `font-size`,
272
+ `font-weight`, and `line-height`; field controls retain their component sizing.
273
+ No authored selectors target individual inputs, error messages, or surrounding UI.
274
+
275
+ The supported declarations are:
276
+
277
+ - Spacing and sizing: `gap`, `row-gap`, `column-gap`, `padding`, `padding-inline`,
278
+ `padding-block`, the four physical padding longhands, `width`, and `max-width`.
279
+ Nonnegative `px`, `rem`, `em`, `ch`, percentages, and `min()`, `max()`, or `clamp()`
280
+ are accepted where the CSS grammar permits. Lengths are capped at 256 units and
281
+ percentages at 100.
282
+ - Alignment: `align-items`, `justify-items`, `align-self`, `justify-self`, and
283
+ `text-align`. Only the supported keywords accepted by the CSS grammar apply.
284
+ - Surfaces: `color`, `background-color`, `border`, the four physical border
285
+ shorthands, `border-color`, `border-width`, `border-style`, and `border-radius`.
286
+ Colors are opaque three- or six-digit hex values. Borders are limited to 8px
287
+ or 0.5rem; radii to 32px or 2rem. Styles are solid, dashed, dotted, double, or none.
288
+ - Typography: `font-family` (serif, sans-serif, monospace, or the bundled
289
+ `"Geist Sans"` / `"Geist Mono"`), `font-size` (12–96px or 0.75–6rem),
290
+ `font-weight` (100–900), `font-style` (normal/italic), `line-height` (normal or 1–2),
291
+ and `letter-spacing` (0–2px or 0–0.1em).
292
+
293
+ For a complete local theme, supply **both** `--form-surface` and `--form-accent`
294
+ as opaque hex colors in the same block:
295
+
296
+ ```yaml
297
+ type: section
298
+ label: Delivery
299
+ css: >-
300
+ --form-surface: #d4ed7a;
301
+ --form-accent: #25320f;
302
+ padding: 1.5rem;
303
+ border-radius: 1rem;
304
+ font-size: 1.5rem;
305
+ children:
306
+ - type: field
307
+ field: output
308
+ ```
309
+
310
+ These two reserved declarations are color seeds, consumed by the existing theme
311
+ generator. It derives local text, input, focus, disabled, and status colors, plus
312
+ a color scheme for native controls. The generated variables live on that layout
313
+ node and inherit into its descendants; a child can start its own theme. Nothing
314
+ is written to the page root. Omitting the seeds preserves the host theme. A plain
315
+ `background-color` only paints the node; use the seeds when its controls also need
316
+ to follow the new palette. Arbitrary custom properties and direct theme-token
317
+ writes remain prohibited. With CSS enabled, the layout gets a paint containment boundary, so an oversized
318
+ surface cannot paint over adjacent application UI. A small inset preserves focus
319
+ rings. Popovers, tooltips, or dialogs that need to extend outside that boundary
320
+ must use a host-controlled portal and carry their own theme context into it.
321
+ Authors cannot change the boundary or portal destination through layout CSS.
322
+
323
+ CSS is parsed into an AST, checked against both this allowlist and CSS property
324
+ grammars, then applied through React style properties. No authored stylesheet,
325
+ selector, HTML, or style element is inserted. Imports, URLs, arbitrary custom properties,
326
+ `var()`, `attr()`, arbitrary functions, positioning, transforms, negative lengths,
327
+ and `!important` are rejected. Blocks are limited to 4,096 characters and 32
328
+ declarations. Any unsupported or malformed declaration discards the entire node's
329
+ CSS block; the structured layout and all input bindings continue to work.
330
+ `resolveLayoutCSS(css)` returns the accepted style object and diagnostic `issues`
331
+ for authoring tools; validate before saving and surface those issues to authors.
332
+ The workflow schema validates the string's shape and size; it does not authorize
333
+ CSS or replace runtime validation in the renderer.
334
+
335
+ This restricts the styling surface; it is not a sandbox for arbitrary CSS or a
336
+ security certification. Permitted sizing and color overrides can still produce an awkward or unreadable
337
+ form; validate authored designs for contrast and usability.
338
+ The host retains its CSP and its normal workflow publishing permissions. Enabling
339
+ this feature does not require allowing external stylesheets or weakening script
340
+ policy. Treat authors as trusted to design the contents of their own form: permitted
341
+ colors and sizing can obscure labels or make controls hard to use. Keep trusted
342
+ confirmation summaries, permissions, and security notices outside the authored
343
+ layout. Full stylesheets or mutually untrusted embedded applications require a
344
+ separate isolation design.
@@ -1,16 +1,16 @@
1
1
  import { FaCloud } from "../../icons/fa/FaCloud.js";
2
- import { FaExclamationTriangle } from "../../icons/fa/FaExclamationTriangle.js";
3
2
  import { FaPlus } from "../../icons/fa/FaPlus.js";
4
- import { FaTimes } from "../../icons/fa/FaTimes.js";
5
3
  import { keyedByContent } from "../../components/keys.js";
6
4
  import { useChatConfig } from "../core/config.js";
7
5
  import { useChat } from "../core/ChatProvider.js";
8
6
  import BlockedGroupBanner from "./BlockedGroupBanner.js";
9
7
  import { getGreeting } from "../core/greeting.js";
10
8
  import useDragDrop from "../core/useDragDrop.js";
9
+ import { NoticeCard } from "../ui/Notice.js";
11
10
  import { ComposerControls, ConnectToolsLink } from "./ComposerChrome.js";
12
11
  import DragOverlay from "./DragOverlay.js";
13
12
  import ChatInput from "./ChatInput.js";
13
+ import ChatStage from "./ChatStage.js";
14
14
  import ProviderIssueBanner from "./ProviderIssueBanner.js";
15
15
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
16
16
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
@@ -76,99 +76,84 @@ function ChatEmptyState({ targetSession }) {
76
76
  children: [
77
77
  /* @__PURE__ */ jsx(BlockedGroupBanner, {}),
78
78
  /* @__PURE__ */ jsx(ProviderIssueBanner, {}),
79
- sessionUnreachable && !sessionBannerDismissed && /* @__PURE__ */ jsxs("div", {
80
- className: "mx-4 mt-2 flex items-start gap-3 p-3 rounded-lg bg-amber-500/10 border border-amber-500/20",
81
- children: [
82
- /* @__PURE__ */ jsx(FaExclamationTriangle, { className: "w-4 h-4 text-amber-500 flex-shrink-0 mt-0.5" }),
83
- /* @__PURE__ */ jsxs("div", {
84
- className: "flex-1 min-w-0",
85
- children: [/* @__PURE__ */ jsx("p", {
86
- className: "text-sm font-medium text-amber-700",
79
+ sessionUnreachable && !sessionBannerDismissed && /* @__PURE__ */ jsx("div", {
80
+ className: "mx-4 mt-2",
81
+ children: /* @__PURE__ */ jsxs(NoticeCard, {
82
+ tone: "warning",
83
+ onDismiss: () => setSessionBannerDismissed(true),
84
+ dismissLabel: strings.chrome.dismiss,
85
+ children: [
86
+ /* @__PURE__ */ jsx("span", {
87
+ className: "block font-medium",
87
88
  children: t.sessionUnreachableTitle(targetSession?.split(":").pop() ?? "")
88
- }), /* @__PURE__ */ jsx("p", {
89
- className: "text-xs text-amber-600/80 mt-0.5",
89
+ }),
90
+ /* @__PURE__ */ jsx("span", {
91
+ className: "mt-0.5 block",
90
92
  children: t.sessionUnreachableBody
91
- })]
92
- }),
93
- /* @__PURE__ */ jsxs("div", {
94
- className: "flex items-center gap-2 flex-shrink-0",
95
- children: [/* @__PURE__ */ jsx("button", {
93
+ }),
94
+ /* @__PURE__ */ jsx("button", {
96
95
  type: "button",
97
96
  onClick: () => refreshModels(),
98
- className: "text-xs font-medium text-amber-700 hover:text-amber-800 hover:bg-amber-500/10 px-2 py-1 rounded transition-colors cursor-pointer",
97
+ className: "mt-1 font-medium underline-offset-2 hover:underline",
99
98
  children: t.retry
100
- }), /* @__PURE__ */ jsx("button", {
101
- type: "button",
102
- onClick: () => setSessionBannerDismissed(true),
103
- className: "text-amber-400 hover:text-amber-600 transition-colors cursor-pointer",
104
- children: /* @__PURE__ */ jsx(FaTimes, { className: "w-3 h-3" })
105
- })]
106
- })
107
- ]
99
+ })
100
+ ]
101
+ })
108
102
  }),
109
103
  /* @__PURE__ */ jsxs("div", {
110
104
  className: "flex-1 flex flex-col relative bg-(--theme-panel-bg)",
111
105
  ...attachmentsAvailable ? dragHandlers : {},
112
- children: [isDragging && attachmentsAvailable && /* @__PURE__ */ jsx(DragOverlay, { className: "rounded-2xl m-4" }), /* @__PURE__ */ jsx("div", {
113
- className: "flex-1 flex flex-col items-center justify-center pt-8 pb-[12vh]",
114
- children: /* @__PURE__ */ jsxs("div", {
115
- className: "w-full",
116
- children: [
117
- !(hasLoadedModels && models.length === 0) && /* @__PURE__ */ jsxs("div", {
118
- className: "mx-auto mb-6 max-w-[var(--chat-column,50rem)] px-8 text-center",
119
- children: [/* @__PURE__ */ jsx("h1", {
120
- className: "text-[1.75rem] leading-tight font-medium tracking-[-0.02em] text-balance text-(--theme-panel)",
121
- children: greeting
122
- }), /* @__PURE__ */ jsx("p", {
123
- className: "mt-2 text-sm theme-muted-text",
124
- children: t.modernSubtitle
125
- })]
126
- }),
127
- hasLoadedModels && models.length === 0 && /* @__PURE__ */ jsxs("div", {
128
- className: "mx-auto mb-6 flex max-w-md flex-col items-center gap-3 px-8 text-center",
129
- children: [
130
- /* @__PURE__ */ jsx(FaCloud, { className: "h-10 w-10 theme-muted-text opacity-60" }),
131
- /* @__PURE__ */ jsx("p", {
132
- className: "text-lg font-medium theme-text",
133
- children: t.noProvidersTitle
134
- }),
135
- /* @__PURE__ */ jsx("p", {
136
- className: "text-sm theme-muted-text",
137
- children: t.noProvidersBody
138
- }),
139
- extraLinks.addProvider && /* @__PURE__ */ jsxs(LinkComponent, {
140
- target: {
141
- kind: "external",
142
- href: extraLinks.addProvider
143
- },
144
- className: "mt-1 inline-flex items-center gap-2 px-4 py-2 rounded-full transition-opacity text-sm font-medium theme-element hover:opacity-90",
145
- children: [/* @__PURE__ */ jsx(FaPlus, { className: "h-3 w-3" }), t.addProvider]
146
- })
147
- ]
148
- }),
149
- /* @__PURE__ */ jsx(ChatInput, {
150
- ref: inputRef,
151
- onSend: handleSendMessage,
152
- disabled: isLoading || isLoadingModels || models.length === 0,
153
- placeholder: isLoadingModels ? tInput.placeholderLoading : hasLoadedModels && models.length === 0 ? tInput.placeholderNoProviders : tInput.placeholder,
154
- settingsLeft: /* @__PURE__ */ jsx(ConnectToolsLink, {}),
155
- settingsRight: /* @__PURE__ */ jsxs(Fragment$1, { children: [slots.composerUsage, /* @__PURE__ */ jsx(ComposerControls, { targetSession })] })
156
- }),
157
- prompts.length > 0 && !(hasLoadedModels && models.length === 0) && /* @__PURE__ */ jsx("ul", {
158
- className: "mx-auto max-w-[var(--chat-column,50rem)] px-8",
159
- children: keyedByContent(prompts, (p) => p).map(({ key, item: prompt }) => /* @__PURE__ */ jsx("li", {
160
- className: "border-b theme-border last:border-b-0",
161
- children: /* @__PURE__ */ jsx("button", {
162
- type: "button",
163
- onClick: () => handleSuggestedPrompt(prompt),
164
- disabled: isLoading || isLoadingModels || models.length === 0,
165
- className: "-mx-3 my-1 block w-[calc(100%+1.5rem)] text-pretty rounded-lg px-3 py-2 text-left text-sm theme-muted-text transition-colors hover:chat-tint hover:theme-text disabled:opacity-50 disabled:cursor-not-allowed",
166
- children: prompt
167
- })
168
- }, key))
169
- })
170
- ]
171
- })
106
+ children: [isDragging && attachmentsAvailable && /* @__PURE__ */ jsx(DragOverlay, { className: "rounded-2xl m-4" }), /* @__PURE__ */ jsxs(ChatStage, {
107
+ ...hasLoadedModels && models.length === 0 ? {} : {
108
+ heading: greeting,
109
+ subheading: t.modernSubtitle
110
+ },
111
+ children: [
112
+ hasLoadedModels && models.length === 0 && /* @__PURE__ */ jsxs("div", {
113
+ className: "mx-auto mb-6 flex max-w-md flex-col items-center gap-3 px-8 text-center",
114
+ children: [
115
+ /* @__PURE__ */ jsx(FaCloud, { className: "h-10 w-10 theme-muted-text opacity-60" }),
116
+ /* @__PURE__ */ jsx("p", {
117
+ className: "text-lg font-medium theme-text",
118
+ children: t.noProvidersTitle
119
+ }),
120
+ /* @__PURE__ */ jsx("p", {
121
+ className: "text-sm theme-muted-text",
122
+ children: t.noProvidersBody
123
+ }),
124
+ extraLinks.addProvider && /* @__PURE__ */ jsxs(LinkComponent, {
125
+ target: {
126
+ kind: "external",
127
+ href: extraLinks.addProvider
128
+ },
129
+ className: "mt-1 inline-flex items-center gap-2 px-4 py-2 rounded-full transition-opacity text-sm font-medium theme-element hover:opacity-90",
130
+ children: [/* @__PURE__ */ jsx(FaPlus, { className: "h-3 w-3" }), t.addProvider]
131
+ })
132
+ ]
133
+ }),
134
+ /* @__PURE__ */ jsx(ChatInput, {
135
+ ref: inputRef,
136
+ reserveContext: true,
137
+ onSend: handleSendMessage,
138
+ disabled: isLoading || isLoadingModels || models.length === 0,
139
+ placeholder: isLoadingModels ? tInput.placeholderLoading : hasLoadedModels && models.length === 0 ? tInput.placeholderNoProviders : tInput.placeholder,
140
+ settingsLeft: /* @__PURE__ */ jsx(ConnectToolsLink, {}),
141
+ settingsRight: /* @__PURE__ */ jsxs(Fragment$1, { children: [slots.composerUsage, /* @__PURE__ */ jsx(ComposerControls, { targetSession })] })
142
+ }),
143
+ prompts.length > 0 && !(hasLoadedModels && models.length === 0) && /* @__PURE__ */ jsx("ul", {
144
+ className: "mx-auto max-w-[var(--chat-column,50rem)] px-8",
145
+ children: keyedByContent(prompts, (p) => p).map(({ key, item: prompt }) => /* @__PURE__ */ jsx("li", {
146
+ className: "border-b theme-border last:border-b-0",
147
+ children: /* @__PURE__ */ jsx("button", {
148
+ type: "button",
149
+ onClick: () => handleSuggestedPrompt(prompt),
150
+ disabled: isLoading || isLoadingModels || models.length === 0,
151
+ className: "-mx-3 my-1 block w-[calc(100%+1.5rem)] text-pretty rounded-lg px-3 py-2 text-left text-sm theme-muted-text transition-colors hover:chat-tint hover:theme-text disabled:opacity-50 disabled:cursor-not-allowed",
152
+ children: prompt
153
+ })
154
+ }, key))
155
+ })
156
+ ]
172
157
  })]
173
158
  })
174
159
  ]
@@ -8,9 +8,6 @@ interface ChatInputProps {
8
8
  placeholder?: string | undefined;
9
9
  conversationId?: string;
10
10
  onEditLastMessage?: () => void;
11
- /** Only the box: no page padding, width cap, or settings row, for a host
12
- * that lays out its own controls around it. */
13
- flush?: boolean;
14
11
  /** Send with nothing typed, for a surface where the text is optional. */
15
12
  allowEmpty?: boolean;
16
13
  /** Attach, paste, and drop files. Off for a target that takes text only. */
@@ -23,10 +20,21 @@ interface ChatInputProps {
23
20
  sendTestId?: string;
24
21
  /** Offers the host's slash commands as the input starts with /. */
25
22
  slash?: SlashMenuConfig | undefined;
23
+ /** Cards above everything else, such as a warning about this turn. */
24
+ notices?: ReactNode;
25
+ /** Sits where a terminal's spinner line would, just above the box. */
26
+ activity?: ReactNode;
26
27
  /** Pills naming where the turn runs, rendered above the input. */
27
28
  context?: ReactNode;
29
+ /** Keeps the row of pills above the box even with none, so the box sits
30
+ * where it does on a surface that has them. */
31
+ reserveContext?: boolean;
32
+ /** Under the box, on the left. */
28
33
  settingsLeft?: ReactNode;
34
+ /** In the box beside send. Defaults to the model and allocation pickers. */
29
35
  settingsRight?: ReactNode;
36
+ /** One centered line under everything. */
37
+ hint?: string | undefined;
30
38
  /** A turn the chat provider does not run, such as an agent session's: it
31
39
  * drives the Stop button in place of the provider's stream and queue. */
32
40
  turn?: {
@@ -5,7 +5,7 @@ import { useChatConfig } from "../core/config.js";
5
5
  import { useChat } from "../core/ChatProvider.js";
6
6
  import AttachmentUpload from "./AttachmentUpload.js";
7
7
  import useDragDrop from "../core/useDragDrop.js";
8
- import { ComposerContext, ComposerSettings } from "./ComposerChrome.js";
8
+ import { ComposerContext, ComposerControls, ComposerSettings } from "./ComposerChrome.js";
9
9
  import DragOverlay from "./DragOverlay.js";
10
10
  import { PasteFileCard } from "./PastedText.js";
11
11
  import { SlashMenu, useSlashMenu } from "./SlashMenu.js";
@@ -88,7 +88,7 @@ function streamControls(chat, turn) {
88
88
  queuedCount: chat.queuedMessages.length
89
89
  };
90
90
  }
91
- var ChatInput = forwardRef(function ChatInput({ onSend, disabled = false, placeholder, conversationId, onEditLastMessage, flush = false, allowEmpty = false, attachments: attachmentsWanted = true, requireModel = true, inputLabel, sendLabel, sendTestId, slash, context, settingsLeft, settingsRight, turn, pastes }, ref) {
91
+ var ChatInput = forwardRef(function ChatInput({ onSend, disabled = false, placeholder, conversationId, onEditLastMessage, allowEmpty = false, attachments: attachmentsWanted = true, requireModel = true, inputLabel, sendLabel, sendTestId, slash, notices, activity, context, reserveContext = false, settingsLeft, settingsRight = /* @__PURE__ */ jsx(ComposerControls, {}), hint, turn, pastes }, ref) {
92
92
  const [input, setInput] = useState(() => readDraft(conversationId));
93
93
  const { cards, handleTextPaste, removeCard, takeCards, blocking: pastesBlocking } = usePasteCards(pastes, input, (value) => {
94
94
  setInput(value);
@@ -200,7 +200,7 @@ var ChatInput = forwardRef(function ChatInput({ onSend, disabled = false, placeh
200
200
  if (pastes && handleTextPaste(e.clipboardData.getData("text/plain"))) e.preventDefault();
201
201
  };
202
202
  const { isDragging, dragHandlers } = useDragDrop({ onFilesDropped: handleFilesDropped });
203
- const toolbar = attachmentsAvailable || !flush && !!settingsRight;
203
+ const toolbar = attachmentsAvailable || !!settingsRight;
204
204
  const action = isStreaming && !input.trim() ? /* @__PURE__ */ jsx("button", {
205
205
  type: "button",
206
206
  onClick: stopStreaming,
@@ -217,12 +217,16 @@ var ChatInput = forwardRef(function ChatInput({ onSend, disabled = false, placeh
217
217
  testId: sendTestId
218
218
  });
219
219
  return /* @__PURE__ */ jsxs("div", {
220
- className: cx("relative", !flush && "chat-composer-dock px-4 pb-5 pt-2 bg-(--theme-panel-bg)"),
220
+ className: "chat-composer-dock relative bg-(--theme-panel-bg) px-4 pt-2 pb-5",
221
221
  ...attachmentsAvailable ? dragHandlers : {},
222
222
  children: [isDragging && /* @__PURE__ */ jsx(DragOverlay, { className: "rounded-2xl" }), /* @__PURE__ */ jsxs("div", {
223
- className: cx(!flush && "w-full max-w-[var(--chat-column,50rem)] mx-auto px-4"),
224
- children: [
225
- context && /* @__PURE__ */ jsx(ComposerContext, { children: context }),
223
+ className: "mx-auto w-full max-w-[var(--chat-column,50rem)] space-y-2 px-4",
224
+ children: [notices, /* @__PURE__ */ jsxs("div", { children: [
225
+ activity && /* @__PURE__ */ jsx("div", {
226
+ className: "mb-2",
227
+ children: activity
228
+ }),
229
+ (context || reserveContext) && /* @__PURE__ */ jsx(ComposerContext, { children: context }),
226
230
  cards.length > 0 && /* @__PURE__ */ jsx("div", {
227
231
  className: "mb-2 flex flex-wrap gap-2",
228
232
  "data-testid": "composer-pastes",
@@ -285,7 +289,7 @@ var ChatInput = forwardRef(function ChatInput({ onSend, disabled = false, placeh
285
289
  onClick: () => setShowAttachments(!showAttachments)
286
290
  }), /* @__PURE__ */ jsxs("div", {
287
291
  className: "ml-auto flex min-w-0 items-center gap-1",
288
- children: [!flush && /* @__PURE__ */ jsx("div", {
292
+ children: [/* @__PURE__ */ jsx("div", {
289
293
  className: "flex min-w-0 items-center justify-end gap-1",
290
294
  children: settingsRight
291
295
  }), /* @__PURE__ */ jsx("div", {
@@ -297,11 +301,15 @@ var ChatInput = forwardRef(function ChatInput({ onSend, disabled = false, placeh
297
301
  ]
298
302
  })]
299
303
  }),
300
- !flush && (settingsLeft || uploadedCount > 0) && /* @__PURE__ */ jsx(ComposerSettings, { left: /* @__PURE__ */ jsxs(Fragment$1, { children: [settingsLeft, uploadedCount > 0 && /* @__PURE__ */ jsx("span", {
304
+ (settingsLeft || uploadedCount > 0) && /* @__PURE__ */ jsx(ComposerSettings, { left: /* @__PURE__ */ jsxs(Fragment$1, { children: [settingsLeft, uploadedCount > 0 && /* @__PURE__ */ jsx("span", {
301
305
  className: "text-[11px] theme-muted-text",
302
306
  children: t.filesAttached(uploadedCount)
303
- })] }) })
304
- ]
307
+ })] }) }),
308
+ hint && /* @__PURE__ */ jsx("p", {
309
+ className: "mt-1 text-center text-[11px] theme-muted-text",
310
+ children: hint
311
+ })
312
+ ] })]
305
313
  })]
306
314
  });
307
315
  });