@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
|
@@ -10,9 +10,6 @@ import { ConfirmDialog } from '../../../primitives/confirm-dialog.js';
|
|
|
10
10
|
import Hint from '../../../primitives/hint.js';
|
|
11
11
|
import { InfoBanner } from '../../../primitives/info-banner.js';
|
|
12
12
|
import Loading from '../../../primitives/loading.js';
|
|
13
|
-
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '../../../primitives/select.js';
|
|
14
|
-
import { SettingsRow, SettingsRowGroup } from '../../../primitives/settings-row.js';
|
|
15
|
-
import { SettingsSubsectionHeader } from '../../../primitives/settings-subsection-header.js';
|
|
16
13
|
import { Skeleton } from '../../../primitives/skeleton.js';
|
|
17
14
|
import { errorToast, successToast } from '../../../primitives/toast.js';
|
|
18
15
|
import { EmptyState } from '../../../primitives/empty-state.js';
|
|
@@ -22,20 +19,15 @@ import { useLocale, useTranslations } from '../../../i18n/index.js';
|
|
|
22
19
|
import { PROJECT_ACTIONS } from '../../../internal/lib/project-actions.js';
|
|
23
20
|
import { useProjectCans } from '../../../internal/lib/use-project-can.js';
|
|
24
21
|
import { cn } from '../../../internal/lib/utils.js';
|
|
25
|
-
import { buildSandboxTemplate, deleteSandboxTemplate,
|
|
26
|
-
import { contract,
|
|
27
|
-
import { ArrowClockwiseIcon, CheckCircleIcon, CircleDashedIcon, CpuIcon, CubeIcon, FileCodeIcon, HardDrivesIcon, MemoryIcon, PackageIcon, PencilSimpleIcon, PlusIcon,
|
|
22
|
+
import { buildSandboxTemplate, deleteSandboxTemplate, listProjectSnapshots, } from '@kortix/sdk';
|
|
23
|
+
import { contract, qk } from '@kortix/sdk/react';
|
|
24
|
+
import { ArrowClockwiseIcon, CheckCircleIcon, CircleDashedIcon, CpuIcon, CubeIcon, FileCodeIcon, HardDrivesIcon, MemoryIcon, PackageIcon, PencilSimpleIcon, PlusIcon, ShippingContainerIcon, SquaresFourIcon, TrashIcon, XCircleIcon, } from '@phosphor-icons/react';
|
|
28
25
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
29
|
-
import { SandboxProviderBadge,
|
|
30
|
-
import { applySandboxProviderResult, pollSandboxProviderTransition, } from '../../sections/view/sandbox-provider-result.js';
|
|
31
|
-
import { formatSandboxProvider, isLegacySandboxProvider, sortSandboxProviders, } from '../../../internal/lib/sandbox-providers.js';
|
|
26
|
+
import { SandboxProviderBadge, availableProviderCoverage, latestObservedAt, } from '../../sections/view/sandbox-provider-coverage.js';
|
|
32
27
|
import { SettingsTabHeader } from '../settings-tab-header.js';
|
|
33
28
|
import { snapshotsQueryRetry } from './snapshots-query.js';
|
|
34
|
-
/**
|
|
35
|
-
const SANDBOX_TAB_ACTIONS = [
|
|
36
|
-
PROJECT_ACTIONS.PROJECT_SANDBOX_WRITE,
|
|
37
|
-
PROJECT_ACTIONS.PROJECT_WRITE,
|
|
38
|
-
];
|
|
29
|
+
/** The one leaf this tab gates on: template create, edit, delete and rebuild. */
|
|
30
|
+
const SANDBOX_TAB_ACTIONS = [PROJECT_ACTIONS.PROJECT_SANDBOX_WRITE];
|
|
39
31
|
/** Three, not five: each skeleton is now a full card, so five of them
|
|
40
32
|
* overflowed the panel and read as a wall of grey. */
|
|
41
33
|
const TEMPLATE_SKELETON_ROWS = [
|
|
@@ -112,34 +104,19 @@ export function describeBase(template, labels = {}) {
|
|
|
112
104
|
mono: false,
|
|
113
105
|
};
|
|
114
106
|
}
|
|
115
|
-
/** Where sessions built from this template actually run. Static configuration,
|
|
116
|
-
* not live status — which is why it belongs in the spec grid next to CPU and
|
|
117
|
-
* memory, and NOT in the runtime footer where a bare `Automatic` badge used
|
|
118
|
-
* to float with no label attached to it. */
|
|
119
|
-
export function describeRouting(providerMode, selectedProvider, tI18nComplete, labels = {}) {
|
|
120
|
-
const mode = describeProviderMode(providerMode, selectedProvider, tI18nComplete);
|
|
121
|
-
if (providerMode === 'automatic')
|
|
122
|
-
return { label: labels.automatic ?? 'Automatic', icon: ShuffleIcon };
|
|
123
|
-
return {
|
|
124
|
-
label: mode.selectedProvider
|
|
125
|
-
? (labels.pinnedWithProvider?.(mode.selectedProvider) ?? `Pinned · ${mode.selectedProvider}`)
|
|
126
|
-
: (labels.pinned ?? 'Pinned'),
|
|
127
|
-
icon: PushPinIcon,
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
107
|
/** Locale-aware age for the runtime footer's "Checked …" stamp. */
|
|
131
108
|
export function TemplateStateLine({ tone, label }) {
|
|
132
109
|
const toneClass = TEMPLATE_TONE_TEXT[tone];
|
|
133
110
|
return (_jsxs("span", { className: cn('flex items-center gap-1.5 text-xs font-medium', toneClass), children: [tone === 'busy' ? (_jsx(Loading, { variant: "spokes", className: cn('size-3.5 shrink-0', toneClass) })) : tone === 'ok' ? (_jsx(CheckCircleIcon, { weight: "fill", className: "size-3.5 shrink-0" })) : tone === 'fail' ? (_jsx(XCircleIcon, { weight: "fill", className: "size-3.5 shrink-0" })) : (_jsx(CircleDashedIcon, { className: "size-3.5 shrink-0" })), label] }));
|
|
134
111
|
}
|
|
135
|
-
export function TemplateRuntimeFooter({ coverage,
|
|
112
|
+
export function TemplateRuntimeFooter({ coverage, formatObservedAt, runtimeLabel, checkedLabel, badgeCopy, }) {
|
|
136
113
|
const available = availableProviderCoverage(coverage);
|
|
137
114
|
if (available.length === 0)
|
|
138
115
|
return null;
|
|
139
116
|
const observedAt = latestObservedAt(coverage);
|
|
140
|
-
return (_jsxs("div", { className: "border-border/60 bg-muted/25 flex flex-wrap items-center gap-x-3 gap-y-1.5 border-t px-4 py-2.5", children: [_jsx("span", { className: "text-muted-foreground shrink-0 text-xs", children: runtimeLabel }), _jsx("div", { className: "flex flex-wrap items-center gap-1.5", children: available.map((item) => (_jsx(SandboxProviderBadge, { item: item,
|
|
117
|
+
return (_jsxs("div", { className: "border-border/60 bg-muted/25 flex flex-wrap items-center gap-x-3 gap-y-1.5 border-t px-4 py-2.5", children: [_jsx("span", { className: "text-muted-foreground shrink-0 text-xs", children: runtimeLabel }), _jsx("div", { className: "flex flex-wrap items-center gap-1.5", children: available.map((item) => (_jsx(SandboxProviderBadge, { item: item, copy: badgeCopy }, item.provider))) }), observedAt ? (_jsx("span", { className: "text-muted-foreground ml-auto shrink-0 text-xs tabular-nums", children: checkedLabel(formatObservedAt(observedAt)) })) : null] }));
|
|
141
118
|
}
|
|
142
|
-
function TemplateCard({ projectId, template, canManage, onEdit,
|
|
119
|
+
function TemplateCard({ projectId, template, canManage, onEdit, }) {
|
|
143
120
|
const tI18nComplete = useTranslations('hardcodedUi.i18nComplete');
|
|
144
121
|
const t = useTranslations('settings.sandbox');
|
|
145
122
|
const locale = useLocale();
|
|
@@ -189,12 +166,7 @@ function TemplateCard({ projectId, template, canManage, onEdit, providerMode, se
|
|
|
189
166
|
builtFrom: t('facts.builtFrom'),
|
|
190
167
|
kortixDefault: t('facts.kortixDefault'),
|
|
191
168
|
});
|
|
192
|
-
|
|
193
|
-
automatic: t('routing.automatic'),
|
|
194
|
-
pinned: t('routing.pinned'),
|
|
195
|
-
pinnedWithProvider: (provider) => t('routing.pinnedWithProvider', { provider }),
|
|
196
|
-
});
|
|
197
|
-
return (_jsxs(_Fragment, { children: [_jsxs("li", { className: "bg-popover overflow-hidden rounded-md border", children: [_jsxs("div", { className: "flex flex-wrap items-start justify-between gap-3 px-4 pt-4 pb-3.5", children: [_jsxs("div", { className: "min-w-0 space-y-1.5", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-1.5", children: [_jsx("span", { className: "text-foreground truncate text-sm font-medium", children: template.name }), _jsx(Badge, { variant: "secondary", size: "sm", children: template.slug }), template.is_default ? (_jsx(Badge, { variant: "muted", size: "sm", children: t('defaultBadge') })) : null] }), _jsx(TemplateStateLine, { tone: stateInfo.tone, label: stateInfo.label })] }), canManage && (_jsxs("div", { className: "flex items-center gap-1", children: [templateId && !template.is_default && (_jsxs(_Fragment, { children: [_jsx(Hint, { label: t('actions.edit'), side: "top", children: _jsx(Button, { size: "icon-base", variant: "ghost", className: "transition-transform active:scale-[0.96]", onClick: () => onEdit(template), "aria-label": t('actions.edit'), children: _jsx(PencilSimpleIcon, { className: "size-3.5 shrink-0" }) }) }), _jsx(Hint, { label: t('actions.delete'), side: "top", children: _jsx(Button, { size: "icon-base", variant: "ghost", className: "text-destructive hover:text-destructive transition-transform active:scale-[0.96]", disabled: deleteMut.isPending, onClick: () => setConfirmDelete(true), "aria-label": t('actions.delete'), children: deleteMut.isPending ? (_jsx(Loading, { className: "size-3.5 shrink-0" })) : (_jsx(TrashIcon, { className: "size-3.5 shrink-0" })) }) })] })), templateId && (_jsxs(Button, { size: "sm", variant: "outline", className: "gap-1.5 transition-transform active:scale-[0.96]", disabled: buildMut.isPending, onClick: () => buildMut.mutate(), children: [buildMut.isPending ? (_jsx(Loading, { className: "size-3.5 shrink-0" })) : (_jsx(ArrowClockwiseIcon, { className: "size-3.5 shrink-0" })), t('actions.rebuild')] }))] }))] }), _jsxs("dl", { className: "border-border/60 grid grid-cols-2 gap-x-4 gap-y-4 border-t px-4 py-4 tabular-nums sm:grid-cols-3", children: [_jsx(FactCell, { icon: CpuIcon, label: t('facts.processor'), value: `${template.cpu} vCPU` }), _jsx(FactCell, { icon: MemoryIcon, label: t('facts.memory'), value: `${template.memory_gb} GiB` }), _jsx(FactCell, { icon: HardDrivesIcon, label: t('facts.storage'), value: `${template.disk_gb} GiB` }), _jsx(FactCell, { icon: base.icon, label: base.label, value: base.value, mono: base.mono }), _jsx(FactCell, { icon: source.icon, label: t('facts.definedIn'), value: source.label, mono: source.mono }), _jsx(FactCell, { icon: routing.icon, label: t('routing.title'), value: routing.label })] }), _jsx(TemplateRuntimeFooter, { coverage: template.provider_coverage, providerMode: providerMode, selectedProvider: selectedProvider, runtimeLabel: t('runtime'), checkedLabel: (value) => t('checked', { value }), formatObservedAt: (observedAt) => relativeTime(observedAt, locale, Date.now(), LONG_AGO), badgeCopy: {
|
|
169
|
+
return (_jsxs(_Fragment, { children: [_jsxs("li", { className: "bg-popover overflow-hidden rounded-md border", children: [_jsxs("div", { className: "flex flex-wrap items-start justify-between gap-3 px-4 pt-4 pb-3.5", children: [_jsxs("div", { className: "min-w-0 space-y-1.5", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-1.5", children: [_jsx("span", { className: "text-foreground truncate text-sm font-medium", children: template.name }), _jsx(Badge, { variant: "secondary", size: "sm", children: template.slug }), template.is_default ? (_jsx(Badge, { variant: "muted", size: "sm", children: t('defaultBadge') })) : null] }), _jsx(TemplateStateLine, { tone: stateInfo.tone, label: stateInfo.label })] }), canManage && (_jsxs("div", { className: "flex items-center gap-1", children: [templateId && !template.is_default && (_jsxs(_Fragment, { children: [_jsx(Hint, { label: t('actions.edit'), side: "top", children: _jsx(Button, { size: "icon-base", variant: "ghost", className: "transition-transform active:scale-[0.96]", onClick: () => onEdit(template), "aria-label": t('actions.edit'), children: _jsx(PencilSimpleIcon, { className: "size-3.5 shrink-0" }) }) }), _jsx(Hint, { label: t('actions.delete'), side: "top", children: _jsx(Button, { size: "icon-base", variant: "ghost", className: "text-destructive hover:text-destructive transition-transform active:scale-[0.96]", disabled: deleteMut.isPending, onClick: () => setConfirmDelete(true), "aria-label": t('actions.delete'), children: deleteMut.isPending ? (_jsx(Loading, { className: "size-3.5 shrink-0" })) : (_jsx(TrashIcon, { className: "size-3.5 shrink-0" })) }) })] })), templateId && (_jsxs(Button, { size: "sm", variant: "outline", className: "gap-1.5 transition-transform active:scale-[0.96]", disabled: buildMut.isPending, onClick: () => buildMut.mutate(), children: [buildMut.isPending ? (_jsx(Loading, { className: "size-3.5 shrink-0" })) : (_jsx(ArrowClockwiseIcon, { className: "size-3.5 shrink-0" })), t('actions.rebuild')] }))] }))] }), _jsxs("dl", { className: "border-border/60 grid grid-cols-2 gap-x-4 gap-y-4 border-t px-4 py-4 tabular-nums sm:grid-cols-3", children: [_jsx(FactCell, { icon: CpuIcon, label: t('facts.processor'), value: `${template.cpu} vCPU` }), _jsx(FactCell, { icon: MemoryIcon, label: t('facts.memory'), value: `${template.memory_gb} GiB` }), _jsx(FactCell, { icon: HardDrivesIcon, label: t('facts.storage'), value: `${template.disk_gb} GiB` }), _jsx(FactCell, { icon: base.icon, label: base.label, value: base.value, mono: base.mono }), _jsx(FactCell, { icon: source.icon, label: t('facts.definedIn'), value: source.label, mono: source.mono })] }), _jsx(TemplateRuntimeFooter, { coverage: template.provider_coverage, runtimeLabel: t('runtime'), checkedLabel: (value) => t('checked', { value }), formatObservedAt: (observedAt) => relativeTime(observedAt, locale, Date.now(), LONG_AGO), badgeCopy: {
|
|
198
170
|
states: {
|
|
199
171
|
ready: t('coverage.ready'),
|
|
200
172
|
building: t('coverage.building'),
|
|
@@ -203,71 +175,14 @@ function TemplateCard({ projectId, template, canManage, onEdit, providerMode, se
|
|
|
203
175
|
unavailable: t('coverage.unavailable'),
|
|
204
176
|
unknown: t('states.unknown'),
|
|
205
177
|
},
|
|
206
|
-
selected: t('coverage.selected'),
|
|
207
|
-
selectedAria: t('coverage.selectedAria'),
|
|
208
178
|
} })] }), _jsx(ConfirmDialog, { open: confirmDelete, onOpenChange: setConfirmDelete, title: t('deleteDialog.title', { name: template.name }), description: t('deleteDialog.description'), confirmLabel: t('deleteDialog.confirm'), confirmVariant: "destructive", isPending: deleteMut.isPending, onConfirm: () => deleteMut.mutate() })] }));
|
|
209
179
|
}
|
|
210
180
|
const TEMPLATE_SKELETON_HEIGHT = 'h-60';
|
|
211
|
-
const AUTO_PROVIDER = '__auto__';
|
|
212
|
-
function SandboxProviderRow({ project, canManage, }) {
|
|
213
|
-
const tI18nComplete = useTranslations('hardcodedUi.i18nComplete');
|
|
214
|
-
const t = useTranslations('settings.sandbox');
|
|
215
|
-
const queryClient = useQueryClient();
|
|
216
|
-
const available = sortSandboxProviders(project.available_sandbox_providers ?? []);
|
|
217
|
-
const current = project.default_sandbox_provider ?? null;
|
|
218
|
-
const mutation = useMutation({
|
|
219
|
-
mutationFn: (next) => updateProjectSandboxProvider(project.project_id, next),
|
|
220
|
-
onSuccess: (result, next) => {
|
|
221
|
-
// FIX-L: the PATCH returns EITHER the updated project (immediate) OR a
|
|
222
|
-
// preparation object (the prepare branch — a switch to a different enabled
|
|
223
|
-
// provider). Write the project cache ONLY for the immediate result; a
|
|
224
|
-
// preparation is a transition, not a project, and must not clobber the
|
|
225
|
-
// cached project shape.
|
|
226
|
-
const kind = applySandboxProviderResult(queryClient, project.project_id, result);
|
|
227
|
-
if (kind === 'preparation') {
|
|
228
|
-
successToast(tI18nComplete('text32f14aa9ea33', {
|
|
229
|
-
provider: next ? formatSandboxProvider(next) : tI18nComplete('text22412aa281cb'),
|
|
230
|
-
}));
|
|
231
|
-
// Poll the durable transition (bounded, backoff, terminal-stop, 404 = done)
|
|
232
|
-
// and refresh the project once it settles so the now-active provider shows.
|
|
233
|
-
void pollSandboxProviderTransition(project.project_id, {
|
|
234
|
-
onSettled: (state) => {
|
|
235
|
-
// invalidateProject() reaches qk.project.scope(project.project_id),
|
|
236
|
-
// which qk.project.summary(project.project_id) nests under — so it
|
|
237
|
-
// already covers the bare-project row too; no separate summary
|
|
238
|
-
// invalidation needed here.
|
|
239
|
-
void invalidateProject(queryClient, project.project_id);
|
|
240
|
-
// qk.projects.scope(): restores the reach the old bare
|
|
241
|
-
// projects-literal prefix match had. A sandbox-provider switch
|
|
242
|
-
// is rare — over-invalidating a few extra account lists costs
|
|
243
|
-
// nothing measurable.
|
|
244
|
-
queryClient.invalidateQueries({ queryKey: qk.projects.scope() });
|
|
245
|
-
const status = state?.latest?.status;
|
|
246
|
-
if (status === 'activated') {
|
|
247
|
-
successToast(tI18nComplete('text1c145081eddd', {
|
|
248
|
-
provider: formatSandboxProvider(state?.latest?.target_provider ?? ''),
|
|
249
|
-
}));
|
|
250
|
-
}
|
|
251
|
-
else if (status === 'failed') {
|
|
252
|
-
errorToast(state?.latest?.label || tI18nComplete('texte9ca8360a34d'));
|
|
253
|
-
}
|
|
254
|
-
},
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
},
|
|
258
|
-
onError: (error) => errorToast(error.message || tI18nComplete('text914be6d9bb89')),
|
|
259
|
-
});
|
|
260
|
-
if (available.length === 0)
|
|
261
|
-
return null;
|
|
262
|
-
return (_jsxs("section", { className: "space-y-3", children: [_jsx(SettingsSubsectionHeader, { title: t('routing.title') }), _jsx(SettingsRowGroup, { children: _jsx(SettingsRow, { label: _jsxs(_Fragment, { children: [t('provider.label'), _jsx(Badge, { variant: "highlight", size: "sm", children: t('provider.experimental') })] }), description: available.some(isLegacySandboxProvider)
|
|
263
|
-
? `${t('provider.description')} ${t('provider.legacyNote')}`
|
|
264
|
-
: t('provider.description'), children: _jsxs(Select, { value: current ?? AUTO_PROVIDER, onValueChange: (v) => mutation.mutate(v === AUTO_PROVIDER ? null : (available.find((provider) => provider === v) ?? null)), disabled: !canManage || mutation.isPending, children: [_jsx(SelectTrigger, { "aria-label": t('provider.label'), className: "h-8 w-48 shrink-0", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { align: "end", children: [_jsx(SelectItem, { value: AUTO_PROVIDER, children: t('routing.automatic') }), available.map((p) => (_jsx(SelectItem, { value: p, children: _jsx(SandboxProviderLabel, { provider: p }) }, p)))] })] }) }) })] }));
|
|
265
|
-
}
|
|
266
181
|
/** Presentational only — no hooks, no data fetching, no store or Supabase
|
|
267
182
|
* read. Kept separate from `SandboxTab` so this renders under
|
|
268
183
|
* `renderToStaticMarkup` with no `QueryClientProvider` — see
|
|
269
184
|
* `GeneralTabView` for the same split. */
|
|
270
|
-
export function SandboxTabView({ isLoading = false, isError = false, errorMessage = '', onRetry = () => { }, headerAction,
|
|
185
|
+
export function SandboxTabView({ isLoading = false, isError = false, errorMessage = '', onRetry = () => { }, headerAction, manifestVersion = null, templatesError = null, isEmpty = true, emptyAction, templatesSlot, copy: copyOverride, }) {
|
|
271
186
|
const t = useTranslations('settings.sandbox');
|
|
272
187
|
const copy = copyOverride ?? {
|
|
273
188
|
loadFailed: t('loadFailed'),
|
|
@@ -279,7 +194,7 @@ export function SandboxTabView({ isLoading = false, isError = false, errorMessag
|
|
|
279
194
|
emptyDescription: t('emptyDescription'),
|
|
280
195
|
};
|
|
281
196
|
const tI18nComplete = useTranslations('hardcodedUi.i18nComplete');
|
|
282
|
-
return (_jsx("div", { className: "mx-auto w-full max-w-2xl space-y-8", children: _jsxs("div", { className: "space-y-8", children: [_jsx(SettingsTabHeader, { tab: "sandbox", action: headerAction }),
|
|
197
|
+
return (_jsx("div", { className: "mx-auto w-full max-w-2xl space-y-8", children: _jsxs("div", { className: "space-y-8", children: [_jsx(SettingsTabHeader, { tab: "sandbox", action: headerAction }), isLoading ? (_jsx("div", { className: "space-y-2", children: TEMPLATE_SKELETON_ROWS.map((row) => (_jsx(Skeleton, { className: cn(TEMPLATE_SKELETON_HEIGHT, 'rounded-md') }, row))) })) : isError ? (_jsx(ErrorState, { size: "sm", title: copy.loadFailed, description: errorMessage, action: _jsx(Button, { variant: "outline", size: "sm", onClick: onRetry, children: copy.retry }) })) : (_jsxs("div", { className: "space-y-4", children: [_jsxs("p", { className: "text-muted-foreground text-sm text-pretty", children: [copy.descriptionStart, " ", _jsx("code", { className: "font-mono", children: "/workspace" }), ".", ' ', copy.descriptionEnd, ' ', manifestFilename(manifestVersion) === 'kortix.yaml' ? (_jsxs(_Fragment, { children: [_jsx("code", { className: "font-mono", children: "sandbox.templates" }), ' ', tI18nComplete.raw('text582967534d0f'), ' ', _jsx("code", { className: "font-mono", children: "kortix.yaml" })] })) : (_jsxs(_Fragment, { children: [_jsx("code", { className: "font-mono", children: "[[sandbox.templates]]" }), ' ', tI18nComplete.raw('text582967534d0f'), ' ', _jsx("code", { className: "font-mono", children: "kortix.toml" })] })), "."] }), templatesError ? (_jsxs(InfoBanner, { tone: "warning", children: [copy.configReadFailed, " ", templatesError] })) : null, isEmpty ? (_jsx(EmptyState, { icon: ShippingContainerIcon, size: "sm", title: copy.emptyTitle, description: copy.emptyDescription, action: emptyAction })) : (templatesSlot)] }))] }) }));
|
|
283
198
|
}
|
|
284
199
|
/** Container: owns every hook (project + snapshots queries, the template
|
|
285
200
|
* form's open/edit state) and renders `SandboxTabView` with real data +
|
|
@@ -289,17 +204,8 @@ export function SandboxTabView({ isLoading = false, isError = false, errorMessag
|
|
|
289
204
|
* `null` otherwise), so nothing here fetches on panel open. */
|
|
290
205
|
export function SandboxTab({ projectId }) {
|
|
291
206
|
const t = useTranslations('settings.sandbox');
|
|
292
|
-
const projectQuery = useQuery({
|
|
293
|
-
queryKey: qk.project.summary(projectId),
|
|
294
|
-
queryFn: () => getProject(projectId),
|
|
295
|
-
...contract('config'),
|
|
296
|
-
});
|
|
297
|
-
// Two different leaves, one batched probe. Template CRUD asserts
|
|
298
|
-
// `project.sandbox.write`; the provider row asserts `project.write`. They
|
|
299
|
-
// are deliberately NOT merged — a role may hold one without the other.
|
|
300
207
|
const caps = useProjectCans(projectId, SANDBOX_TAB_ACTIONS);
|
|
301
208
|
const canManage = caps[PROJECT_ACTIONS.PROJECT_SANDBOX_WRITE]?.allowed === true;
|
|
302
|
-
const canEditProvider = caps[PROJECT_ACTIONS.PROJECT_WRITE]?.allowed === true;
|
|
303
209
|
const { version: manifestVersion } = useProjectManifestVersion(projectId);
|
|
304
210
|
const snapshotsQuery = useQuery({
|
|
305
211
|
queryKey: qk.project.snapshots(projectId),
|
|
@@ -320,11 +226,6 @@ export function SandboxTab({ projectId }) {
|
|
|
320
226
|
const [editingTemplate, setEditingTemplate] = useState(null);
|
|
321
227
|
const data = snapshotsQuery.data;
|
|
322
228
|
const templates = Array.isArray(data?.templates) ? data.templates : [];
|
|
323
|
-
const pinnedProvider = projectQuery.data
|
|
324
|
-
? (projectQuery.data.default_sandbox_provider ?? null)
|
|
325
|
-
: (data?.selected_provider ?? null);
|
|
326
|
-
const providerMode = pinnedProvider ? 'pinned' : 'automatic';
|
|
327
|
-
const selectedProvider = pinnedProvider;
|
|
328
229
|
const openNewForm = () => {
|
|
329
230
|
setEditingTemplate(null);
|
|
330
231
|
setFormOpen(true);
|
|
@@ -334,5 +235,5 @@ export function SandboxTab({ projectId }) {
|
|
|
334
235
|
setFormOpen(true);
|
|
335
236
|
};
|
|
336
237
|
const newTemplateAction = canManage ? (_jsxs(Button, { size: "sm", variant: "outline", className: "gap-1.5 transition-transform active:scale-[0.96]", onClick: openNewForm, children: [_jsx(PlusIcon, { className: "size-3.5 shrink-0" }), t('actions.new')] })) : undefined;
|
|
337
|
-
return (_jsxs(_Fragment, { children: [_jsx(SandboxTabView, { isLoading: snapshotsQuery.isLoading, isError: snapshotsQuery.isError, errorMessage: snapshotsQuery.error?.message ?? '', onRetry: () => snapshotsQuery.refetch(), headerAction: canManage ? (_jsxs(Button, { size: "sm", variant: "secondary", className: "gap-1.5 transition-transform active:scale-[0.96]", onClick: openNewForm, children: [_jsx(PlusIcon, { className: "size-4 shrink-0" }), t('actions.new')] })) : undefined,
|
|
238
|
+
return (_jsxs(_Fragment, { children: [_jsx(SandboxTabView, { isLoading: snapshotsQuery.isLoading, isError: snapshotsQuery.isError, errorMessage: snapshotsQuery.error?.message ?? '', onRetry: () => snapshotsQuery.refetch(), headerAction: canManage ? (_jsxs(Button, { size: "sm", variant: "secondary", className: "gap-1.5 transition-transform active:scale-[0.96]", onClick: openNewForm, children: [_jsx(PlusIcon, { className: "size-4 shrink-0" }), t('actions.new')] })) : undefined, manifestVersion: manifestVersion, templatesError: data?.templates_error ?? null, isEmpty: templates.length === 0, emptyAction: newTemplateAction, templatesSlot: templates.length === 0 ? undefined : (_jsx("ul", { className: "space-y-2", children: templates.map((t) => (_jsx(TemplateCard, { projectId: projectId, template: t, canManage: canManage, onEdit: openEditForm }, t.template_id ?? `tpl-${t.slug}`))) })) }), _jsx(SandboxTemplateForm, { projectId: projectId, open: formOpen, onOpenChange: setFormOpen, template: editingTemplate })] }));
|
|
338
239
|
}
|
|
@@ -29,6 +29,10 @@ export interface EntityDetailModalProps {
|
|
|
29
29
|
* uses this so editing happens in this same shell (one level deep), not
|
|
30
30
|
* in a modal stacked on the modal. Omit for the normal file pane. */
|
|
31
31
|
paneOverride?: ReactNode;
|
|
32
|
+
/** Offer Remove for a project skill. Only the Skills page opts in: it closes
|
|
33
|
+
* the modal when the refetched config drops the skill. The agent editor
|
|
34
|
+
* does not, because its unsaved draft could grant the removed skill again. */
|
|
35
|
+
removable?: boolean;
|
|
32
36
|
onOpenChange: (open: boolean) => void;
|
|
33
37
|
}
|
|
34
38
|
/**
|
|
@@ -45,7 +49,7 @@ export interface EntityDetailModalProps {
|
|
|
45
49
|
* `Modal`/`ModalContent` itself, which would replay the open animation and
|
|
46
50
|
* drop focus trap continuity.
|
|
47
51
|
*/
|
|
48
|
-
export declare function EntityDetailModal({ projectId, entity, kind, open, isResolving, aside, meta, paneOverride, onOpenChange, }: EntityDetailModalProps): import("react").JSX.Element;
|
|
52
|
+
export declare function EntityDetailModal({ projectId, entity, kind, open, isResolving, aside, meta, paneOverride, removable, onOpenChange, }: EntityDetailModalProps): import("react").JSX.Element;
|
|
49
53
|
export declare function EntityFilePane({ path, content, isLoading, isError, error, onRetry, errorTitle, skeletonRows, }: {
|
|
50
54
|
path: string;
|
|
51
55
|
content: string | undefined;
|
|
@@ -21,6 +21,7 @@ import { useProjectAccountId } from '@kortix/sdk/react';
|
|
|
21
21
|
import { PencilSimpleIcon } from '@phosphor-icons/react';
|
|
22
22
|
import { useQuery } from '@tanstack/react-query';
|
|
23
23
|
import { FadedScrollArea } from '../../../primitives/faded-scroll-area.js';
|
|
24
|
+
import { RemoveSkillButton, removableSkillFolder } from '../../skills/remove-skill-button.js';
|
|
24
25
|
import { buildFileTree, entityDirectory, isMarkdownPath, languageForPath } from './entity-files.js';
|
|
25
26
|
const WRITE_ACTION = {
|
|
26
27
|
skill: PROJECT_ACTIONS.PROJECT_SKILL_WRITE,
|
|
@@ -60,7 +61,7 @@ const OWNS_ITS_DIRECTORY = {
|
|
|
60
61
|
* `Modal`/`ModalContent` itself, which would replay the open animation and
|
|
61
62
|
* drop focus trap continuity.
|
|
62
63
|
*/
|
|
63
|
-
export function EntityDetailModal({ projectId, entity, kind, open, isResolving = false, aside, meta, paneOverride, onOpenChange, }) {
|
|
64
|
+
export function EntityDetailModal({ projectId, entity, kind, open, isResolving = false, aside, meta, paneOverride, removable = false, onOpenChange, }) {
|
|
64
65
|
// A skill/command with no description renders no `ModalDescription`, so
|
|
65
66
|
// Radix's Dialog can't find one to auto-associate via `aria-describedby`
|
|
66
67
|
// and logs a dev-only "Missing Description" warning. Passing
|
|
@@ -69,7 +70,7 @@ export function EntityDetailModal({ projectId, entity, kind, open, isResolving =
|
|
|
69
70
|
// message itself names this exact fix. Only applied when there truly is no
|
|
70
71
|
// description; when one exists, `ModalDescription` wires up normally.
|
|
71
72
|
const suppressDescriptionWarning = !entity?.description;
|
|
72
|
-
return (_jsx(Modal, { open: open, onOpenChange: onOpenChange, children: _jsx(ModalContent, { className: "bg-popover space-y-0 lg:max-w-6xl", ...(suppressDescriptionWarning ? { 'aria-describedby': undefined } : {}), children: entity ? (_jsx(EntityModalBody, { projectId: projectId, entity: entity, kind: kind, aside: aside, meta: meta, paneOverride: paneOverride }, entity.path)) : isResolving ? (_jsx(EntityModalSkeleton, { kind: kind })) : null }) }));
|
|
73
|
+
return (_jsx(Modal, { open: open, onOpenChange: onOpenChange, children: _jsx(ModalContent, { className: "bg-popover space-y-0 lg:max-w-6xl", ...(suppressDescriptionWarning ? { 'aria-describedby': undefined } : {}), children: entity ? (_jsx(EntityModalBody, { projectId: projectId, entity: entity, kind: kind, aside: aside, meta: meta, paneOverride: paneOverride, removable: removable }, entity.path)) : isResolving ? (_jsx(EntityModalSkeleton, { kind: kind })) : null }) }));
|
|
73
74
|
}
|
|
74
75
|
/**
|
|
75
76
|
* The shell, while the selected path is being resolved against a config that
|
|
@@ -83,7 +84,7 @@ function EntityModalSkeleton({ kind }) {
|
|
|
83
84
|
const tI18nComplete = useTranslations('hardcodedUi.i18nComplete');
|
|
84
85
|
return (_jsxs(_Fragment, { children: [_jsxs(ModalHeader, { className: "border-border/60 space-y-1 border-b pb-4", children: [_jsxs(ModalTitle, { className: "sr-only", children: [tI18nComplete.raw('textdc380888c4e2'), " ", kind] }), _jsx(Skeleton, { className: "h-5 w-48 rounded-sm", "aria-hidden": true })] }), _jsx(ModalBody, { className: "max-h-[70vh] overflow-hidden p-0", children: _jsxs("div", { className: "flex min-h-0 flex-col lg:h-[70vh] lg:flex-row", children: [OWNS_ITS_DIRECTORY[kind] ? (_jsx("div", { className: "border-border/60 shrink-0 space-y-1.5 border-b p-2 lg:h-full lg:w-64 lg:border-r lg:border-b-0", children: [0, 1, 2].map((i) => (_jsx(Skeleton, { className: "h-7 w-full rounded-md" }, i))) })) : null, _jsxs("div", { className: "min-w-0 flex-1 space-y-3 p-4", children: [_jsx(Skeleton, { className: "h-4 w-32 rounded-sm" }), _jsx(Skeleton, { className: "h-40 w-full rounded-md" })] })] }) })] }));
|
|
85
86
|
}
|
|
86
|
-
function EntityModalBody({ projectId, entity, kind, aside, meta, paneOverride, }) {
|
|
87
|
+
function EntityModalBody({ projectId, entity, kind, aside, meta, paneOverride, removable, }) {
|
|
87
88
|
const tI18nComplete = useTranslations('hardcodedUi.i18nComplete');
|
|
88
89
|
const configure = useConfigureThread(projectId);
|
|
89
90
|
// `accountId` skips useProjectCan's own getProject and lets the IAM probe
|
|
@@ -126,12 +127,14 @@ function EntityModalBody({ projectId, entity, kind, aside, meta, paneOverride, }
|
|
|
126
127
|
// click in the alert as an outside-click and dismiss the modal underneath
|
|
127
128
|
// (see packages/ui/src/primitives/alert-dialog.tsx).
|
|
128
129
|
const editButton = canWrite ? (_jsx(EntityEditButton, { name: entity.name, pending: configure.pending, onEdit: () => configure.start(editConfigPrompt(kind, entity.name, entity.path)) })) : null;
|
|
130
|
+
// Only a project skill can be removed; the API refuses a platform `kortix-*` one.
|
|
131
|
+
const removableFolder = removable && canWrite && kind === 'skill' ? removableSkillFolder(entity.path) : null;
|
|
129
132
|
// The aside earns its column when it holds something: the caller's settings,
|
|
130
133
|
// or — for a single-file kind, which has no rail to host it — the edit
|
|
131
134
|
// action. Without this an agent modal viewed by a reader with no write
|
|
132
135
|
// permission would render an empty 320px column.
|
|
133
136
|
const showAside = Boolean(aside) || (!hasFileRail && editButton !== null);
|
|
134
|
-
const fileRail = (_jsxs("div", { className: "border-border/60 flex shrink-0 flex-col border-b p-2 lg:h-full lg:w-64 lg:overflow-hidden lg:border-r lg:border-b-0", children: [_jsxs(FadedScrollArea, { fadeColor: "from-popover", rootClassName: "h-auto min-h-0 flex-1", className: "overscroll-contain", children: [_jsx(FileTree, { title: tI18nComplete.raw('textabc7e9892806'), children: _jsx(FileTreeNav, { nodes: nodes, selectedPath: selectedPath, onSelect: setSelectedPath, label: `${entity.name} files` }) }), filesQuery.isError ? (_jsxs("p", { className: "text-muted-foreground mt-4 text-xs text-pretty", children: [tI18nComplete.raw('text881c0bd6449b'), ' ', _jsx("button", { type: "button", onClick: () => void filesQuery.refetch(), className: "text-foreground underline underline-offset-2", children: tI18nComplete.raw('text942087cc2d41') })] })) : null] }), editButton ?
|
|
137
|
+
const fileRail = (_jsxs("div", { className: "border-border/60 flex shrink-0 flex-col border-b p-2 lg:h-full lg:w-64 lg:overflow-hidden lg:border-r lg:border-b-0", children: [_jsxs(FadedScrollArea, { fadeColor: "from-popover", rootClassName: "h-auto min-h-0 flex-1", className: "overscroll-contain", children: [_jsx(FileTree, { title: tI18nComplete.raw('textabc7e9892806'), children: _jsx(FileTreeNav, { nodes: nodes, selectedPath: selectedPath, onSelect: setSelectedPath, label: `${entity.name} files` }) }), filesQuery.isError ? (_jsxs("p", { className: "text-muted-foreground mt-4 text-xs text-pretty", children: [tI18nComplete.raw('text881c0bd6449b'), ' ', _jsx("button", { type: "button", onClick: () => void filesQuery.refetch(), className: "text-foreground underline underline-offset-2", children: tI18nComplete.raw('text942087cc2d41') })] })) : null] }), editButton ? (_jsxs("div", { className: "shrink-0 space-y-2 pt-4", children: [editButton, removableFolder ? (_jsx(RemoveSkillButton, { projectId: projectId, name: entity.name, folder: removableFolder })) : null] })) : null] }));
|
|
135
138
|
return (_jsxs(_Fragment, { children: [_jsx(ModalHeader, { className: "border-border/60 space-y-1 border-b pr-10 pb-4", children: _jsxs("div", { className: "flex flex-wrap items-center gap-x-2 gap-y-1", children: [_jsx(ModalTitle, { className: "min-w-0 truncate", children: entity.name }), meta] }) }), _jsx(ModalBody, { className: "max-h-[70vh] overflow-hidden p-0", children: _jsxs("div", { className: "flex min-h-0 flex-col overflow-y-auto lg:h-[70vh] lg:flex-row lg:overflow-hidden", children: [hasFileRail ? fileRail : null, showAside ? (_jsxs("aside", { className: "border-border/60 flex h-full min-h-0 shrink-0 flex-col justify-between space-y-3 border-b p-4 lg:h-full lg:min-h-0 lg:w-80 lg:overflow-y-auto lg:border-r lg:border-b-0", children: [aside, _jsx("div", { className: "mt-auto pt-4", children: hasFileRail ? null : editButton })] })) : null, _jsx("div", { className: cn('bg-popover min-w-0 flex-1 overflow-hidden', paneOverride ? 'flex flex-col' : 'overflow-y-auto'), children: paneOverride ?? (_jsx(EntityFilePane, { path: selectedPath, content: fileQuery.data?.content, isLoading: fileQuery.isLoading, isError: fileQuery.isError, error: fileQuery.error, onRetry: () => fileQuery.refetch() }, selectedPath)) })] }) })] }));
|
|
136
139
|
}
|
|
137
140
|
export function EntityFilePane({ path, content, isLoading, isError, error, onRetry, errorTitle, skeletonRows = 5, }) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The folder `POST /skills/remove` takes for the skill at `path`, or null when
|
|
3
|
+
* the API refuses it: a platform `kortix-*` skill, or a SKILL.md at the root.
|
|
4
|
+
*/
|
|
5
|
+
export declare function removableSkillFolder(path: string): string | null;
|
|
6
|
+
/**
|
|
7
|
+
* Removes a project skill: its whole folder, edits included, and its entry on
|
|
8
|
+
* every agent, in one commit on the default branch (KRTX-2821). The click only
|
|
9
|
+
* opens the confirmation. On success the project config is refetched; it no
|
|
10
|
+
* longer lists the skill, so the detail modal around this button closes itself.
|
|
11
|
+
*/
|
|
12
|
+
export declare function RemoveSkillButton({ projectId, name, folder, }: {
|
|
13
|
+
projectId: string;
|
|
14
|
+
name: string;
|
|
15
|
+
folder: string;
|
|
16
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { removeProjectSkill } from '@kortix/sdk';
|
|
4
|
+
import { qk } from '@kortix/sdk/react';
|
|
5
|
+
import { TrashIcon } from '@phosphor-icons/react';
|
|
6
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
7
|
+
import { useState } from 'react';
|
|
8
|
+
import { useTranslations } from '../../i18n/index.js';
|
|
9
|
+
import { Button } from '../../primitives/button.js';
|
|
10
|
+
import { ConfirmDialog } from '../../primitives/confirm-dialog.js';
|
|
11
|
+
import { errorToast, successToast } from '../../primitives/toast.js';
|
|
12
|
+
import { entityDirectory } from '../shared/entity/entity-files.js';
|
|
13
|
+
/**
|
|
14
|
+
* The folder `POST /skills/remove` takes for the skill at `path`, or null when
|
|
15
|
+
* the API refuses it: a platform `kortix-*` skill, or a SKILL.md at the root.
|
|
16
|
+
*/
|
|
17
|
+
export function removableSkillFolder(path) {
|
|
18
|
+
const folder = entityDirectory(path);
|
|
19
|
+
const name = folder.split('/').at(-1) ?? '';
|
|
20
|
+
return folder && !name.startsWith('kortix-') ? folder : null;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Removes a project skill: its whole folder, edits included, and its entry on
|
|
24
|
+
* every agent, in one commit on the default branch (KRTX-2821). The click only
|
|
25
|
+
* opens the confirmation. On success the project config is refetched; it no
|
|
26
|
+
* longer lists the skill, so the detail modal around this button closes itself.
|
|
27
|
+
*/
|
|
28
|
+
export function RemoveSkillButton({ projectId, name, folder, }) {
|
|
29
|
+
const tI18nComplete = useTranslations('hardcodedUi.i18nComplete');
|
|
30
|
+
const queryClient = useQueryClient();
|
|
31
|
+
const [confirming, setConfirming] = useState(false);
|
|
32
|
+
const remove = useMutation({
|
|
33
|
+
mutationFn: () => removeProjectSkill(projectId, { folder, confirm: true }),
|
|
34
|
+
onSuccess: () => {
|
|
35
|
+
setConfirming(false);
|
|
36
|
+
successToast(tI18nComplete.raw('text5f6ba69d66d1'));
|
|
37
|
+
void queryClient.invalidateQueries({ queryKey: qk.project.detail(projectId) });
|
|
38
|
+
},
|
|
39
|
+
onError: (error) => errorToast(error.message),
|
|
40
|
+
});
|
|
41
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Button, { variant: "outline", size: "sm", className: "w-full justify-start gap-1.5", disabled: remove.isPending, onClick: () => setConfirming(true), children: [_jsx(TrashIcon, { className: "size-3.5 shrink-0" }), tI18nComplete.raw('textc3812fc4acb8')] }), _jsx(ConfirmDialog, { open: confirming, onOpenChange: setConfirming, title: tI18nComplete('textbc43ab815937', { value0: name }), description: _jsxs(_Fragment, { children: [tI18nComplete.raw('text0c044575853f'), " ", _jsx("code", { className: "font-mono", children: folder }), tI18nComplete.raw('textcb8d6b0c94c9')] }), confirmLabel: tI18nComplete.raw('texta8597db6f24b'), confirmVariant: "destructive", confirmIcon: _jsx(TrashIcon, { className: "size-4 shrink-0" }), isPending: remove.isPending, onConfirm: () => remove.mutate() })] }));
|
|
42
|
+
}
|
|
@@ -110,7 +110,7 @@ export function SkillsPage({ projectId }) {
|
|
|
110
110
|
// The copy invites an action, so the action is here — not only in
|
|
111
111
|
// the header. Docs stays as the secondary, for the reader who has
|
|
112
112
|
// no write permission and gets no primary at all.
|
|
113
|
-
action: createButton('Create a skill'), secondaryAction: _jsx(Button, { asChild: true, variant: "ghost", size: "sm", className: "gap-1.5", children: _jsx("a", { href: "https://opencode.ai/docs/skills/", target: "_blank", rel: "noopener noreferrer", children: tI18nComplete.raw('text7af023c43013') }) }) })), children: filtered.map((skill) => (_jsx(CatalogCard, { title: skill.name, description: skill.description, onClick: () => setSelectedPath(skill.path) }, skill.path))) }), _jsx(EntityDetailModal, { projectId: projectId, entity: detail.record, kind: "skill", open: detail.open, isResolving: detail.isResolving, onOpenChange: (next) => {
|
|
113
|
+
action: createButton('Create a skill'), secondaryAction: _jsx(Button, { asChild: true, variant: "ghost", size: "sm", className: "gap-1.5", children: _jsx("a", { href: "https://opencode.ai/docs/skills/", target: "_blank", rel: "noopener noreferrer", children: tI18nComplete.raw('text7af023c43013') }) }) })), children: filtered.map((skill) => (_jsx(CatalogCard, { title: skill.name, description: skill.description, onClick: () => setSelectedPath(skill.path) }, skill.path))) }), _jsx(EntityDetailModal, { projectId: projectId, entity: detail.record, kind: "skill", removable: true, open: detail.open, isResolving: detail.isResolving, onOpenChange: (next) => {
|
|
114
114
|
if (!next)
|
|
115
115
|
setSelectedPath(null);
|
|
116
116
|
} }), _jsx(NewSkillModal, { projectId: projectId, open: formOpen, onOpenChange: setFormOpen })] }));
|
|
@@ -3,8 +3,6 @@ import type { ProjectTrigger } from '@kortix/sdk';
|
|
|
3
3
|
import { type ReactNode } from 'react';
|
|
4
4
|
/** "next Mon 09:00", "next 08:45" today, "On event" for webhooks and app events, "Paused" while paused. */
|
|
5
5
|
export declare function nextRunLabel(trigger: Pick<ProjectTrigger, 'type' | 'enabled' | 'next_fire_at' | 'next_cron_at' | 'auto_paused_at'>, t: UiTranslator, locale: string, now?: number): string;
|
|
6
|
-
/** "2d ago", "9h ago", "3w ago" in the viewer's locale, or "Never run". */
|
|
7
|
-
export declare function lastRunLabel(trigger: Pick<ProjectTrigger, 'last_fired_at' | 'event'>, t: UiTranslator, locale: string, now?: number): string;
|
|
8
6
|
export declare function TriggerList({ triggers, groups, eventNames, selectedSlug, runningSlug, onOpen, selection, projectPaused, }: {
|
|
9
7
|
triggers: ProjectTrigger[];
|
|
10
8
|
/** The trigger whose Run now is in flight: its row shows the session busy mark. */
|
|
@@ -9,6 +9,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
9
9
|
import { Checkbox } from '../../../primitives/checkbox.js';
|
|
10
10
|
import { SessionDotMatrix } from '../../../primitives/dot-matrix/session-dot-matrix.js';
|
|
11
11
|
import { useLocale, useTranslations } from '../../../i18n/index.js';
|
|
12
|
+
import { lastRunLabel } from '../../../internal/lib/trigger-clock.js';
|
|
12
13
|
import { cn } from '../../../internal/lib/utils.js';
|
|
13
14
|
import { Fragment } from 'react';
|
|
14
15
|
import { describeNextRun, describeWhen, triggerName, triggerSubtitle } from './schedule-copy.js';
|
|
@@ -35,24 +36,6 @@ export function nextRunLabel(trigger, t, locale, now = Date.now()) {
|
|
|
35
36
|
: date.toLocaleDateString(locale, { month: 'short', day: 'numeric' });
|
|
36
37
|
return t('text27a2c8b3a0d2', { value0: day ? `${day} ${time}` : time });
|
|
37
38
|
}
|
|
38
|
-
/** "2d ago", "9h ago", "3w ago" in the viewer's locale, or "Never run". */
|
|
39
|
-
export function lastRunLabel(trigger, t, locale, now = Date.now()) {
|
|
40
|
-
const iso = trigger.last_fired_at ?? trigger.event?.last_event_at ?? null;
|
|
41
|
-
const at = iso ? Date.parse(iso) : Number.NaN;
|
|
42
|
-
if (!Number.isFinite(at))
|
|
43
|
-
return t.raw('text3d40a69d3160');
|
|
44
|
-
const rtf = new Intl.RelativeTimeFormat(locale, { style: 'narrow', numeric: 'auto' });
|
|
45
|
-
const min = Math.floor(Math.max(0, now - at) / 60_000);
|
|
46
|
-
if (min < 1)
|
|
47
|
-
return rtf.format(0, 'second');
|
|
48
|
-
if (min < 60)
|
|
49
|
-
return rtf.format(-min, 'minute');
|
|
50
|
-
if (min < 24 * 60)
|
|
51
|
-
return rtf.format(-Math.floor(min / 60), 'hour');
|
|
52
|
-
if (min < 7 * 24 * 60)
|
|
53
|
-
return rtf.format(-Math.floor(min / (24 * 60)), 'day');
|
|
54
|
-
return rtf.format(-Math.floor(min / (7 * 24 * 60)), 'week');
|
|
55
|
-
}
|
|
56
39
|
export function TriggerList({ triggers, groups, eventNames, selectedSlug, runningSlug = null, onOpen, selection = null, projectPaused = false, }) {
|
|
57
40
|
const row = (trigger) => (_jsx(TriggerRow, { trigger: trigger, eventNames: eventNames, selected: !selection && trigger.slug === selectedSlug, running: trigger.slug === runningSlug, onOpen: () => onOpen(trigger), checked: selection ? selection.selected.has(trigger.slug) : null, projectPaused: projectPaused, onToggle: (range) => selection?.onToggle(trigger, range) }, trigger.slug));
|
|
58
41
|
return (
|
|
@@ -1,41 +1,10 @@
|
|
|
1
|
+
import { type ClockState } from '../../../internal/lib/trigger-clock.js';
|
|
1
2
|
import type { ProjectTrigger } from '@kortix/sdk';
|
|
2
|
-
export type ClockState = 'ok' | 'failed' | 'needs_connection' | 'pending' | 'paused' | 'never';
|
|
3
3
|
/**
|
|
4
4
|
* The health the clock shows: the badge state, with "live" split by whether
|
|
5
5
|
* it ever ran. `projectPaused` (the project-wide switch) pauses every clock.
|
|
6
6
|
*/
|
|
7
7
|
export declare function clockState(trigger: ProjectTrigger, projectPaused?: boolean, running?: boolean): ClockState;
|
|
8
|
-
/**
|
|
9
|
-
* ONE tone per state drives both the reflection and the dot, so they cannot
|
|
10
|
-
* disagree. Three colours, always full strength: green = live (running, ran,
|
|
11
|
-
* or not run yet), red = failed, yellow = paused or waiting on setup.
|
|
12
|
-
*/
|
|
13
|
-
export declare const CLOCK_TONE: {
|
|
14
|
-
readonly ok: {
|
|
15
|
-
readonly reflection: "text-kortix-green";
|
|
16
|
-
readonly dot: "bg-kortix-green";
|
|
17
|
-
};
|
|
18
|
-
readonly never: {
|
|
19
|
-
readonly reflection: "text-kortix-green";
|
|
20
|
-
readonly dot: "bg-kortix-green";
|
|
21
|
-
};
|
|
22
|
-
readonly failed: {
|
|
23
|
-
readonly reflection: "text-kortix-red";
|
|
24
|
-
readonly dot: "bg-kortix-red";
|
|
25
|
-
};
|
|
26
|
-
readonly needs_connection: {
|
|
27
|
-
readonly reflection: "text-kortix-yellow";
|
|
28
|
-
readonly dot: "bg-kortix-yellow";
|
|
29
|
-
};
|
|
30
|
-
readonly pending: {
|
|
31
|
-
readonly reflection: "text-kortix-yellow";
|
|
32
|
-
readonly dot: "bg-kortix-yellow";
|
|
33
|
-
};
|
|
34
|
-
readonly paused: {
|
|
35
|
-
readonly reflection: "text-kortix-yellow";
|
|
36
|
-
readonly dot: "bg-kortix-yellow";
|
|
37
|
-
};
|
|
38
|
-
};
|
|
39
8
|
export declare function TriggerTile({ trigger, projectPaused, running, surface, }: {
|
|
40
9
|
trigger: ProjectTrigger;
|
|
41
10
|
/** The project-wide pause switch is on. */
|
|
@@ -13,6 +13,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
13
13
|
*/
|
|
14
14
|
import { REFLECTION_MASK } from '../../../primitives/toast.js';
|
|
15
15
|
import { useTranslations } from '../../../i18n/index.js';
|
|
16
|
+
import { CLOCK_TONE } from '../../../internal/lib/trigger-clock.js';
|
|
16
17
|
import { cn } from '../../../internal/lib/utils.js';
|
|
17
18
|
import { ClockIcon } from '@phosphor-icons/react';
|
|
18
19
|
import { triggerBadgeState } from './schedule-copy.js';
|
|
@@ -34,22 +35,6 @@ export function clockState(trigger, projectPaused = false, running = false) {
|
|
|
34
35
|
return state;
|
|
35
36
|
return trigger.last_fired_at || trigger.event?.last_event_at ? 'ok' : 'never';
|
|
36
37
|
}
|
|
37
|
-
const GREEN = { reflection: 'text-kortix-green', dot: 'bg-kortix-green' };
|
|
38
|
-
const RED = { reflection: 'text-kortix-red', dot: 'bg-kortix-red' };
|
|
39
|
-
const YELLOW = { reflection: 'text-kortix-yellow', dot: 'bg-kortix-yellow' };
|
|
40
|
-
/**
|
|
41
|
-
* ONE tone per state drives both the reflection and the dot, so they cannot
|
|
42
|
-
* disagree. Three colours, always full strength: green = live (running, ran,
|
|
43
|
-
* or not run yet), red = failed, yellow = paused or waiting on setup.
|
|
44
|
-
*/
|
|
45
|
-
export const CLOCK_TONE = {
|
|
46
|
-
ok: GREEN,
|
|
47
|
-
never: GREEN,
|
|
48
|
-
failed: RED,
|
|
49
|
-
needs_connection: YELLOW,
|
|
50
|
-
pending: YELLOW,
|
|
51
|
-
paused: YELLOW,
|
|
52
|
-
};
|
|
53
38
|
const LABEL = {
|
|
54
39
|
ok: LABEL_KEY.live,
|
|
55
40
|
failed: LABEL_KEY.error,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type InfiniteData } from '@tanstack/react-query';
|
|
1
|
+
import { type InfiniteData, type QueryClient } from '@tanstack/react-query';
|
|
2
2
|
import { type ChangeRequest, type ChangeRequestDetailResponse, type ChangeRequestDiffResponse, type ChangeRequestMergePreview, type ChangeRequestMergeResponse, type ChangeRequestStatus, type VersionDiffPreview } from '../api/change-requests.js';
|
|
3
3
|
export declare const changeRequestKeys: {
|
|
4
4
|
all: readonly ["project-files", "change-requests"];
|
|
@@ -118,6 +118,19 @@ export declare function useVersionDiff(input: {
|
|
|
118
118
|
projectId?: string;
|
|
119
119
|
}): import("@tanstack/react-query").UseQueryResult<VersionDiffPreview, Error>;
|
|
120
120
|
export declare function useChangeRequestMergePreview(crId: string | null, enabled?: boolean): import("@tanstack/react-query").UseQueryResult<ChangeRequestMergePreview, Error>;
|
|
121
|
+
/**
|
|
122
|
+
* Marks every change-request read of one project stale: each list (the
|
|
123
|
+
* session's outcome cards, the sidebar brief), each detail, and the review
|
|
124
|
+
* summary behind the sidebar's "Needs you" count. Called at turn end, because
|
|
125
|
+
* an agent opens and closes change requests from its sandbox through the REST
|
|
126
|
+
* API and the browser is not told (KRTX-3009, KRTX-3010).
|
|
127
|
+
*/
|
|
128
|
+
export declare function invalidateChangeRequestReads(qc: QueryClient, projectId: string): void;
|
|
129
|
+
/**
|
|
130
|
+
* Invalidates every CR query for the active project — used after open / merge
|
|
131
|
+
* / close / reopen so all panels and detail views re-fetch.
|
|
132
|
+
*/
|
|
133
|
+
export declare function invalidateAfterChangeRequestWrite(qc: QueryClient, projectId: string): void;
|
|
121
134
|
export declare function useOpenChangeRequest(options?: {
|
|
122
135
|
projectId?: string;
|
|
123
136
|
}): import("@tanstack/react-query").UseMutationResult<ChangeRequest, Error, {
|
|
@@ -217,39 +217,58 @@ export function useChangeRequestMergePreview(crId, enabled = true) {
|
|
|
217
217
|
staleTime: 10_000,
|
|
218
218
|
});
|
|
219
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* Marks every change-request read of one project stale: each list (the
|
|
222
|
+
* session's outcome cards, the sidebar brief), each detail, and the review
|
|
223
|
+
* summary behind the sidebar's "Needs you" count. Called at turn end, because
|
|
224
|
+
* an agent opens and closes change requests from its sandbox through the REST
|
|
225
|
+
* API and the browser is not told (KRTX-3009, KRTX-3010).
|
|
226
|
+
*/
|
|
227
|
+
export function invalidateChangeRequestReads(qc, projectId) {
|
|
228
|
+
if (!projectId)
|
|
229
|
+
return;
|
|
230
|
+
qc.invalidateQueries({ queryKey: changeRequestKeys.project(projectId) });
|
|
231
|
+
qc.invalidateQueries({ queryKey: qk.project.reviewSummary(projectId) });
|
|
232
|
+
}
|
|
220
233
|
/**
|
|
221
234
|
* Invalidates every CR query for the active project — used after open / merge
|
|
222
235
|
* / close / reopen so all panels and detail views re-fetch.
|
|
223
236
|
*/
|
|
237
|
+
export function invalidateAfterChangeRequestWrite(qc, projectId) {
|
|
238
|
+
invalidateChangeRequestReads(qc, projectId);
|
|
239
|
+
// Branches list shows ahead/behind that may shift after a merge.
|
|
240
|
+
qc.invalidateQueries({ queryKey: branchKeys.list(projectId) });
|
|
241
|
+
// The merge commit lands on the default branch — commit list goes stale.
|
|
242
|
+
qc.invalidateQueries({ queryKey: commitKeys.project(projectId) });
|
|
243
|
+
// Whether this version still differs from its base changes the moment a CR
|
|
244
|
+
// merges — refresh the "Alternate version of main · N changes" banner
|
|
245
|
+
// (git-status, which is otherwise sticky and never re-fetches on its own),
|
|
246
|
+
// the live version-diff preview, and the cached session row (base_ref etc.).
|
|
247
|
+
qc.invalidateQueries({ queryKey: gitStatusKeys.all });
|
|
248
|
+
qc.invalidateQueries({ queryKey: versionDiffKeys.project(projectId) });
|
|
249
|
+
// Every individual git-connected session under this project — a CR
|
|
250
|
+
// landing on the base ref can change what `getProjectSession` returns
|
|
251
|
+
// (base_ref etc.) for any of them. sessionsScope is the shared prefix
|
|
252
|
+
// that reaches the sessions list AND every qk.project.session(id, sid)
|
|
253
|
+
// entry in one call; there is no "every session, not the list" prefix to
|
|
254
|
+
// narrow to, so this deliberately also refreshes the sessions list.
|
|
255
|
+
qc.invalidateQueries({ queryKey: qk.project.sessionsScope(projectId) });
|
|
256
|
+
// Landing a CR on the base ref is the one thing that happens INSIDE the app
|
|
257
|
+
// that can make an open session's compiled agent config stale. The
|
|
258
|
+
// freshness query is deliberately not polled — this is what tells it to
|
|
259
|
+
// look again, so the chip appears on merge rather than on next focus.
|
|
260
|
+
qc.invalidateQueries({ queryKey: ['session-config', projectId] });
|
|
261
|
+
// A merged CR can rewrite kortix.yaml: add or remove agents and change
|
|
262
|
+
// `default_agent`. Both live in the project detail entry, and the composer's
|
|
263
|
+
// agent list sits under it (`[...detail, 'agents']`), so New session kept
|
|
264
|
+
// offering removed agents until a reload (KRTX-3012).
|
|
265
|
+
qc.invalidateQueries({ queryKey: qk.project.detail(projectId) });
|
|
266
|
+
}
|
|
224
267
|
function useInvalidateAll(projectIdArg) {
|
|
225
268
|
const qc = useQueryClient();
|
|
226
269
|
const ctx = useProjectContext();
|
|
227
270
|
const projectId = projectIdArg ?? ctx?.projectId ?? '';
|
|
228
|
-
return () =>
|
|
229
|
-
qc.invalidateQueries({ queryKey: changeRequestKeys.project(projectId) });
|
|
230
|
-
// Branches list shows ahead/behind that may shift after a merge.
|
|
231
|
-
qc.invalidateQueries({ queryKey: branchKeys.list(projectId) });
|
|
232
|
-
// The merge commit lands on the default branch — commit list goes stale.
|
|
233
|
-
qc.invalidateQueries({ queryKey: commitKeys.project(projectId) });
|
|
234
|
-
// Whether this version still differs from its base changes the moment a CR
|
|
235
|
-
// merges — refresh the "Alternate version of main · N changes" banner
|
|
236
|
-
// (git-status, which is otherwise sticky and never re-fetches on its own),
|
|
237
|
-
// the live version-diff preview, and the cached session row (base_ref etc.).
|
|
238
|
-
qc.invalidateQueries({ queryKey: gitStatusKeys.all });
|
|
239
|
-
qc.invalidateQueries({ queryKey: versionDiffKeys.project(projectId) });
|
|
240
|
-
// Every individual git-connected session under this project — a CR
|
|
241
|
-
// landing on the base ref can change what `getProjectSession` returns
|
|
242
|
-
// (base_ref etc.) for any of them. sessionsScope is the shared prefix
|
|
243
|
-
// that reaches the sessions list AND every qk.project.session(id, sid)
|
|
244
|
-
// entry in one call; there is no "every session, not the list" prefix to
|
|
245
|
-
// narrow to, so this deliberately also refreshes the sessions list.
|
|
246
|
-
qc.invalidateQueries({ queryKey: qk.project.sessionsScope(projectId) });
|
|
247
|
-
// Landing a CR on the base ref is the one thing that happens INSIDE the app
|
|
248
|
-
// that can make an open session's compiled agent config stale. The
|
|
249
|
-
// freshness query is deliberately not polled — this is what tells it to
|
|
250
|
-
// look again, so the chip appears on merge rather than on next focus.
|
|
251
|
-
qc.invalidateQueries({ queryKey: ['session-config', projectId] });
|
|
252
|
-
};
|
|
271
|
+
return () => invalidateAfterChangeRequestWrite(qc, projectId);
|
|
253
272
|
}
|
|
254
273
|
export function useOpenChangeRequest(options) {
|
|
255
274
|
const ctx = useProjectContext();
|