@kortix/ui 0.14.4 → 0.14.5

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 (118) hide show
  1. package/README.md +8 -4
  2. package/dist/customize/sections/llm-provider/constants.d.ts +0 -1
  3. package/dist/customize/sections/llm-provider/constants.js +0 -2
  4. package/dist/customize/sections/llm-provider/models-tab-group.d.ts +1 -1
  5. package/dist/customize/sections/llm-provider/models-tab-group.js +7 -4
  6. package/dist/customize/sections/llm-provider/models-tab-row.d.ts +2 -18
  7. package/dist/customize/sections/llm-provider/models-tab-row.js +6 -30
  8. package/dist/customize/sections/llm-provider/models-tab.d.ts +0 -1
  9. package/dist/customize/sections/llm-provider/models-tab.js +1 -10
  10. package/dist/customize/sections/llm-provider/use-connected-providers.js +2 -2
  11. package/dist/customize/sections/llm-provider/utils.d.ts +1 -1
  12. package/dist/customize/sections/llm-provider/utils.js +7 -14
  13. package/dist/customize/sections/view/gateway/_shared.js +1 -2
  14. package/dist/customize/sections/view/gateway/gateway-routing.js +3 -2
  15. package/dist/customize/sections/view/sandbox-provider-coverage.d.ts +3 -18
  16. package/dist/customize/sections/view/sandbox-provider-coverage.js +7 -27
  17. package/dist/customize/settings/tabs/sandbox-tab.d.ts +2 -27
  18. package/dist/customize/settings/tabs/sandbox-tab.js +13 -112
  19. package/dist/customize/shared/entity/entity-modal.d.ts +5 -1
  20. package/dist/customize/shared/entity/entity-modal.js +7 -4
  21. package/dist/customize/skills/remove-skill-button.d.ts +16 -0
  22. package/dist/customize/skills/remove-skill-button.js +42 -0
  23. package/dist/customize/skills/skills-page.js +1 -1
  24. package/dist/customize/triggers/schedule/trigger-list.d.ts +0 -2
  25. package/dist/customize/triggers/schedule/trigger-list.js +1 -18
  26. package/dist/customize/triggers/schedule/trigger-tile.d.ts +1 -32
  27. package/dist/customize/triggers/schedule/trigger-tile.js +1 -16
  28. package/dist/files/project/hooks/use-change-requests.d.ts +14 -1
  29. package/dist/files/project/hooks/use-change-requests.js +44 -25
  30. package/dist/files/renderers/docx/docx-viewer.js +4 -1
  31. package/dist/files/renderers/pdf/pdf-renderer.d.ts +3 -1
  32. package/dist/files/renderers/pdf/pdf-renderer.js +2 -2
  33. package/dist/files/renderers/pdf/pdf-viewer.d.ts +3 -0
  34. package/dist/files/renderers/pdf/pdf-viewer.js +15 -6
  35. package/dist/files/viewer/file-content-renderer.d.ts +4 -1
  36. package/dist/files/viewer/file-content-renderer.js +2 -2
  37. package/dist/i18n/en.d.ts +26 -35
  38. package/dist/i18n/en.js +35 -44
  39. package/dist/index.d.ts +1 -1
  40. package/dist/internal/hooks/use-notification-center.js +21 -15
  41. package/dist/internal/hooks/use-review-session-summary.d.ts +6 -0
  42. package/dist/internal/hooks/use-review-session-summary.js +11 -0
  43. package/dist/internal/lib/browser-error-noise.d.ts +1 -1
  44. package/dist/internal/lib/browser-error-noise.js +1 -1
  45. package/dist/internal/lib/browser-noise/rules/rejections.d.ts +10 -0
  46. package/dist/internal/lib/browser-noise/rules/rejections.js +27 -1
  47. package/dist/internal/lib/provider-branding.js +14 -3
  48. package/dist/internal/lib/trigger-clock.d.ts +42 -0
  49. package/dist/internal/lib/trigger-clock.js +34 -0
  50. package/dist/internal/lib/web-notifications.d.ts +6 -0
  51. package/dist/internal/lib/web-notifications.js +57 -7
  52. package/dist/internal/notifications/notification-bell.d.ts +13 -0
  53. package/dist/internal/notifications/notification-bell.js +1 -1
  54. package/dist/internal/reminders/reminder-filter.d.ts +12 -0
  55. package/dist/internal/reminders/reminder-filter.js +34 -0
  56. package/dist/internal/reminders/reminder-format.d.ts +6 -0
  57. package/dist/internal/reminders/reminder-format.js +20 -2
  58. package/dist/internal/stores/kortix-computer-store.d.ts +1 -1
  59. package/dist/internal/stores/session-hover-store.d.ts +20 -5
  60. package/dist/internal/stores/session-hover-store.js +31 -13
  61. package/dist/internal/stores/session-panel-memory-store.js +2 -0
  62. package/dist/primitives/count-badge.d.ts +3 -1
  63. package/dist/primitives/count-badge.js +3 -3
  64. package/dist/primitives/glyph-paths-source.js +1 -1
  65. package/dist/primitives/glyph-paths.generated.js +1 -1
  66. package/dist/providers/ui-provider.d.ts +57 -0
  67. package/dist/providers/ui-provider.js +4 -0
  68. package/dist/session/composer/ComposerCard.d.ts +2 -0
  69. package/dist/session/composer/ComposerCard.js +5 -1
  70. package/dist/session/composer/agent-selector.js +12 -5
  71. package/dist/session/composer/composer-toolbar.js +4 -2
  72. package/dist/session/composer/composer-underbar.js +3 -1
  73. package/dist/session/composer/composer.js +34 -5
  74. package/dist/session/composer/menus/slash-actions.d.ts +22 -2
  75. package/dist/session/composer/menus/slash-actions.js +20 -0
  76. package/dist/session/composer/menus/slash-items.js +19 -3
  77. package/dist/session/header/reminder-parts.d.ts +285 -0
  78. package/dist/session/header/reminder-parts.js +115 -0
  79. package/dist/session/header/session-reminders-indicator.d.ts +13 -0
  80. package/dist/session/header/session-reminders-indicator.js +142 -35
  81. package/dist/session/models/model-availability.d.ts +23 -0
  82. package/dist/session/models/model-availability.js +20 -0
  83. package/dist/session/models/model-grouping.js +1 -4
  84. package/dist/session/models/model-selector.js +23 -14
  85. package/dist/session/models/use-model-connection-gate.js +4 -1
  86. package/dist/session/outcomes/session-outcomes-provider.d.ts +3 -1
  87. package/dist/session/outcomes/session-outcomes-provider.js +31 -3
  88. package/dist/session/overrides/session-overrides-composer.d.ts +1 -1
  89. package/dist/session/overrides/session-overrides-composer.js +5 -1
  90. package/dist/session/panel/mobile-tool-drawer.js +4 -2
  91. package/dist/session/panel/open-session-quick-view.js +2 -1
  92. package/dist/session/panel/panel-fallback.js +8 -4
  93. package/dist/session/panel/session-panel-provider.d.ts +1 -0
  94. package/dist/session/panel/session-panel-provider.js +13 -1
  95. package/dist/session/panel/session-reminders-panel.d.ts +4 -0
  96. package/dist/session/panel/session-reminders-panel.js +109 -0
  97. package/dist/session/panel/shared/use-panel-handoffs.d.ts +2 -2
  98. package/dist/session/panel/shared/use-panel-handoffs.js +5 -1
  99. package/dist/session/print/session-print-header.d.ts +0 -10
  100. package/dist/session/print/session-print-header.js +16 -1
  101. package/dist/session/status/session-surface.js +1 -1
  102. package/dist/session/thread/optimistic-turn.d.ts +5 -1
  103. package/dist/session/thread/optimistic-turn.js +8 -6
  104. package/dist/session/thread/session-chat.d.ts +6 -1
  105. package/dist/session/thread/session-chat.js +22 -3
  106. package/dist/session/thread/session-thread.d.ts +10 -1
  107. package/dist/session/thread/session-thread.js +2 -2
  108. package/dist/session/turn/burst-step-body.js +4 -0
  109. package/dist/styles.css +1 -1
  110. package/dist/workspace/sidebar/project-session-list.js +52 -55
  111. package/dist/workspace/sidebar/session-brief-hover-card.d.ts +35 -18
  112. package/dist/workspace/sidebar/session-brief-hover-card.js +114 -37
  113. package/dist/workspace/sidebar/session-status-mark.d.ts +15 -1
  114. package/dist/workspace/sidebar/session-status-mark.js +63 -17
  115. package/package.json +7 -3
  116. package/skills/kortix-ui/SKILL.md +1 -1
  117. package/dist/customize/sections/view/sandbox-provider-result.d.ts +0 -47
  118. package/dist/customize/sections/view/sandbox-provider-result.js +0 -79
