@kortix/ui 0.14.4 → 0.14.6

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 (228) hide show
  1. package/README.md +8 -4
  2. package/dist/access/access/access-dialog.js +8 -5
  3. package/dist/access/access/copy-row.js +2 -2
  4. package/dist/access/sharing-picker.js +2 -1
  5. package/dist/browser/app-in-browser.js +5 -3
  6. package/dist/browser/browser-panel.js +6 -6
  7. package/dist/browser/sandbox-browser-chrome.d.ts +4 -1
  8. package/dist/browser/sandbox-browser-chrome.js +4 -3
  9. package/dist/customize/agents/agent-access.d.ts +20 -0
  10. package/dist/customize/agents/agent-access.js +17 -0
  11. package/dist/customize/agents/agent-grant-pages.js +5 -6
  12. package/dist/customize/agents/agent-page.js +2 -1
  13. package/dist/customize/agents/agents-page.js +18 -1
  14. package/dist/customize/connectors/add-mcp/add-mcp-server.js +4 -2
  15. package/dist/customize/connectors/connector-routes.d.ts +3 -1
  16. package/dist/customize/connectors/connector-routes.js +18 -0
  17. package/dist/customize/connectors/connectors-page.js +23 -4
  18. package/dist/customize/connectors/detail/connector-advanced-tools.js +4 -6
  19. package/dist/customize/providers/provider-accordion.js +2 -1
  20. package/dist/customize/providers/provider-connect.js +2 -1
  21. package/dist/customize/sections/component/telegram-channel-row.js +1 -1
  22. package/dist/customize/sections/connector-connection-modal.js +11 -9
  23. package/dist/customize/sections/connectors-view.js +11 -13
  24. package/dist/customize/sections/llm-provider/constants.d.ts +0 -1
  25. package/dist/customize/sections/llm-provider/constants.js +0 -2
  26. package/dist/customize/sections/llm-provider/models-tab-group.d.ts +1 -1
  27. package/dist/customize/sections/llm-provider/models-tab-group.js +7 -4
  28. package/dist/customize/sections/llm-provider/models-tab-row.d.ts +2 -18
  29. package/dist/customize/sections/llm-provider/models-tab-row.js +6 -30
  30. package/dist/customize/sections/llm-provider/models-tab.d.ts +0 -1
  31. package/dist/customize/sections/llm-provider/models-tab.js +1 -10
  32. package/dist/customize/sections/llm-provider/use-connected-providers.js +2 -2
  33. package/dist/customize/sections/llm-provider/utils.d.ts +1 -1
  34. package/dist/customize/sections/llm-provider/utils.js +7 -14
  35. package/dist/customize/sections/view/gateway/_shared.js +1 -2
  36. package/dist/customize/sections/view/gateway/gateway-routing.js +18 -20
  37. package/dist/customize/sections/view/sandbox-provider-coverage.d.ts +3 -18
  38. package/dist/customize/sections/view/sandbox-provider-coverage.js +7 -27
  39. package/dist/customize/settings/rail-copy-context.js +5 -3
  40. package/dist/customize/settings/tabs/sandbox-tab.d.ts +2 -27
  41. package/dist/customize/settings/tabs/sandbox-tab.js +13 -112
  42. package/dist/customize/settings/tabs/sandbox-template-form.js +9 -12
  43. package/dist/customize/shared/capability-tabs.js +2 -1
  44. package/dist/customize/shared/entity/entity-modal.d.ts +5 -1
  45. package/dist/customize/shared/entity/entity-modal.js +7 -4
  46. package/dist/customize/skills/remove-skill-button.d.ts +16 -0
  47. package/dist/customize/skills/remove-skill-button.js +42 -0
  48. package/dist/customize/skills/skills-page.js +4 -6
  49. package/dist/customize/triggers/schedule/event-trigger-copy.js +3 -2
  50. package/dist/customize/triggers/schedule/schedule-builder.js +7 -4
  51. package/dist/customize/triggers/schedule/trigger-composer.js +5 -2
  52. package/dist/customize/triggers/schedule/trigger-list.d.ts +0 -2
  53. package/dist/customize/triggers/schedule/trigger-list.js +1 -18
  54. package/dist/customize/triggers/schedule/trigger-tile.d.ts +1 -32
  55. package/dist/customize/triggers/schedule/trigger-tile.js +1 -16
  56. package/dist/customize/triggers/schedule/when-event.js +1 -1
  57. package/dist/diff/diff-view.js +9 -10
  58. package/dist/files/editor/code-editor.js +2 -3
  59. package/dist/files/project/components/change-request-detail-dialog.d.ts +3 -1
  60. package/dist/files/project/components/change-request-detail-dialog.js +3 -5
  61. package/dist/files/project/components/change-request-status-badge.d.ts +12 -0
  62. package/dist/files/project/components/change-request-status-badge.js +27 -0
  63. package/dist/files/project/components/change-requests-panel.js +1 -1
  64. package/dist/files/project/components/drive-explorer.js +2 -1
  65. package/dist/files/project/components/drive-grid-view.js +11 -7
  66. package/dist/files/project/components/drive-list-view.js +4 -4
  67. package/dist/files/project/components/file-breadcrumbs.js +1 -1
  68. package/dist/files/project/components/file-explorer-page.js +1 -1
  69. package/dist/files/project/components/file-history-popover.js +1 -6
  70. package/dist/files/project/components/file-search.js +6 -1
  71. package/dist/files/project/components/open-change-request-dialog.js +2 -1
  72. package/dist/files/project/components/version-selector-logic.d.ts +11 -0
  73. package/dist/files/project/components/version-selector-logic.js +26 -0
  74. package/dist/files/project/components/version-selector.js +11 -20
  75. package/dist/files/project/hooks/use-change-requests.d.ts +14 -1
  76. package/dist/files/project/hooks/use-change-requests.js +44 -25
  77. package/dist/files/project/hooks/use-server-health.js +17 -15
  78. package/dist/files/renderers/docx/docx-viewer.js +4 -6
  79. package/dist/files/renderers/image-renderer.js +6 -24
  80. package/dist/files/renderers/pdf/pdf-renderer.d.ts +3 -1
  81. package/dist/files/renderers/pdf/pdf-renderer.js +2 -2
  82. package/dist/files/renderers/pdf/pdf-viewer.d.ts +3 -0
  83. package/dist/files/renderers/pdf/pdf-viewer.js +16 -7
  84. package/dist/files/renderers/presentation/FullScreenPresentationViewer.js +2 -11
  85. package/dist/files/renderers/show-content-renderer.js +1 -1
  86. package/dist/files/renderers/sqlite-renderer.js +8 -3
  87. package/dist/files/renderers/xlsx/xlsx-viewer.js +15 -16
  88. package/dist/files/tree/components/file-tree-item.js +1 -1
  89. package/dist/files/viewer/file-content-renderer.d.ts +4 -1
  90. package/dist/files/viewer/file-content-renderer.js +2 -2
  91. package/dist/files/viewer/file-preview-modal.js +1 -1
  92. package/dist/files/viewer/use-file-content-state.js +1 -1
  93. package/dist/i18n/en.d.ts +36 -35
  94. package/dist/i18n/en.js +45 -44
  95. package/dist/index.d.ts +1 -1
  96. package/dist/internal/desktop/desktop-back-button.js +5 -3
  97. package/dist/internal/hooks/use-auto-scroll.js +5 -3
  98. package/dist/internal/hooks/use-notification-center.js +21 -15
  99. package/dist/internal/hooks/use-review-session-summary.d.ts +6 -0
  100. package/dist/internal/hooks/use-review-session-summary.js +11 -0
  101. package/dist/internal/lib/browser-error-noise.d.ts +1 -1
  102. package/dist/internal/lib/browser-error-noise.js +1 -1
  103. package/dist/internal/lib/browser-noise/rules/rejections.d.ts +10 -0
  104. package/dist/internal/lib/browser-noise/rules/rejections.js +27 -1
  105. package/dist/internal/lib/provider-branding.js +14 -3
  106. package/dist/internal/lib/trigger-clock.d.ts +42 -0
  107. package/dist/internal/lib/trigger-clock.js +34 -0
  108. package/dist/internal/lib/web-notifications.d.ts +6 -0
  109. package/dist/internal/lib/web-notifications.js +57 -7
  110. package/dist/internal/notifications/notification-bell.d.ts +13 -0
  111. package/dist/internal/notifications/notification-bell.js +1 -1
  112. package/dist/internal/notifications/notification-rows.js +2 -1
  113. package/dist/internal/outcomes/outcome-types.d.ts +5 -0
  114. package/dist/internal/reminders/reminder-filter.d.ts +12 -0
  115. package/dist/internal/reminders/reminder-filter.js +34 -0
  116. package/dist/internal/reminders/reminder-format.d.ts +6 -0
  117. package/dist/internal/reminders/reminder-format.js +20 -2
  118. package/dist/internal/stores/kortix-computer-store.d.ts +1 -1
  119. package/dist/internal/stores/session-hover-store.d.ts +20 -5
  120. package/dist/internal/stores/session-hover-store.js +31 -13
  121. package/dist/internal/stores/session-panel-memory-store.js +2 -0
  122. package/dist/markdown/code/inline-code.js +8 -0
  123. package/dist/markdown/code/output-paths-context.d.ts +18 -0
  124. package/dist/markdown/code/output-paths-context.js +35 -0
  125. package/dist/markdown/genui/components/charts.js +6 -5
  126. package/dist/primitives/count-badge.d.ts +3 -1
  127. package/dist/primitives/count-badge.js +3 -3
  128. package/dist/primitives/glyph-paths-source.js +1 -1
  129. package/dist/primitives/glyph-paths.generated.js +1 -1
  130. package/dist/primitives/map.js +2 -1
  131. package/dist/primitives/preview-image.js +4 -2
  132. package/dist/primitives/textarea.js +1 -1
  133. package/dist/primitives/typed-title.js +12 -14
  134. package/dist/primitives/typing-animation.js +4 -2
  135. package/dist/providers/ui-provider.d.ts +57 -0
  136. package/dist/providers/ui-provider.js +4 -0
  137. package/dist/session/composer/ComposerCard.d.ts +2 -0
  138. package/dist/session/composer/ComposerCard.js +5 -1
  139. package/dist/session/composer/agent-selector.js +17 -5
  140. package/dist/session/composer/composer-chat-input.js +5 -3
  141. package/dist/session/composer/composer-logic.js +8 -7
  142. package/dist/session/composer/composer-toolbar.js +4 -2
  143. package/dist/session/composer/composer-underbar.js +3 -1
  144. package/dist/session/composer/composer.js +34 -5
  145. package/dist/session/composer/draft/composer-draft.js +4 -4
  146. package/dist/session/composer/menus/slash-actions.d.ts +22 -2
  147. package/dist/session/composer/menus/slash-actions.js +20 -0
  148. package/dist/session/composer/menus/slash-items.js +19 -3
  149. package/dist/session/composer/pasted-text.js +3 -2
  150. package/dist/session/composer/stores/queued-draft-store.js +3 -2
  151. package/dist/session/header/reminder-parts.d.ts +294 -0
  152. package/dist/session/header/reminder-parts.js +120 -0
  153. package/dist/session/header/session-reminders-indicator.d.ts +13 -0
  154. package/dist/session/header/session-reminders-indicator.js +142 -35
  155. package/dist/session/media/sandbox-url-detector.js +0 -7
  156. package/dist/session/models/model-availability.d.ts +23 -0
  157. package/dist/session/models/model-availability.js +20 -0
  158. package/dist/session/models/model-grouping.js +1 -4
  159. package/dist/session/models/model-selector.js +23 -14
  160. package/dist/session/models/use-model-connection-gate.js +4 -1
  161. package/dist/session/outcomes/change-request-outcomes.js +24 -3
  162. package/dist/session/outcomes/session-outcomes-provider.d.ts +3 -1
  163. package/dist/session/outcomes/session-outcomes-provider.js +31 -3
  164. package/dist/session/overrides/provider-secret-pool-editor.js +2 -1
  165. package/dist/session/overrides/session-overrides-composer.d.ts +1 -1
  166. package/dist/session/overrides/session-overrides-composer.js +5 -1
  167. package/dist/session/panel/advanced/advanced-panel.js +3 -7
  168. package/dist/session/panel/easy/app-preview.js +12 -2
  169. package/dist/session/panel/easy/easy-panel-logic.d.ts +1 -1
  170. package/dist/session/panel/easy/easy-panel-logic.js +1 -1
  171. package/dist/session/panel/easy/outputs-card.js +2 -1
  172. package/dist/session/panel/easy/panel-card.js +5 -3
  173. package/dist/session/panel/easy/preview-load.d.ts +14 -0
  174. package/dist/session/panel/easy/preview-load.js +15 -0
  175. package/dist/session/panel/easy/step-detail-body.js +2 -7
  176. package/dist/session/panel/mobile-tool-drawer.js +4 -2
  177. package/dist/session/panel/open-session-quick-view.js +2 -1
  178. package/dist/session/panel/panel-fallback.js +8 -4
  179. package/dist/session/panel/session-panel-provider.d.ts +1 -0
  180. package/dist/session/panel/session-panel-provider.js +19 -3
  181. package/dist/session/panel/session-reminders-panel.d.ts +4 -0
  182. package/dist/session/panel/session-reminders-panel.js +109 -0
  183. package/dist/session/panel/shared/use-panel-derivations.js +6 -4
  184. package/dist/session/panel/shared/use-panel-handoffs.d.ts +2 -2
  185. package/dist/session/panel/shared/use-panel-handoffs.js +5 -1
  186. package/dist/session/panel/use-session-panel-layout.js +7 -5
  187. package/dist/session/print/session-print-header.d.ts +0 -10
  188. package/dist/session/print/session-print-header.js +25 -9
  189. package/dist/session/status/session-surface.js +1 -1
  190. package/dist/session/thread/optimistic-turn.d.ts +5 -1
  191. package/dist/session/thread/optimistic-turn.js +8 -6
  192. package/dist/session/thread/session-chat.d.ts +6 -1
  193. package/dist/session/thread/session-chat.js +34 -12
  194. package/dist/session/thread/session-history.js +3 -1
  195. package/dist/session/thread/session-thread.d.ts +10 -1
  196. package/dist/session/thread/session-thread.js +2 -2
  197. package/dist/session/thread/transcript.js +13 -7
  198. package/dist/session/tool/shared/file-list.js +19 -2
  199. package/dist/session/tool/shared/infrastructure-contexts.d.ts +7 -0
  200. package/dist/session/tool/shared/infrastructure-contexts.js +7 -0
  201. package/dist/session/tool/shared/infrastructure-output.js +9 -5
  202. package/dist/session/tool/shared/infrastructure-row-header.js +9 -1
  203. package/dist/session/tool/shared/infrastructure-rows.js +7 -5
  204. package/dist/session/tool/tools/bash-tool.js +10 -6
  205. package/dist/session/tool/tools/web-search-tool.js +25 -3
  206. package/dist/session/turn/burst-step-body.js +4 -0
  207. package/dist/session/turn/recovered-failures.d.ts +8 -0
  208. package/dist/session/turn/recovered-failures.js +32 -0
  209. package/dist/session/turn/streaming-cadence.js +5 -8
  210. package/dist/styles.css +1 -1
  211. package/dist/terminal/session-terminal-panel.js +3 -5
  212. package/dist/theme/theme.js +2 -1
  213. package/dist/workspace/projects/modal/github-import-pickers.js +7 -6
  214. package/dist/workspace/review/review-center.js +13 -10
  215. package/dist/workspace/session-workspace.js +5 -9
  216. package/dist/workspace/sessions/deleted-session-row.js +5 -2
  217. package/dist/workspace/sidebar/footer/project-computer-nav.js +6 -4
  218. package/dist/workspace/sidebar/footer/project-mcp-nav.js +6 -2
  219. package/dist/workspace/sidebar/modal/rename-session-modal.js +8 -3
  220. package/dist/workspace/sidebar/project-session-list.js +52 -55
  221. package/dist/workspace/sidebar/session-brief-hover-card.d.ts +35 -18
  222. package/dist/workspace/sidebar/session-brief-hover-card.js +114 -37
  223. package/dist/workspace/sidebar/session-status-mark.d.ts +15 -1
  224. package/dist/workspace/sidebar/session-status-mark.js +63 -17
  225. package/package.json +7 -3
  226. package/skills/kortix-ui/SKILL.md +1 -1
  227. package/dist/customize/sections/view/sandbox-provider-result.d.ts +0 -47
  228. package/dist/customize/sections/view/sandbox-provider-result.js +0 -79
