@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.
- package/README.md +8 -4
- package/dist/customize/sections/llm-provider/constants.d.ts +0 -1
- package/dist/customize/sections/llm-provider/constants.js +0 -2
- package/dist/customize/sections/llm-provider/models-tab-group.d.ts +1 -1
- package/dist/customize/sections/llm-provider/models-tab-group.js +7 -4
- package/dist/customize/sections/llm-provider/models-tab-row.d.ts +2 -18
- package/dist/customize/sections/llm-provider/models-tab-row.js +6 -30
- package/dist/customize/sections/llm-provider/models-tab.d.ts +0 -1
- package/dist/customize/sections/llm-provider/models-tab.js +1 -10
- package/dist/customize/sections/llm-provider/use-connected-providers.js +2 -2
- package/dist/customize/sections/llm-provider/utils.d.ts +1 -1
- package/dist/customize/sections/llm-provider/utils.js +7 -14
- package/dist/customize/sections/view/gateway/_shared.js +1 -2
- package/dist/customize/sections/view/gateway/gateway-routing.js +3 -2
- package/dist/customize/sections/view/sandbox-provider-coverage.d.ts +3 -18
- package/dist/customize/sections/view/sandbox-provider-coverage.js +7 -27
- package/dist/customize/settings/tabs/sandbox-tab.d.ts +2 -27
- package/dist/customize/settings/tabs/sandbox-tab.js +13 -112
- package/dist/customize/shared/entity/entity-modal.d.ts +5 -1
- package/dist/customize/shared/entity/entity-modal.js +7 -4
- package/dist/customize/skills/remove-skill-button.d.ts +16 -0
- package/dist/customize/skills/remove-skill-button.js +42 -0
- package/dist/customize/skills/skills-page.js +1 -1
- package/dist/customize/triggers/schedule/trigger-list.d.ts +0 -2
- package/dist/customize/triggers/schedule/trigger-list.js +1 -18
- package/dist/customize/triggers/schedule/trigger-tile.d.ts +1 -32
- package/dist/customize/triggers/schedule/trigger-tile.js +1 -16
- package/dist/files/project/hooks/use-change-requests.d.ts +14 -1
- package/dist/files/project/hooks/use-change-requests.js +44 -25
- package/dist/files/renderers/docx/docx-viewer.js +4 -1
- package/dist/files/renderers/pdf/pdf-renderer.d.ts +3 -1
- package/dist/files/renderers/pdf/pdf-renderer.js +2 -2
- package/dist/files/renderers/pdf/pdf-viewer.d.ts +3 -0
- package/dist/files/renderers/pdf/pdf-viewer.js +15 -6
- package/dist/files/viewer/file-content-renderer.d.ts +4 -1
- package/dist/files/viewer/file-content-renderer.js +2 -2
- package/dist/i18n/en.d.ts +26 -35
- package/dist/i18n/en.js +35 -44
- package/dist/index.d.ts +1 -1
- package/dist/internal/hooks/use-notification-center.js +21 -15
- package/dist/internal/hooks/use-review-session-summary.d.ts +6 -0
- package/dist/internal/hooks/use-review-session-summary.js +11 -0
- package/dist/internal/lib/browser-error-noise.d.ts +1 -1
- package/dist/internal/lib/browser-error-noise.js +1 -1
- package/dist/internal/lib/browser-noise/rules/rejections.d.ts +10 -0
- package/dist/internal/lib/browser-noise/rules/rejections.js +27 -1
- package/dist/internal/lib/provider-branding.js +14 -3
- package/dist/internal/lib/trigger-clock.d.ts +42 -0
- package/dist/internal/lib/trigger-clock.js +34 -0
- package/dist/internal/lib/web-notifications.d.ts +6 -0
- package/dist/internal/lib/web-notifications.js +57 -7
- package/dist/internal/notifications/notification-bell.d.ts +13 -0
- package/dist/internal/notifications/notification-bell.js +1 -1
- package/dist/internal/reminders/reminder-filter.d.ts +12 -0
- package/dist/internal/reminders/reminder-filter.js +34 -0
- package/dist/internal/reminders/reminder-format.d.ts +6 -0
- package/dist/internal/reminders/reminder-format.js +20 -2
- package/dist/internal/stores/kortix-computer-store.d.ts +1 -1
- package/dist/internal/stores/session-hover-store.d.ts +20 -5
- package/dist/internal/stores/session-hover-store.js +31 -13
- package/dist/internal/stores/session-panel-memory-store.js +2 -0
- package/dist/primitives/count-badge.d.ts +3 -1
- package/dist/primitives/count-badge.js +3 -3
- package/dist/primitives/glyph-paths-source.js +1 -1
- package/dist/primitives/glyph-paths.generated.js +1 -1
- package/dist/providers/ui-provider.d.ts +57 -0
- package/dist/providers/ui-provider.js +4 -0
- package/dist/session/composer/ComposerCard.d.ts +2 -0
- package/dist/session/composer/ComposerCard.js +5 -1
- package/dist/session/composer/agent-selector.js +12 -5
- package/dist/session/composer/composer-toolbar.js +4 -2
- package/dist/session/composer/composer-underbar.js +3 -1
- package/dist/session/composer/composer.js +34 -5
- package/dist/session/composer/menus/slash-actions.d.ts +22 -2
- package/dist/session/composer/menus/slash-actions.js +20 -0
- package/dist/session/composer/menus/slash-items.js +19 -3
- package/dist/session/header/reminder-parts.d.ts +285 -0
- package/dist/session/header/reminder-parts.js +115 -0
- package/dist/session/header/session-reminders-indicator.d.ts +13 -0
- package/dist/session/header/session-reminders-indicator.js +142 -35
- package/dist/session/models/model-availability.d.ts +23 -0
- package/dist/session/models/model-availability.js +20 -0
- package/dist/session/models/model-grouping.js +1 -4
- package/dist/session/models/model-selector.js +23 -14
- package/dist/session/models/use-model-connection-gate.js +4 -1
- package/dist/session/outcomes/session-outcomes-provider.d.ts +3 -1
- package/dist/session/outcomes/session-outcomes-provider.js +31 -3
- package/dist/session/overrides/session-overrides-composer.d.ts +1 -1
- package/dist/session/overrides/session-overrides-composer.js +5 -1
- package/dist/session/panel/mobile-tool-drawer.js +4 -2
- package/dist/session/panel/open-session-quick-view.js +2 -1
- package/dist/session/panel/panel-fallback.js +8 -4
- package/dist/session/panel/session-panel-provider.d.ts +1 -0
- package/dist/session/panel/session-panel-provider.js +13 -1
- package/dist/session/panel/session-reminders-panel.d.ts +4 -0
- package/dist/session/panel/session-reminders-panel.js +109 -0
- package/dist/session/panel/shared/use-panel-handoffs.d.ts +2 -2
- package/dist/session/panel/shared/use-panel-handoffs.js +5 -1
- package/dist/session/print/session-print-header.d.ts +0 -10
- package/dist/session/print/session-print-header.js +16 -1
- package/dist/session/status/session-surface.js +1 -1
- package/dist/session/thread/optimistic-turn.d.ts +5 -1
- package/dist/session/thread/optimistic-turn.js +8 -6
- package/dist/session/thread/session-chat.d.ts +6 -1
- package/dist/session/thread/session-chat.js +22 -3
- package/dist/session/thread/session-thread.d.ts +10 -1
- package/dist/session/thread/session-thread.js +2 -2
- package/dist/session/turn/burst-step-body.js +4 -0
- package/dist/styles.css +1 -1
- package/dist/workspace/sidebar/project-session-list.js +52 -55
- package/dist/workspace/sidebar/session-brief-hover-card.d.ts +35 -18
- package/dist/workspace/sidebar/session-brief-hover-card.js +114 -37
- package/dist/workspace/sidebar/session-status-mark.d.ts +15 -1
- package/dist/workspace/sidebar/session-status-mark.js +63 -17
- package/package.json +7 -3
- package/skills/kortix-ui/SKILL.md +1 -1
- package/dist/customize/sections/view/sandbox-provider-result.d.ts +0 -47
- 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
|
|
30
|
-
* card reads as moving to the row under the pointer instead of
|
|
31
|
-
* reopening. This is the rule the design system already states for
|
|
32
|
-
* delay before the first, none for the rest
|
|
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 ?
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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,
|
|
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 {
|
|
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 (
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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) ||
|
|
59
|
-
|
|
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(
|
|
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:
|
|
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
|
|
14
|
-
|
|
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
|
-
}, [
|
|
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
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|