@@ -12,6 +12,10 @@ import { create } from 'zustand';
12
12
  * the active row is what is left, and it is also the cheaper shape: exactly one
13
13
  * card is mounted at any time, by construction rather than by timing luck.
14
14
  *
15
+ * The store also holds the active row's element and the one card's content
16
+ * slot (`SessionBriefCardHost`): rows render their brief into that slot, and
17
+ * the host glides to the row instead of a new card appearing at it.
18
+ *
15
19
  * The delays live here rather than on the primitive for a performance reason.
16
20
  * If `openDelay` were a prop derived from `warm`, every row in the sidebar would
17
21
  * re-render each time the group warmed or cooled. Nothing selects `warm`, so it
@@ -21,18 +25,22 @@ import { create } from 'zustand';
21
25
  /** The first card costs a delay, so a pointer crossing the list on its way
22
26
  * somewhere else does not flash open every row it passes over. */
23
27
  export const HOVER_OPEN_DELAY_MS = 200;
28
+ /** While a card is open, a move to another row waits this long, and a newer
29
+ * row restarts the wait: a pointer sweeping down the list moves the card once,
30
+ * to where it stops, instead of re-rendering it for every row it crosses. */
31
+ export const HOVER_MOVE_DEBOUNCE_MS = 50;
24
32
  /** Long enough for the pointer to cross the `sideOffset` gap into the card. */