@@ -163,7 +163,8 @@ function MapMarker({ longitude, latitude, label, children, ...markerOptions }) {
163
163
  marker.setLngLat([longitude, latitude]);
164
164
  }
165
165
  }, [marker, longitude, latitude]);
166
- return _jsx(MarkerContext.Provider, { value: { marker, map }, children: children });
166
+ const markerContextValue = useMemo(() => ({ marker, map }), [marker, map]);
167
+ return _jsx(MarkerContext.Provider, { value: markerContextValue, children: children });
167
168
  }
168
169
  function MarkerContent({ children, className }) {
169
170
  const { marker } = useMarkerContext();
@@ -22,10 +22,12 @@ const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
22
22
  const PreviewImageContent = React.forwardRef(({ className, fileContent, fileName, ...props }, ref) => {
23
23
  const [zoomLevel, setZoomLevel] = React.useState(DEFAULT_ZOOM);
24
24
  const [transformOrigin, setTransformOrigin] = React.useState(DEFAULT_ORIGIN);
25
- React.useEffect(() => {
25
+ const [zoomedContent, setZoomedContent] = React.useState(fileContent);
26
+ if (fileContent !== zoomedContent) {
27
+ setZoomedContent(fileContent);
26
28
  setZoomLevel(DEFAULT_ZOOM);
27
29
  setTransformOrigin(DEFAULT_ORIGIN);
28
- }, [fileContent]);
30
+ }
29
31
  const getTransformOrigin = (event) => {
30
32
  const rect = event.currentTarget.getBoundingClientRect();
31
33
  if (!rect.width || !rect.height) {
@@ -59,7 +59,7 @@ export const useAutosizeTextArea = ({ textAreaRef, triggerAutoSize, maxHeight =
59
59
  clearTimeout(resizeTimeoutRef.current);
60
60
  }
61
61
  };
62
- }, [textAreaRef.current, triggerAutoSize, minHeight, maxHeight, currentHeight, init]);
62
+ }, [textAreaRef, triggerAutoSize, minHeight, maxHeight, currentHeight, init]);
63
63
  };
64
64
  export const Textarea = React.forwardRef(({ maxHeight = Number.MAX_SAFE_INTEGER, minHeight = 52, className, onChange, value, variant: _variant, ...props }, ref) => {
65
65
  const textAreaRef = React.useRef(null);
@@ -19,25 +19,23 @@ import { shouldTypeOnChange, TYPE_SPEED_MS } from './typed-title-logic.js';
19
19
  import { TypingAnimation } from './typing-animation.js';
20
20
  import { cn } from '../lib/utils.js';
21
21
  import { useReducedMotion } from 'motion/react';
22
- import { useCallback, useEffect, useRef, useState } from 'react';
22
+ import { useCallback, useState } from 'react';
23
23
  export { TYPE_SPEED_MS };
24
24
  export function useTypeOnChange(text) {
25
- // The last value this row DISPLAYED, in a ref rather than state: it must not
26
- // itself cause a render, and it has to update in the same pass that decides
27
- // to animate, or a second change arriving mid-type compares against a stale
28
- // value and is missed.
29
- const previousRef = useRef(null);
25
+ // The last value this row DISPLAYED. It updates in the same render pass that
26
+ // decides to animate, or a second change arriving mid-type compares against
27
+ // a stale value and is missed.
28
+ const [previous, setPrevious] = useState(null);
30
29
  const [isTyping, setIsTyping] = useState(false);
31
30
  const [runId, setRunId] = useState(0);
32
31
  const reduceMotion = !!useReducedMotion();
33
- useEffect(() => {
34
- const previous = previousRef.current;
35
- previousRef.current = text;
36
- if (!shouldTypeOnChange({ previous, next: text, reduceMotion }))
37
- return;
38
- setIsTyping(true);
39
- setRunId((id) => id + 1);
40
- }, [text, reduceMotion]);
32
+ if (previous !== text) {
33
+ setPrevious(text);
34
+ if (shouldTypeOnChange({ previous, next: text, reduceMotion })) {
35
+ setIsTyping(true);
36
+ setRunId((id) => id + 1);
37
+ }
38
+ }
41
39
  const stop = useCallback(() => setIsTyping(false), []);
42
40
  return { isTyping, runId, stop };
43
41
  }
@@ -34,12 +34,14 @@ export function TypingAnimation({ children, words, className, duration = 100, ty
34
34
  const deletingSpeed = deleteSpeed ?? typingSpeed / 2;
35
35
  const shouldStart = startOnView ? isInView : true;
36
36
  const animationSourceKey = useMemo(() => (words ? words.join('\u0000') : (children ?? '')), [words, children]);
37
- useEffect(() => {
37
+ const [seenSourceKey, setSeenSourceKey] = useState(animationSourceKey);
38
+ if (animationSourceKey !== seenSourceKey) {
39
+ setSeenSourceKey(animationSourceKey);
38
40
  setDisplayedText('');
39
41
  setCurrentWordIndex(0);
40
42
  setCurrentCharIndex(0);
41
43
  setPhase('typing');
42
- }, [animationSourceKey]);
44
+ }
43
45
  useEffect(() => {
44
46
  let timeout = null;
45
47
  if (shouldStart && wordsToAnimate.length > 0) {
@@ -177,6 +177,63 @@ export interface UiHostConfig {
177
177
  * still enforces each route. Default: true.
178
178
  */
179
179
  accountApiEnabled: boolean;
180
+ /**
181
+ * The composer shows the model picker and the reasoning-effort picker. A
182
+ * host that runs one model for its users sets false: the session runs its
183
+ * resolved default model. Default: true.
184
+ */
185
+ modelPickerEnabled: boolean;
186
+ /** The composer shows the context-usage ring (and its route to the context modal). Default: true. */
187
+ contextMeterEnabled: boolean;
188
+ /**
189
+ * The composer shows the session-overrides gear (sandbox, secrets,
190
+ * connectors, provider keys, folders). A host whose users never see the
191
+ * infrastructure behind a session sets false. Default: true.
192
+ */
193
+ sessionOverridesEnabled: boolean;
194
+ /**
195
+ * The agents the composer's agent picker offers, by name. Unset: every
196
+ * selectable agent. A host that installs its own agents beside the
197
+ * project's defaults lists its own here.
198
+ */
199
+ offeredAgents?: readonly string[];
200
+ /** Display names by agent name, for the agent picker. Unset or missing: the title-cased name. */
201
+ agentLabels?: Readonly<Record<string, string>>;
202
+ /**
203
+ * A host that keeps files in its own store takes the composer's files here:
204
+ * the attach button, a drop and a pasted file. The composer then uploads
205
+ * none of them to the session's attachment store and shows no tray for
206
+ * them. Unset: they upload as prompt attachments.
207
+ */
208
+ takeFiles?: (files: File[]) => void;
209
+ /** The attach picker's `accept` list. Unset: images, PDF, text and code files. */
210
+ attachAccept?: string;
211
+ /**
212
+ * The composer's `/` menu lists the session runtime's skills and commands,
213
+ * and the Compact session action. A host that hides the harness from its
214
+ * users sets false. Default: true.
215
+ */
216
+ runtimeSlashEnabled: boolean;
217
+ /**
218
+ * Rows the host adds to the composer's `/` menu, under their own headings.
219
+ * A hook, so it may read the host's own data. Unset: none.
220
+ */
221
+ useSlashActions?: () => readonly UiSlashAction[];
222
+ }
223
+ /**
224
+ * A row the host adds to the composer's `/` menu. Selecting it deletes the
225
+ * `/` text and calls `onSelect`. When `onSelect` returns `{ insertText }`,
226
+ * the composer inserts that text at the caret.
227
+ */
228
+ export interface UiSlashAction {
229
+ id: string;
230
+ label: string;
231
+ description: string;
232
+ /** The heading the row sits under, for example "Workflows". */
233
+ group: string;
234
+ onSelect: () => void | {
235
+ insertText: string;
236
+ };
180
237
  }
181
238
  /** What the session browser frames: a port and path inside the session's sandbox, or a URL. */
182
239
  export interface UiPreviewRequest {
@@ -59,6 +59,10 @@ export const defaultUiHostConfig = {
59
59
  commandPaletteShortcut: false,
60
60
  settingsShortcut: false,
61
61
  accountApiEnabled: true,
62
+ modelPickerEnabled: true,
63
+ contextMeterEnabled: true,
64
+ sessionOverridesEnabled: true,
65
+ runtimeSlashEnabled: true,
62
66
  };
63
67
  const useNoViewer = () => undefined;
64
68
  /** A viewer hook built from an id hook: `{ id }`, stable per id. */
@@ -16,6 +16,8 @@ import type { AttachedFile } from './types.js';
16
16
  * phase 1).
17
17
  */
18
18
  export interface ComposerCardDerived {
19
+ /** The commands the `/` menu lists: the `commands` prop, or none when the host hides the runtime's (`runtimeSlashEnabled`). */
20
+ commands: NonNullable<SessionChatInputProps['commands']>;
19
21
  tHardcodedUi: ReturnType<typeof useTranslations>;
20
22
  isDragOver: boolean;
21
23
  /** Unset inside a `ComposerDropZone`: the zone owns every file drag. */
@@ -10,6 +10,9 @@ import { EMPTY_AGENTS, EMPTY_COMMANDS, EMPTY_MODELS, EMPTY_SLASH_FILES, EMPTY_VA
10
10
  import { shouldFocusEditorFromPadding } from './composer-logic.js';
11
11
  import { ComposerToolbar } from './composer-toolbar.js';
12
12
  import { ComposerUnderbar } from './composer-underbar.js';
13
+ import { useUiHostConfig } from '../../providers/ui-provider.js';
14
+ /** What the attach picker offers when the host names no list. */
15
+ const DEFAULT_ATTACH_ACCEPT = 'image/*,.pdf,.txt,.md,.json,.csv,.xml,.yaml,.yml,.toml,.js,.ts,.jsx,.tsx,.py,.rb,.go,.rs,.java,.c,.cpp,.h,.css,.html,.vue,.svelte,.log,.sql,.zip,.tar,.gz,.rar';
13
16
  const ComposerEditorLazy = lazy(() => import('./editor/composer-editor.js').then((mod) => ({ default: mod.ComposerEditor })));
14
17
  function ComposerEditorFallback() {
15
18
  return _jsx("div", { className: "min-h-[1.5em]", "aria-hidden": true });
@@ -20,6 +23,7 @@ export function ComposerCard({ tHardcodedUi, isDragOver, handleDragEnter, handle
20
23
  // values, not the component's defaulted locals.
21
24
  agents = EMPTY_AGENTS, commands = EMPTY_COMMANDS, slashFiles = EMPTY_SLASH_FILES, models = EMPTY_MODELS, notice = null, disabled = false, lockForQuestion = false, lockForApproval = false, questionButtonLabel = null, questionActive = false, submitLabel = null, questionCanAct = true, escCount = 0, isBusy = false, isSending = false, onStop, stopDisabled = false, canStop = true, modelsLoading = false, modelRequired = false, onModelChange, modelDefaultControls, providers, variants = EMPTY_VARIANTS, selectedVariant = null, onVariantChange, projectId, toolbarSlot, rewind, selectedAgent = null, onAgentChange, noAccessibleAgents = false, onContextClick, messages, underbarPlacement = 'below', slashMenuPlacement = 'above', cardClassName, onArrowUpAtStart, }) {
22
25
  const cardRef = useRef(null);
26
+ const { attachAccept } = useUiHostConfig();
23
27
  const inlineUnderbar = underbarPlacement === 'inline';
24
28
  return (_jsxs(_Fragment, { children: [_jsxs("div", { ref: cardRef, onDragEnter: handleDragEnter, onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleDropFiles, className: cn(
25
29
  // One shadow, from the ladder. `shadow-card` is defined nowhere in
@@ -47,7 +51,7 @@ agents = EMPTY_AGENTS, commands = EMPTY_COMMANDS, slashFiles = EMPTY_SLASH_FILES
47
51
  // the div and immediately fights the caret we are placing.
48
52
  e.preventDefault();
49
53
  editorRef.current?.focus();
50
- }, children: [_jsx(AnimatedComposerPlaceholder, { placeholder: editorPlaceholder, active: animatePlaceholder }), _jsx(Suspense, { fallback: _jsx(ComposerEditorFallback, {}), children: _jsx(ComposerEditorLazy, { ref: setEditorRef, placeholder: animatePlaceholder ? '' : editorPlaceholder, disabled: editorDisabled, onSubmit: handleSubmit, onArrowUpAtStart: onArrowUpAtStart ? handleArrowUpAtStart : undefined, onEmptyChange: setIsEmpty, onDocChange: handleDocChange, agents: agents, sessions: allSessions ?? [], currentSessionId: sessionId, commands: commands, actions: slashActions, files: slashFiles, onSelectAction: handleSelectAction, slashDockSelector: `#${dockId}`, onMenuOpenChange: setMenuOpen }) })] }), _jsx("input", { ref: fileInputRef, type: "file", accept: "image/*,.pdf,.txt,.md,.json,.csv,.xml,.yaml,.yml,.toml,.js,.ts,.jsx,.tsx,.py,.rb,.go,.rs,.java,.c,.cpp,.h,.css,.html,.vue,.svelte,.log,.sql,.zip,.tar,.gz,.rar", multiple: true, className: "hidden", onChange: handleFileSelect }), _jsx(ComposerToolbar, { leading: inlineUnderbar ? (_jsx(ComposerUnderbar, { variant: "inline", onAttachClick: handleAttachClick, agents: primaryAgents, selectedAgent: selectedAgent, onAgentChange: onAgentChange, agentMenuOpen: agentMenuOpen, onAgentMenuOpenChange: setAgentMenuOpen, noAccessibleAgents: noAccessibleAgents, messages: messages, models: models, selectedModel: availableSelectedModel, onContextClick: onContextClick })) : null, modelsLoading: modelsLoading, models: models, selectedModel: availableSelectedModel, onModelChange: onModelChange, modelDefaultControls: modelDefaultControls, providers: providers, modelRequired: modelRequired, modelMenuOpen: modelMenuOpen, onModelMenuOpenChange: setModelMenuOpen, reasoningMenuOpen: reasoningMenuOpen, onReasoningMenuOpenChange: setReasoningMenuOpen, variants: variants, selectedVariant: selectedVariant, onVariantChange: onVariantChange, projectId: projectId,
54
+ }, children: [_jsx(AnimatedComposerPlaceholder, { placeholder: editorPlaceholder, active: animatePlaceholder }), _jsx(Suspense, { fallback: _jsx(ComposerEditorFallback, {}), children: _jsx(ComposerEditorLazy, { ref: setEditorRef, placeholder: animatePlaceholder ? '' : editorPlaceholder, disabled: editorDisabled, onSubmit: handleSubmit, onArrowUpAtStart: onArrowUpAtStart ? handleArrowUpAtStart : undefined, onEmptyChange: setIsEmpty, onDocChange: handleDocChange, agents: agents, sessions: allSessions ?? [], currentSessionId: sessionId, commands: commands, actions: slashActions, files: slashFiles, onSelectAction: handleSelectAction, slashDockSelector: `#${dockId}`, onMenuOpenChange: setMenuOpen }) })] }), _jsx("input", { ref: fileInputRef, type: "file", accept: attachAccept ?? DEFAULT_ATTACH_ACCEPT, multiple: true, className: "hidden", onChange: handleFileSelect }), _jsx(ComposerToolbar, { leading: inlineUnderbar ? (_jsx(ComposerUnderbar, { variant: "inline", onAttachClick: handleAttachClick, agents: primaryAgents, selectedAgent: selectedAgent, onAgentChange: onAgentChange, agentMenuOpen: agentMenuOpen, onAgentMenuOpenChange: setAgentMenuOpen, noAccessibleAgents: noAccessibleAgents, messages: messages, models: models, selectedModel: availableSelectedModel, onContextClick: onContextClick })) : null, modelsLoading: modelsLoading, models: models, selectedModel: availableSelectedModel, onModelChange: onModelChange, modelDefaultControls: modelDefaultControls, providers: providers, modelRequired: modelRequired, modelMenuOpen: modelMenuOpen, onModelMenuOpenChange: setModelMenuOpen, reasoningMenuOpen: reasoningMenuOpen, onReasoningMenuOpenChange: setReasoningMenuOpen, variants: variants, selectedVariant: selectedVariant, onVariantChange: onVariantChange, projectId: projectId,
51
55
  // Inline placement has no under-row, so the slot (the session
52
56
  // overrides gear, meta indicator) rides the toolbar itself. With
53
57
  // the 'below' placement the ComposerUnderbar further down renders
@@ -5,6 +5,7 @@ import { Button } from '../../primitives/button.js';
5
5
  import { CommandGroup, CommandInput, CommandItem, CommandList, CommandPopover, CommandPopoverContent, CommandPopoverTrigger, CommandSeparator, } from '../../primitives/command.js';
6
6
  import Hint from '../../primitives/hint.js';
7
7
  import { useTranslations } from '../../i18n/index.js';
8
+ import { useUiHostConfig } from '../../providers/ui-provider.js';
8
9
  import { cn } from '../../lib/utils.js';
9
10
  import { capitalizeWords, isMetaAgentName, META_AGENT_DISPLAY_NAME } from '../../_vendor/kortix-shared.js';
10
11
  import { CaretDownIcon, CheckIcon, SparkleIcon } from '@phosphor-icons/react';
@@ -42,7 +43,16 @@ export function AgentSelector({ agents, selectedAgent, onSelect, disabled = fals
42
43
  const open = controlledOpen ?? internalOpen;
43
44
  const setOpen = onOpenChange ?? setInternalOpen;
44
45
  const [search, setSearch] = useState('');
45
- const primaryAgents = useMemo(() => composerSelectableAgents(agents), [agents]);
46
+ const { offeredAgents, agentLabels } = useUiHostConfig();
47
+ // A host that lists its agents (`offeredAgents`) offers only those; it names them with `agentLabels`.
48
+ const offered = useMemo(() => {
49
+ if (!offeredAgents)
50
+ return agents;
51
+ const offeredNames = new Set(offeredAgents);
52
+ return agents.filter((a) => offeredNames.has(a.name));
53
+ }, [agents, offeredAgents]);
54
+ const primaryAgents = useMemo(() => composerSelectableAgents(offered), [offered]);
55
+ const label = (name) => agentLabels?.[name] ?? (isMetaAgentName(name) ? META_AGENT_DISPLAY_NAME : capitalizeWords(name));
46
56
  // A `flash` state, a `prevAgentRef` feeding it, two effects and a 400ms
47
57
  // timer used to live here. Nothing ever READ `flash` — it was written on
48
58
  // every agent change and rendered nowhere, so the whole apparatus bought a
@@ -55,8 +65,10 @@ export function AgentSelector({ agents, selectedAgent, onSelect, disabled = fals
55
65
  const q = search.toLowerCase().trim();
56
66
  if (!q)
57
67
  return primaryAgents;
58
- return primaryAgents.filter((a) => a.name.toLowerCase().includes(q) || (a.description || '').toLowerCase().includes(q));
59
- }, [primaryAgents, search]);
68
+ return primaryAgents.filter((a) => a.name.toLowerCase().includes(q) ||
69
+ (agentLabels?.[a.name] ?? '').toLowerCase().includes(q) ||
70
+ (a.description || '').toLowerCase().includes(q));
71
+ }, [primaryAgents, search, agentLabels]);
60
72
  const filteredMeta = useMemo(() => filteredPrimary.filter((a) => isMetaAgentName(a.name)), [filteredPrimary]);
61
73
  const filteredProject = useMemo(() => filteredPrimary.filter((a) => !isMetaAgentName(a.name)), [filteredPrimary]);
62
74
  // Keyed off the FULL list, never the filtered one: deriving it from
@@ -71,7 +83,7 @@ export function AgentSelector({ agents, selectedAgent, onSelect, disabled = fals
71
83
  // fallback stays only for a truly unresolved selection (no name at all),
72
84
  // matching the resolver's own first-accessible pre-selection.
73
85
  const displayedName = resolveDisplayedAgentName(primaryAgents, selectedAgent);
74
- const displayName = agentDisplayLabel(agents, selectedAgent);
86
+ const displayName = displayedName ? label(displayedName) : agentDisplayLabel(offered, selectedAgent);
75
87
  const metaSelected = isMetaAgentName(displayedName);
76
88
  /**
77
89
  * One agent: name, its type, its description, and a check when it is the
@@ -102,7 +114,7 @@ export function AgentSelector({ agents, selectedAgent, onSelect, disabled = fals
102
114
  }, children: [meta && (
103
115
  // Meta is the platform agent that runs the others, so its row leads
104
116
  // with its own mark instead of reading as one more peer in the list.
105
- _jsx("span", { className: "bg-muted flex size-8 shrink-0 items-center justify-center rounded-md", children: _jsx(SparkleIcon, { weight: "fill", className: "text-foreground size-4" }) })), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-1.5", children: [_jsx("span", { className: "text-foreground truncate text-sm font-medium", children: isMetaAgentName(agent.name) ? META_AGENT_DISPLAY_NAME : capitalizeWords(agent.name) }), meta && (_jsx(Badge, { variant: "outline", size: "xs", className: "shrink-0 font-normal", children: tHardcodedUi.raw('i18nComplete.textc78ffe195710') }))] }), agent.description && (_jsx("p", { className: "text-muted-foreground mt-0.5 truncate text-xs", children: agent.description }))] }), isSelected && (
117
+ _jsx("span", { className: "bg-muted flex size-8 shrink-0 items-center justify-center rounded-md", children: _jsx(SparkleIcon, { weight: "fill", className: "text-foreground size-4" }) })), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-1.5", children: [_jsx("span", { className: "text-foreground truncate text-sm font-medium", children: label(agent.name) }), meta && (_jsx(Badge, { variant: "outline", size: "xs", className: "shrink-0 font-normal", children: tHardcodedUi.raw('i18nComplete.textc78ffe195710') }))] }), agent.description && (_jsx("p", { className: "text-muted-foreground mt-0.5 truncate text-xs", children: agent.description }))] }), isSelected && (
106
118
  // `mt-0.5` optically centres the 16px check against the first line
107
119
  // rather than the top edge of the text box.
108
120
  _jsx(CheckIcon, { className: "text-foreground mt-0.5 size-4 shrink-0" }))] }, agent.name));
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useCallback, useEffect, useMemo, useState } from 'react';
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
4
4
  import { SessionOverridesComposer } from '../overrides/session-overrides-composer.js';
5
5
  import { SessionChatInput, } from './session-chat-input.js';
6
6
  import { useProjectConfig, useRuntimeAgents, useRuntimeCommands, useRuntimeConfig, useRuntimeProviders, useSessionModelSelection } from '@kortix/sdk/react';
@@ -103,10 +103,11 @@ export function ComposerChatInput({ onSend, onCommand, sessionId, projectId, isB
103
103
  useEffect(() => {
104
104
  onAgentSelectionChange?.(selectedAgentName);
105
105
  }, [onAgentSelectionChange, selectedAgentName]);
106
- const [newSessionScope, setNewSessionScope] = useState(null);
106
+ // Read only by `options()` at send time, so a ref: a commit needs no render.
107
+ const newSessionScopeRef = useRef(null);
107
108
  const [newProviderSecretPools, setNewProviderSecretPools] = useState({});
108
109
  const handleCommittedScope = useCallback((commit) => {
109
- setNewSessionScope(commit ? { agentName: selectedAgentName, commit } : null);
110
+ newSessionScopeRef.current = commit ? { agentName: selectedAgentName, commit } : null;
110
111
  }, [selectedAgentName]);
111
112
  // The axes a session can override that have no control of their own —
112
113
  // secrets, connectors, sandbox. Agent, model and effort are NOT passed: this
@@ -126,6 +127,7 @@ export function ComposerChatInput({ onSend, onCommand, sessionId, projectId, isB
126
127
  o.model = local.model.currentKey;
127
128
  if (local.model.variant.current)
128
129
  o.variant = local.model.variant.current;
130
+ const newSessionScope = newSessionScopeRef.current;
129
131
  if (!sessionId && newSessionScope && newSessionScope.agentName === selectedAgentName) {
130
132
  o.scope = newSessionScope.commit;
131
133
  }
@@ -51,11 +51,7 @@ export function extractReplyQuotes(text) {
51
51
  const { quotes } = parseReplyContexts(text);
52
52
  if (quotes.length === 0)
53
53
  return { quotes: [], text };
54
- const unique = [];
55
- for (const quote of quotes) {
56
- if (quote && !unique.includes(quote))
57
- unique.push(quote);
58
- }
54
+ const unique = [...new Set(quotes.filter(Boolean))];
59
55
  return { quotes: unique, text: stripReplyContexts(text) };
60
56
  }
61
57
  /**
@@ -96,12 +92,16 @@ export function mergeComposerQuotes(restored, current) {
96
92
  * returns `current` itself.
97
93
  */
98
94
  export function restoreComposerQuotes(current, texts, makeId) {
95
+ const currentByText = new Map();
96
+ for (const quote of current)
97
+ if (!currentByText.has(quote.text))
98
+ currentByText.set(quote.text, quote);
99
99
  const restored = [];
100
100
  for (const raw of texts) {
101
101
  const text = raw.trim();
102
102
  if (!text || restored.some((quote) => quote.text === text))
103
103
  continue;
104
- restored.push(current.find((quote) => quote.text === text) ?? { id: makeId(), text });
104
+ restored.push(currentByText.get(text) ?? { id: makeId(), text });
105
105
  }
106
106
  if (restored.length === 0)
107
107
  return current;
@@ -249,7 +249,8 @@ export function planQuoteRequests({ requests, appliedIds, }) {
249
249
  * itself when nothing matches, so a no-op ack does not re-render.
250
250
  */
251
251
  export function acknowledgeQuoteRequests(requests, ids) {
252
- const remaining = requests.filter((request) => !ids.includes(request.id));
252
+ const acked = new Set(ids);
253
+ const remaining = requests.filter((request) => !acked.has(request.id));
253
254
  return remaining.length === requests.length ? requests : remaining;
254
255
  }
255
256
  /**
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useTranslations } from '../../i18n/index.js';
4
+ import { useUiHostConfig } from '../../providers/ui-provider.js';
4
5
  import { ArrowCounterClockwiseIcon } from '@phosphor-icons/react';
5
6
  import { Button } from '../../primitives/button.js';
6
7
  import { HoverCard, HoverCardContent, HoverCardTrigger } from '../../primitives/hover-card.js';
@@ -10,8 +11,9 @@ import { ReasoningEffortSelector } from './reasoning-effort-selector.js';
10
11
  import { SendStopControl } from './send-stop-control.js';
11
12
  export function ComposerToolbar({ models, modelsLoading, selectedModel, onModelChange, modelDefaultControls, providers, modelRequired, modelMenuOpen, onModelMenuOpenChange, reasoningMenuOpen, onReasoningMenuOpenChange, variants, selectedVariant, onVariantChange, projectId, toolbarSlot, rewind, leading, isSending, isBusy, onStop, stopDisabled, canStop, escCount, lockForQuestion, questionButtonLabel, submitLabel, questionCanAct, hasText, canSubmit, submitDisabled, disabled, modelUnavailable, agentUnavailable = false, attachmentFailed = false, attachmentUnsupported = null, onSubmit, }) {
12
13
  const tI18nComplete = useTranslations('hardcodedUi.i18nComplete');
13
- const showModel = (models.length > 0 || modelRequired) && !!onModelChange;
14
- return (_jsxs("div", { className: "kortix-composer-toolbar flex items-center justify-between gap-1 overflow-visible", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-1 overflow-visible", children: [leading, showModel && (_jsx(ModelSelector, { models: models, modelsLoading: modelsLoading, selectedModel: selectedModel, onSelect: onModelChange, providers: providers, defaultControls: modelDefaultControls, triggerLabelClassName: "max-w-[7rem]", projectId: projectId, offerProviderAccounts: true, open: modelMenuOpen, onOpenChange: onModelMenuOpenChange })), _jsx(ReasoningEffortSelector, { variants: variants, selectedVariant: selectedVariant, onVariantChange: onVariantChange, open: reasoningMenuOpen, onOpenChange: onReasoningMenuOpenChange })] }), _jsxs("div", { className: "flex shrink-0 items-center gap-1.5", children: [rewind && (_jsxs(HoverCard, { openDelay: 0, closeDelay: 0, children: [_jsx(HoverCardTrigger, { asChild: true, children: _jsx("span", { className: "inline-flex", children: _jsxs(Button, { type: "button", variant: "ghost", size: "sm", disabled: rewind.pending || rewind.disabled, onClick: rewind.onRestore, className: "text-muted-foreground hover:text-foreground gap-1.5", children: [rewind.pending ? (_jsx(Loading, { className: "size-3.5 shrink-0" })) : (_jsx(ArrowCounterClockwiseIcon, { className: "size-3.5 shrink-0" })), tI18nComplete.raw('texta76e13b98392')] }) }) }), _jsx(HoverCardContent, { className: "px-3 py-2 text-sm text-balance", children: rewind.disabled && !rewind.pending
14
+ const { modelPickerEnabled } = useUiHostConfig();
15
+ const showModel = modelPickerEnabled && (models.length > 0 || modelRequired) && !!onModelChange;
16
+ return (_jsxs("div", { className: "kortix-composer-toolbar flex items-center justify-between gap-1 overflow-visible", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-1 overflow-visible", children: [leading, showModel && (_jsx(ModelSelector, { models: models, modelsLoading: modelsLoading, selectedModel: selectedModel, onSelect: onModelChange, providers: providers, defaultControls: modelDefaultControls, triggerLabelClassName: "max-w-[7rem]", projectId: projectId, offerProviderAccounts: true, open: modelMenuOpen, onOpenChange: onModelMenuOpenChange })), modelPickerEnabled && _jsx(ReasoningEffortSelector, { variants: variants, selectedVariant: selectedVariant, onVariantChange: onVariantChange, open: reasoningMenuOpen, onOpenChange: onReasoningMenuOpenChange })] }), _jsxs("div", { className: "flex shrink-0 items-center gap-1.5", children: [rewind && (_jsxs(HoverCard, { openDelay: 0, closeDelay: 0, children: [_jsx(HoverCardTrigger, { asChild: true, children: _jsx("span", { className: "inline-flex", children: _jsxs(Button, { type: "button", variant: "ghost", size: "sm", disabled: rewind.pending || rewind.disabled, onClick: rewind.onRestore, className: "text-muted-foreground hover:text-foreground gap-1.5", children: [rewind.pending ? (_jsx(Loading, { className: "size-3.5 shrink-0" })) : (_jsx(ArrowCounterClockwiseIcon, { className: "size-3.5 shrink-0" })), tI18nComplete.raw('texta76e13b98392')] }) }) }), _jsx(HoverCardContent, { className: "px-3 py-2 text-sm text-balance", children: rewind.disabled && !rewind.pending
15
17
  ? tI18nComplete.raw('text98433e61649a')
16
18
  : tI18nComplete.raw('text4c25090df9ea') })] })), toolbarSlot, _jsx(SendStopControl, { isSending: isSending, isBusy: isBusy, onStop: onStop, stopDisabled: stopDisabled, canStop: canStop, escCount: escCount, lockForQuestion: lockForQuestion, questionButtonLabel: questionButtonLabel, submitLabel: submitLabel, questionCanAct: questionCanAct, hasText: hasText, canSubmit: canSubmit, submitDisabled: submitDisabled, disabled: disabled, modelUnavailable: modelUnavailable, agentUnavailable: agentUnavailable, attachmentFailed: attachmentFailed, attachmentUnsupported: attachmentUnsupported, onSubmit: onSubmit })] })] }));
17
19
  }
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useTranslations } from '../../i18n/index.js';
4
+ import { useUiHostConfig } from '../../providers/ui-provider.js';
4
5
  import { PaperclipIcon as Paperclip } from '@phosphor-icons/react';
5
6
  import { Button } from '../../primitives/button.js';
6
7
  import Hint from '../../primitives/hint.js';
@@ -15,6 +16,7 @@ const NO_AGENT_SELECT = () => { };
15
16
  const EMPTY_AGENTS = [];
16
17
  export function ComposerUnderbar({ onAttachClick, agents, selectedAgent, agentMenuOpen, onAgentMenuOpenChange, onAgentChange, noAccessibleAgents = false, messages, models, selectedModel, onContextClick, variant = 'row', toolbarSlot, }) {
17
18
  const t = useTranslations('threads');
19
+ const { contextMeterEnabled } = useUiHostConfig();
18
20
  /*
19
21
  The agent picker is UNCONDITIONAL. It used to earn its slot
20
22
  (`agents.length > 0 && onAgentChange`), and that condition is exactly how
@@ -52,5 +54,5 @@ export function ComposerUnderbar({ onAttachClick, agents, selectedAgent, agentMe
52
54
  : 'kortix-composer-underbar flex items-center justify-between gap-2 px-px pt-1.5 pb-2', children: [_jsxs("div", { className: "flex min-w-0 items-center gap-1", children: [_jsx(Hint, { side: "top", label: t('attachFiles'), children: _jsx(Button, { type: "button", variant: "ghost", size: "icon-base", onClick: onAttachClick, "aria-label": t('attachFiles'),
53
55
  // `hit-area-3` — the ≈29px ghost chip clears the 44px mobile tap
54
56
  // floor only with the shared pseudo-element extension.
55
- className: "text-muted-foreground hit-area-3 rounded-lg", children: _jsx(Paperclip, { className: "size-4 shrink-0" }) }) }), _jsx(AgentSelector, { agents: noAccessibleAgents ? EMPTY_AGENTS : agents, selectedAgent: selectedAgent, open: agentMenuOpen, onOpenChange: onAgentMenuOpenChange, onSelect: onAgentChange ?? NO_AGENT_SELECT, triggerLabelClassName: "max-w-[7rem]", unavailableHint: noAccessibleAgents ? NO_AGENT_ACCESS_MESSAGE : undefined })] }), _jsxs("div", { className: "flex items-center gap-1", children: [inline && !messages ? null : (_jsx(TokenProgress, { messages: messages, models: models, selectedModel: selectedModel, onContextClick: onContextClick })), toolbarSlot] })] }));
57
+ className: "text-muted-foreground hit-area-3 rounded-lg", children: _jsx(Paperclip, { className: "size-4 shrink-0" }) }) }), _jsx(AgentSelector, { agents: noAccessibleAgents ? EMPTY_AGENTS : agents, selectedAgent: selectedAgent, open: agentMenuOpen, onOpenChange: onAgentMenuOpenChange, onSelect: onAgentChange ?? NO_AGENT_SELECT, triggerLabelClassName: "max-w-[7rem]", unavailableHint: noAccessibleAgents ? NO_AGENT_ACCESS_MESSAGE : undefined })] }), _jsxs("div", { className: "flex items-center gap-1", children: [(inline && !messages) || !contextMeterEnabled ? null : (_jsx(TokenProgress, { messages: messages, models: models, selectedModel: selectedModel, onContextClick: onContextClick })), toolbarSlot] })] }));
56
58
  }
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { errorToast } from '../../primitives/toast.js';
4
+ import { useUiHostConfig } from '../../providers/ui-provider.js';
4
5
  import { useTranslations } from '../../i18n/index.js';
5
6
  import { cn } from '../../lib/utils.js';
6
7
  import { isImageFile } from '../../internal/lib/utils/file-utils.js';
@@ -28,7 +29,7 @@ import { appendComposerQuote, classifyPaste, isUndoKey, popPasteUndo, extractRep
28
29
  import { getContextUsage } from './context-ring.js';
29
30
  import { useComposerFocus } from './hooks/use-composer-focus.js';
30
31
  import { useMenuRevalidation } from './hooks/use-file-search.js';
31
- import { controlToOpenFor, localizedSlashActions } from './menus/slash-actions.js';
32
+ import { controlToOpenFor, hostSlashActions, localizedSlashActions, slashActionShown } from './menus/slash-actions.js';
32
33
  import { createSubmitLatch } from './submit-latch.js';
33
34
  /**
34
35
  * The composer's outer shell — max width, centering, and the horizontal gutter
@@ -95,6 +96,8 @@ const NO_QUOTE_REQUESTS = [];
95
96
  const NO_SUBSCRIPTION = () => { };
96
97
  const NO_COMMAND_CHIP = () => null;
97
98
  const EMPTY_DOCUMENT = textToDocument('');
99
+ const NO_SLASH_ACTIONS = [];
100
+ const useNoSlashActions = () => NO_SLASH_ACTIONS;
98
101
  function setDocumentWithoutStealingFocus(handle, doc) {
99
102
  if (!handle)
100
103
  return;
@@ -107,7 +110,7 @@ function setDocumentWithoutStealingFocus(handle, doc) {
107
110
  }
108
111
  }
109
112
  function ComposerImpl(props) {
110
- const { onSend, promptAttachments: hostPromptAttachments, isBusy = false, sessionWorking, runtimeReady = true, onStop, stopDisabled = false, canStop = true, isSending = false, rewind, agents = EMPTY_AGENTS, selectedAgent = null, onAgentChange, noAccessibleAgents = false, commands = EMPTY_COMMANDS,
113
+ const { onSend, promptAttachments: hostPromptAttachments, isBusy = false, sessionWorking, runtimeReady = true, onStop, stopDisabled = false, canStop = true, isSending = false, rewind, agents = EMPTY_AGENTS, selectedAgent = null, onAgentChange, noAccessibleAgents = false, commands: runtimeCommands = EMPTY_COMMANDS,
111
114
  /** Whether the live command list has EVER resolved — see `planUnresolvedCommandChip`. */
112
115
  commandsResolved = false, slashFiles = EMPTY_SLASH_FILES, onCommand, onHoldCommand, models = EMPTY_MODELS, selectedModel = null, onModelChange, modelDefaultControls, variants = EMPTY_VARIANTS, selectedVariant = null, onVariantChange, messages, sessionId, projectId, draftScope = null, draftActive = true, disabled = false, notice = null, onNoticeRetry, clearOnSend = true, modelRequired = false, modelsLoading = false, autoFocus, placeholder = 'Ask anything…', hint, onArrowUpAtStart, prefill = null, onPrefillApplied, attachRequestId = null, providers, onContextClick, onCompactClick, inputSlot, toolbarSlot, underbarPlacement = 'below', slashMenuPlacement = 'above', cardClassName, quoteRequests = NO_QUOTE_REQUESTS, onQuoteRequestsApplied, lockForQuestion = false, lockForApproval = false, onCustomAnswer, questionButtonLabel = null, questionCanAct = true, onQuestionAction, escCount = 0, parentClassName, } = props;
113
116
  const tI18nComplete = useTranslations('hardcodedUi.i18nComplete');
@@ -190,6 +193,11 @@ function ComposerImpl(props) {
190
193
  const [menuOpen, setMenuOpen] = useState(false);
191
194
  useMenuRevalidation(menuOpen, projectId);
192
195
  const fileInputRef = useRef(null);
196
+ const { takeFiles, modelPickerEnabled, contextMeterEnabled, runtimeSlashEnabled, useSlashActions } = useUiHostConfig();
197
+ // The host's own `/` rows. The config is fixed for the app's life, so the hook is the same one every render.
198
+ const hostSlashRows = (useSlashActions ?? useNoSlashActions)();
199
+ // A host that hides the harness lists none of its commands, and a typed "/init" stays text.
200
+ const commands = runtimeSlashEnabled ? runtimeCommands : EMPTY_COMMANDS;
193
201
  const dragDepthRef = useRef(0);
194
202
  const savedDocBeforeQuestionRef = useRef(null);
195
203
  // A host that owns the controller leaves this composer's own one idle: hooks
@@ -312,6 +320,11 @@ function ComposerImpl(props) {
312
320
  const primaryAgents = useMemo(() => agents.filter((a) => !a.hidden && a.mode !== 'subagent'), [agents]);
313
321
  const editorDisabled = disabled || lockForApproval;
314
322
  const appendAttachedFiles = useCallback((files) => {
323
+ // The host keeps files in its own store: they go there, not to the session.
324
+ if (takeFiles) {
325
+ takeFiles(Array.from(files));
326
+ return;
327
+ }
315
328
  // One empty or oversized file must not refuse the whole drop: the SDK
316
329
  // checks a batch at once, so the files that fit are split off first and
317
330
  // each refused one is named (KRTX-2557).
@@ -339,7 +352,7 @@ function ComposerImpl(props) {
339
352
  catch (error) {
340
353
  errorToast(error instanceof Error ? error.message : tComposerAttachments('couldNotAttach'));
341
354
  }
342
- }, [addPromptAttachments, tComposerAttachments]);
355
+ }, [addPromptAttachments, tComposerAttachments, takeFiles]);
343
356
  useEffect(() => () => {
344
357
  for (const file of attachedFilesRef.current) {
345
358
  if (file.kind === 'local')
@@ -917,7 +930,11 @@ function ComposerImpl(props) {
917
930
  // Rows whose handler this host did not provide are dropped, not shown
918
931
  // dead — the `set-scope` lesson in `slash-actions.ts`: a row that
919
932
  // highlights, offers "Use", and does nothing is worse than no row.
933
+ // A row whose control the host hides is dropped too (`slashActionShown`).
934
+ const host = { modelPickerEnabled, contextMeterEnabled, runtimeSlashEnabled };
920
935
  const available = localizedSlashActions(tI18nComplete).filter((action) => {
936
+ if (!slashActionShown(action.id, host))
937
+ return false;
921
938
  if (action.id === 'switch-agent')
922
939
  return canSwitchAgent;
923
940
  if (action.id === 'compact-session')
@@ -936,9 +953,19 @@ function ComposerImpl(props) {
936
953
  return { ...action, value: `${contextUsage.percent}%`, context: contextUsage };
937
954
  }
938
955
  return action;
939
- });
940
- }, [selectedAgent, canSwitchAgent, onCompactClick, onContextClick, contextUsage, tI18nComplete]);
956
+ }).concat(hostSlashActions(hostSlashRows));
957
+ }, [
958
+ selectedAgent, canSwitchAgent, onCompactClick, onContextClick, contextUsage, tI18nComplete,
959
+ modelPickerEnabled, contextMeterEnabled, runtimeSlashEnabled, hostSlashRows,
960
+ ]);
941
961
  const handleSelectAction = useCallback((action) => {
962
+ // A host row runs the host's handler; it may hand back text for the composer.
963
+ if (action.onSelect) {
964
+ const out = action.onSelect();
965
+ if (out?.insertText)
966
+ editorRef.current?.insertAtCursor(out.insertText);
967
+ return;
968
+ }
942
969
  // Which control a row opens lives in `controlToOpenFor`
943
970
  // (menus/slash-actions.ts) so it can be tested; this file cannot be.
944
971
  const control = controlToOpenFor(action.id);
@@ -1362,6 +1389,8 @@ function ComposerImpl(props) {
1362
1389
  const showQueueStrip = Boolean(inputSlot);
1363
1390
  const cardProps = {
1364
1391
  tHardcodedUi,
1392
+ // The filtered list (`runtimeSlashEnabled`), over the host's `commands` prop in `props`.
1393
+ commands,
1365
1394
  isDragOver,
1366
1395
  // Inside a `ComposerDropZone` the zone owns every file drag.
1367
1396
  handleDragEnter: inDropZone ? undefined : handleDragEnter,
@@ -76,14 +76,14 @@ export function deserializeDraft(raw, currentUserId) {
76
76
  if (!Array.isArray(candidate.files))
77
77
  return null;
78
78
  const legacy = liftLegacyQuoteNodes(candidate.doc);
79
- const quotes = [];
79
+ const quotes = new Set();
80
80
  const stored = Array.isArray(candidate.quotes) ? candidate.quotes : [];
81
81
  for (const quote of [...stored, ...legacy.quotes]) {
82
82
  if (typeof quote !== 'string')
83
83
  continue;
84
84
  const text = quote.trim();
85
- if (text && !quotes.includes(text))
86
- quotes.push(text);
85
+ if (text)
86
+ quotes.add(text);
87
87
  }
88
88
  // Only `{ id, text }` with both set crosses back; anything else in storage is dropped.
89
89
  const storedPastes = Array.isArray(candidate.pastes) ? candidate.pastes : [];
@@ -101,7 +101,7 @@ export function deserializeDraft(raw, currentUserId) {
101
101
  u: candidate.u,
102
102
  doc: legacy.doc,
103
103
  files: candidate.files.filter(isRemote),
104
- quotes,
104
+ quotes: [...quotes],
105
105
  pastes,
106
106
  uploads,
107
107
  };
@@ -1,8 +1,10 @@
1
1
  import type { UiTranslator } from '../../../i18n/index.js';
2
2
  import type { ContextUsage } from '../context-ring.js';
3
+ import type { UiSlashAction } from '../../../providers/ui-provider.js';
3
4
  export type SlashActionId = 'switch-model' | 'switch-agent' | 'set-reasoning-effort' | 'attach-file' | 'compact-session' | 'show-context';
4
5
  export interface SlashAction {
5
- id: SlashActionId;
6
+ /** A built-in action, or `host:<id>` for a row the host added (`UiHostConfig.useSlashActions`). */
7
+ id: SlashActionId | `host:${string}`;
6
8
  label: string;
7
9
  /** Also searched, so "thinking" finds reasoning effort. */
8
10
  description: string;
@@ -29,7 +31,25 @@ export interface SlashAction {
29
31
  * row falls back to a static muted ring and a plain text description.
30
32
  */
31
33
  context?: ContextUsage;
34
+ /** A host row's heading (`UiSlashAction.group`). Built-in actions have none. */
35
+ group?: string;
36
+ /** A host row's handler (`UiSlashAction.onSelect`). */
37
+ onSelect?: UiSlashAction['onSelect'];
32
38
  }
39
+ /** The host's `/` rows as slash actions. */
40
+ export declare function hostSlashActions(rows: readonly UiSlashAction[]): SlashAction[];
41
+ /**
42
+ * Which built-in rows a host shows. A row whose control the host hides is
43
+ * dropped, not shown dead: the model picker (`modelPickerEnabled`) owns Switch
44
+ * model and Set reasoning effort, the context meter (`contextMeterEnabled`)
45
+ * owns Show context, and the runtime's commands (`runtimeSlashEnabled`) own
46
+ * Compact session.
47
+ */
48
+ export declare function slashActionShown(id: SlashAction['id'], host: {
49
+ modelPickerEnabled: boolean;
50
+ contextMeterEnabled: boolean;
51
+ runtimeSlashEnabled: boolean;
52
+ }): boolean;
33
53
  /**
34
54
  * Composer operations, executed locally. These never reach the agent — they
35
55
  * open the control they name. Distinct from the Commands section, which runs
@@ -54,5 +74,5 @@ export type SlashActionControl = 'model' | 'reasoning' | 'agent';
54
74
  * `bun test` has no DOM, so a mapping left inline there could not be asserted
55
75
  * at all.
56
76
  */
57
- export declare function controlToOpenFor(id: SlashActionId): SlashActionControl | null;
77
+ export declare function controlToOpenFor(id: SlashAction['id']): SlashActionControl | null;
58
78
  export declare function filterSlashActions(actions: SlashAction[], query: string): SlashAction[];
@@ -1,5 +1,25 @@
1
1
  import { localizeUiCatalog } from '../../../i18n/ui-catalog/localize-ui-catalog.js';
2
2
  import { REMAINING_UI_TRANSLATION_KEYS } from '../../../i18n/ui-catalog/remaining-ui-translation-keys.generated.js';
3
+ /** The host's `/` rows as slash actions. */
4
+ export function hostSlashActions(rows) {
5
+ return rows.map((r) => ({ id: `host:${r.id}`, label: r.label, description: r.description, group: r.group, onSelect: r.onSelect }));
6
+ }
7
+ /**
8
+ * Which built-in rows a host shows. A row whose control the host hides is
9
+ * dropped, not shown dead: the model picker (`modelPickerEnabled`) owns Switch
10
+ * model and Set reasoning effort, the context meter (`contextMeterEnabled`)
11
+ * owns Show context, and the runtime's commands (`runtimeSlashEnabled`) own
12
+ * Compact session.
13
+ */
14
+ export function slashActionShown(id, host) {
15
+ if (id === 'switch-model' || id === 'set-reasoning-effort')
16
+ return host.modelPickerEnabled;
17
+ if (id === 'show-context')
18
+ return host.contextMeterEnabled;
19
+ if (id === 'compact-session')
20
+ return host.runtimeSlashEnabled;
21
+ return true;
22
+ }
3
23
  /**
4
24
  * Composer operations, executed locally. These never reach the agent — they
5
25
  * open the control they name. Distinct from the Commands section, which runs