25
33
  export const HOVER_CLOSE_DELAY_MS = 100;
26
34
  /** How long the group stays warm after its last card closes. */
27
35
  export const HOVER_WARM_GRACE_MS = 300;
28
36
  /**
29
- * Once one card is open the group is warm and the next row is instant, so the
30
- * card reads as moving to the row under the pointer instead of closing and
31
- * reopening. This is the rule the design system already states for tooltips:
32
- * delay before the first, none for the rest while the group is active.
37
+ * Once one card is open the group is warm and the next row costs only the move
38
+ * debounce, so the card reads as moving to the row under the pointer instead of
39
+ * closing and reopening. This is the rule the design system already states for
40
+ * tooltips: delay before the first, (almost) none for the rest.
33
41
  */
34
42
  export function hoverOpenDelayMs(warm) {
35
- return warm ? 0 : HOVER_OPEN_DELAY_MS;
43
+ return warm ? HOVER_MOVE_DEBOUNCE_MS : HOVER_OPEN_DELAY_MS;
36
44
  }
37
45
  /**
38
46
  * Compare-and-clear, the same guard `session-switch-store` uses: a close
@@ -44,6 +52,8 @@ export function clearIfActive(current, sessionId) {
44
52
  let openTimer = null;
45
53
  let closeTimer = null;
46
54
  let warmTimer = null;
55
+ /** The row a pending open will show; its close must also close the card still showing. */
56
+ let pendingId = null;
47
57
  function cancel(timer) {
48
58
  if (timer)
49
59
  clearTimeout(timer);
@@ -51,8 +61,12 @@ function cancel(timer) {
51
61
  }
52
62
  export const useSessionHoverStore = create((set, get) => ({
53
63
  activeSessionId: null,
64
+ anchor: null,
65
+ via: 'pointer',
66
+ slot: null,
54
67
  warm: false,
55
- openSession: (sessionId) => {
68
+ setSlot: (slot) => set({ slot }),
69
+ openSession: (sessionId, anchor, via = 'pointer') => {
56
70
  closeTimer = cancel(closeTimer);
57
71
  warmTimer = cancel(warmTimer);
58
72
  // A pending open belongs to whichever row the pointer has just left.
@@ -61,21 +75,24 @@ export const useSessionHoverStore = create((set, get) => ({
61
75
  // or crossing the gap back from the card would flicker it.
62
76
  if (get().activeSessionId === sessionId)
63
77
  return;
64
- if (hoverOpenDelayMs(get().warm) === 0) {
65
- set({ activeSessionId: sessionId, warm: true });
66
- return;
67
- }
78
+ const show = { activeSessionId: sessionId, anchor: anchor ?? null, via, warm: true };
79
+ pendingId = sessionId;
68
80
  openTimer = setTimeout(() => {
69
81
  openTimer = null;
70
- set({ activeSessionId: sessionId, warm: true });
71
- }, HOVER_OPEN_DELAY_MS);
82
+ pendingId = null;
83
+ set(show);
84
+ }, hoverOpenDelayMs(get().warm));
72
85
  },
73
86
  closeSession: (sessionId) => {
74
87
  openTimer = cancel(openTimer);
75
88
  closeTimer = cancel(closeTimer);
89
+ // Leaving a row whose move had not landed yet: the card still shows the
90
+ // previous row, and that card is the one to close.
91
+ const target = pendingId === sessionId ? get().activeSessionId : sessionId;
92
+ pendingId = null;
76
93
  closeTimer = setTimeout(() => {
77
94
  closeTimer = null;
78
- set((state) => ({ activeSessionId: clearIfActive(state.activeSessionId, sessionId) }));
95
+ set((state) => ({ activeSessionId: target ? clearIfActive(state.activeSessionId, target) : state.activeSessionId }));
79
96
  warmTimer = setTimeout(() => {
80
97
  warmTimer = null;
81
98
  set({ warm: false });
@@ -83,6 +100,7 @@ export const useSessionHoverStore = create((set, get) => ({
83
100
  }, HOVER_CLOSE_DELAY_MS);
84
101
  },
85
102
  dismiss: () => {
103
+ pendingId = null;
86
104
  openTimer = cancel(openTimer);
87
105
  closeTimer = cancel(closeTimer);
88
106
  warmTimer = cancel(warmTimer);
@@ -40,6 +40,8 @@ export function rememberedPanelOf({ terminalOpen, detailKey, }) {
40
40
  return { kind: 'view', view: 'files', target: { changes: true } };
41
41
  if (detailKey === 'audit')
42
42
  return { kind: 'view', view: 'audit' };
43
+ if (detailKey === 'reminders')
44
+ return { kind: 'view', view: 'reminders' };
43
45
  if (detailKey === 'app:landing')
44
46
  return { kind: 'view', view: 'browser' };
45
47
  if (detailKey?.startsWith('app:'))
@@ -19,8 +19,10 @@ declare const TONE: {
19
19
  readonly conflict: "bg-kortix-orange/15";
20
20
  };
21
21
  export type CountBadgeTone = keyof typeof TONE;
22
- export declare function CountBadge({ count, tone, className, ...props }: {
22
+ export declare function CountBadge({ count, tone, icon, className, ...props }: {
23
23
  count: number;
24
24
  tone?: CountBadgeTone;
25
+ /** A glyph that names what is counted, drawn before the number. */
26
+ icon?: React.ReactNode;
25
27
  } & Omit<React.ComponentProps<'span'>, 'children'>): import("react").JSX.Element;
26
28
  export {};
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Badge } from './badge.js';
3
3
  import { cn } from '../lib/utils.js';
4
4
  /**
@@ -21,6 +21,6 @@ const TONE = {
21
21
  reminder: 'bg-kortix-purple/15',
22
22
  conflict: 'bg-kortix-orange/15',
23
23
  };
24
- export function CountBadge({ count, tone = 'neutral', className, ...props }) {
25
- return (_jsx(Badge, { variant: "transparent", size: "tabular", className: cn(TONE[tone], 'shrink-0 text-current', className), ...props, children: count > 99 ? '99+' : count }));
24
+ export function CountBadge({ count, tone = 'neutral', icon, className, ...props }) {
25
+ return (_jsxs(Badge, { variant: "transparent", size: "tabular", className: cn(TONE[tone], 'shrink-0 text-current', icon && 'gap-1', className), ...props, children: [icon, count > 99 ? '99+' : count] }));
26
26
  }
@@ -27,7 +27,7 @@ export function renderGlyphPathsModule() {
27
27
  const paths = glyphPathsInDefaultWeight();
28
28
  const lines = Object.entries(paths).map(([name, d]) => ` ${JSON.stringify(name)}: ${JSON.stringify(d)},`);
29
29
  return [
30
- '// GENERATED by apps/web/scripts/generate-glyph-paths.tsx. Do not edit.',
30
+ '// GENERATED by packages/ui/scripts/generate-glyph-paths.tsx. Do not edit.',
31
31
  '// The path of every catalogue glyph in DEFAULT_ICON_WEIGHT; see glyph-face.tsx.',
32
32
  '',
33
33
  `export const GLYPH_PATHS_WEIGHT = ${JSON.stringify(DEFAULT_ICON_WEIGHT)};`,
@@ -1,4 +1,4 @@
1
- // GENERATED by apps/web/scripts/generate-glyph-paths.tsx. Do not edit.
1
+ // GENERATED by packages/ui/scripts/generate-glyph-paths.tsx. Do not edit.
2
2
  // The path of every catalogue glyph in DEFAULT_ICON_WEIGHT; see glyph-face.tsx.
3
3
  export const GLYPH_PATHS_WEIGHT = "bold";
4
4
  export const GLYPH_PATHS = {
@@ -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,11 @@ 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(() => (offeredAgents ? agents.filter((a) => offeredAgents.includes(a.name)) : agents), [agents, offeredAgents]);
49
+ const primaryAgents = useMemo(() => composerSelectableAgents(offered), [offered]);
50
+ const label = (name) => agentLabels?.[name] ?? (isMetaAgentName(name) ? META_AGENT_DISPLAY_NAME : capitalizeWords(name));
46
51
  // A `flash` state, a `prevAgentRef` feeding it, two effects and a 400ms
47
52
  // timer used to live here. Nothing ever READ `flash` — it was written on
48
53
  // every agent change and rendered nowhere, so the whole apparatus bought a
@@ -55,8 +60,10 @@ export function AgentSelector({ agents, selectedAgent, onSelect, disabled = fals
55
60
  const q = search.toLowerCase().trim();
56
61
  if (!q)
57
62
  return primaryAgents;
58
- return primaryAgents.filter((a) => a.name.toLowerCase().includes(q) || (a.description || '').toLowerCase().includes(q));
59
- }, [primaryAgents, search]);
63
+ return primaryAgents.filter((a) => a.name.toLowerCase().includes(q) ||
64
+ (agentLabels?.[a.name] ?? '').toLowerCase().includes(q) ||
65
+ (a.description || '').toLowerCase().includes(q));
66
+ }, [primaryAgents, search, agentLabels]);
60
67
  const filteredMeta = useMemo(() => filteredPrimary.filter((a) => isMetaAgentName(a.name)), [filteredPrimary]);
61
68
  const filteredProject = useMemo(() => filteredPrimary.filter((a) => !isMetaAgentName(a.name)), [filteredPrimary]);
62
69
  // Keyed off the FULL list, never the filtered one: deriving it from
@@ -71,7 +78,7 @@ export function AgentSelector({ agents, selectedAgent, onSelect, disabled = fals
71
78
  // fallback stays only for a truly unresolved selection (no name at all),
72
79
  // matching the resolver's own first-accessible pre-selection.
73
80
  const displayedName = resolveDisplayedAgentName(primaryAgents, selectedAgent);
74
- const displayName = agentDisplayLabel(agents, selectedAgent);
81
+ const displayName = displayedName ? label(displayedName) : agentDisplayLabel(offered, selectedAgent);
75
82
  const metaSelected = isMetaAgentName(displayedName);
76
83
  /**
77
84
  * One agent: name, its type, its description, and a check when it is the
@@ -102,7 +109,7 @@ export function AgentSelector({ agents, selectedAgent, onSelect, disabled = fals
102
109
  }, children: [meta && (
103
110
  // Meta is the platform agent that runs the others, so its row leads
104
111
  // 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 && (
112
+ _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
113
  // `mt-0.5` optically centres the 16px check against the first line
107
114
  // rather than the top edge of the text box.
108
115
  _jsx(CheckIcon, { className: "text-foreground mt-0.5 size-4 shrink-0" }))] }, agent.name));
@@ -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,
@@ -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
@@ -96,7 +96,7 @@ export function buildSlashSections({ commands, actions = SLASH_ACTIONS, files =
96
96
  const filteredActions = filterSlashActions(actions, query);
97
97
  const filteredFiles = filterSlashFiles(files, query);
98
98
  const fileCap = q ? FILE_LIMIT : FILE_PREVIEW_LIMIT;
99
- // Render order: Actions, Outputs, Context, Skills, Commands, MCP.
99
+ // Render order: Actions, the host's groups, Outputs, Context, Skills, Commands, MCP.
100
100
  //
101
101
  // Actions moved from last to first. They are the fixed, always-present rows
102
102
  // (switch model, attach a file, …) — the same six every time, in the same
@@ -112,11 +112,12 @@ export function buildSlashSections({ commands, actions = SLASH_ACTIONS, files =
112
112
  // in would desync the highlight from the keyboard.
113
113
  let index = 0;
114
114
  const sections = [];
115
- if (filteredActions.length) {
115
+ const builtIn = filteredActions.filter((a) => !a.group);
116
+ if (builtIn.length) {
116
117
  sections.push({
117
118
  heading: actionsHeading,
118
119
  hideHeading: true,
119
- rows: filteredActions.map((action) => ({
120
+ rows: builtIn.map((action) => ({
120
121
  index: index++,
121
122
  type: 'action',
122
123
  name: action.label,
@@ -127,6 +128,21 @@ export function buildSlashSections({ commands, actions = SLASH_ACTIONS, files =
127
128
  })),
128
129
  });
129
130
  }
131
+ // The host's rows (`UiHostConfig.useSlashActions`), under their own headings, in the order given.
132
+ const hostGroups = [...new Set(filteredActions.filter((a) => a.group).map((a) => a.group))];
133
+ for (const heading of hostGroups) {
134
+ sections.push({
135
+ heading,
136
+ rows: filteredActions.filter((a) => a.group === heading).map((action) => ({
137
+ index: index++,
138
+ type: 'action',
139
+ name: action.label,
140
+ description: action.description,
141
+ hint: action.hint,
142
+ action,
143
+ })),
144
+ });
145
+ }
130
146
  // Outputs, then Context, then the command buckets.
131
147
  //
132
148
  // Above the commands because this is the one part of the palette that is