@kortix/ui 0.14.4 → 0.14.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/README.md +8 -4
  2. package/dist/customize/sections/llm-provider/constants.d.ts +0 -1
  3. package/dist/customize/sections/llm-provider/constants.js +0 -2
  4. package/dist/customize/sections/llm-provider/models-tab-group.d.ts +1 -1
  5. package/dist/customize/sections/llm-provider/models-tab-group.js +7 -4
  6. package/dist/customize/sections/llm-provider/models-tab-row.d.ts +2 -18
  7. package/dist/customize/sections/llm-provider/models-tab-row.js +6 -30
  8. package/dist/customize/sections/llm-provider/models-tab.d.ts +0 -1
  9. package/dist/customize/sections/llm-provider/models-tab.js +1 -10
  10. package/dist/customize/sections/llm-provider/use-connected-providers.js +2 -2
  11. package/dist/customize/sections/llm-provider/utils.d.ts +1 -1
  12. package/dist/customize/sections/llm-provider/utils.js +7 -14
  13. package/dist/customize/sections/view/gateway/_shared.js +1 -2
  14. package/dist/customize/sections/view/gateway/gateway-routing.js +3 -2
  15. package/dist/customize/sections/view/sandbox-provider-coverage.d.ts +3 -18
  16. package/dist/customize/sections/view/sandbox-provider-coverage.js +7 -27
  17. package/dist/customize/settings/tabs/sandbox-tab.d.ts +2 -27
  18. package/dist/customize/settings/tabs/sandbox-tab.js +13 -112
  19. package/dist/customize/shared/entity/entity-modal.d.ts +5 -1
  20. package/dist/customize/shared/entity/entity-modal.js +7 -4
  21. package/dist/customize/skills/remove-skill-button.d.ts +16 -0
  22. package/dist/customize/skills/remove-skill-button.js +42 -0
  23. package/dist/customize/skills/skills-page.js +1 -1
  24. package/dist/customize/triggers/schedule/trigger-list.d.ts +0 -2
  25. package/dist/customize/triggers/schedule/trigger-list.js +1 -18
  26. package/dist/customize/triggers/schedule/trigger-tile.d.ts +1 -32
  27. package/dist/customize/triggers/schedule/trigger-tile.js +1 -16
  28. package/dist/files/project/hooks/use-change-requests.d.ts +14 -1
  29. package/dist/files/project/hooks/use-change-requests.js +44 -25
  30. package/dist/files/renderers/docx/docx-viewer.js +4 -1
  31. package/dist/files/renderers/pdf/pdf-renderer.d.ts +3 -1
  32. package/dist/files/renderers/pdf/pdf-renderer.js +2 -2
  33. package/dist/files/renderers/pdf/pdf-viewer.d.ts +3 -0
  34. package/dist/files/renderers/pdf/pdf-viewer.js +15 -6
  35. package/dist/files/viewer/file-content-renderer.d.ts +4 -1
  36. package/dist/files/viewer/file-content-renderer.js +2 -2
  37. package/dist/i18n/en.d.ts +26 -35
  38. package/dist/i18n/en.js +35 -44
  39. package/dist/index.d.ts +1 -1
  40. package/dist/internal/hooks/use-notification-center.js +21 -15
  41. package/dist/internal/hooks/use-review-session-summary.d.ts +6 -0
  42. package/dist/internal/hooks/use-review-session-summary.js +11 -0
  43. package/dist/internal/lib/browser-error-noise.d.ts +1 -1
  44. package/dist/internal/lib/browser-error-noise.js +1 -1
  45. package/dist/internal/lib/browser-noise/rules/rejections.d.ts +10 -0
  46. package/dist/internal/lib/browser-noise/rules/rejections.js +27 -1
  47. package/dist/internal/lib/provider-branding.js +14 -3
  48. package/dist/internal/lib/trigger-clock.d.ts +42 -0
  49. package/dist/internal/lib/trigger-clock.js +34 -0
  50. package/dist/internal/lib/web-notifications.d.ts +6 -0
  51. package/dist/internal/lib/web-notifications.js +57 -7
  52. package/dist/internal/notifications/notification-bell.d.ts +13 -0
  53. package/dist/internal/notifications/notification-bell.js +1 -1
  54. package/dist/internal/reminders/reminder-filter.d.ts +12 -0
  55. package/dist/internal/reminders/reminder-filter.js +34 -0
  56. package/dist/internal/reminders/reminder-format.d.ts +6 -0
  57. package/dist/internal/reminders/reminder-format.js +20 -2
  58. package/dist/internal/stores/kortix-computer-store.d.ts +1 -1
  59. package/dist/internal/stores/session-hover-store.d.ts +20 -5
  60. package/dist/internal/stores/session-hover-store.js +31 -13
  61. package/dist/internal/stores/session-panel-memory-store.js +2 -0
  62. package/dist/primitives/count-badge.d.ts +3 -1
  63. package/dist/primitives/count-badge.js +3 -3
  64. package/dist/primitives/glyph-paths-source.js +1 -1
  65. package/dist/primitives/glyph-paths.generated.js +1 -1
  66. package/dist/providers/ui-provider.d.ts +57 -0
  67. package/dist/providers/ui-provider.js +4 -0
  68. package/dist/session/composer/ComposerCard.d.ts +2 -0
  69. package/dist/session/composer/ComposerCard.js +5 -1
  70. package/dist/session/composer/agent-selector.js +12 -5
  71. package/dist/session/composer/composer-toolbar.js +4 -2
  72. package/dist/session/composer/composer-underbar.js +3 -1
  73. package/dist/session/composer/composer.js +34 -5
  74. package/dist/session/composer/menus/slash-actions.d.ts +22 -2
  75. package/dist/session/composer/menus/slash-actions.js +20 -0
  76. package/dist/session/composer/menus/slash-items.js +19 -3
  77. package/dist/session/header/reminder-parts.d.ts +285 -0
  78. package/dist/session/header/reminder-parts.js +115 -0
  79. package/dist/session/header/session-reminders-indicator.d.ts +13 -0
  80. package/dist/session/header/session-reminders-indicator.js +142 -35
  81. package/dist/session/models/model-availability.d.ts +23 -0
  82. package/dist/session/models/model-availability.js +20 -0
  83. package/dist/session/models/model-grouping.js +1 -4
  84. package/dist/session/models/model-selector.js +23 -14
  85. package/dist/session/models/use-model-connection-gate.js +4 -1
  86. package/dist/session/outcomes/session-outcomes-provider.d.ts +3 -1
  87. package/dist/session/outcomes/session-outcomes-provider.js +31 -3
  88. package/dist/session/overrides/session-overrides-composer.d.ts +1 -1
  89. package/dist/session/overrides/session-overrides-composer.js +5 -1
  90. package/dist/session/panel/mobile-tool-drawer.js +4 -2
  91. package/dist/session/panel/open-session-quick-view.js +2 -1
  92. package/dist/session/panel/panel-fallback.js +8 -4
  93. package/dist/session/panel/session-panel-provider.d.ts +1 -0
  94. package/dist/session/panel/session-panel-provider.js +13 -1
  95. package/dist/session/panel/session-reminders-panel.d.ts +4 -0
  96. package/dist/session/panel/session-reminders-panel.js +109 -0
  97. package/dist/session/panel/shared/use-panel-handoffs.d.ts +2 -2
  98. package/dist/session/panel/shared/use-panel-handoffs.js +5 -1
  99. package/dist/session/print/session-print-header.d.ts +0 -10
  100. package/dist/session/print/session-print-header.js +16 -1
  101. package/dist/session/status/session-surface.js +1 -1
  102. package/dist/session/thread/optimistic-turn.d.ts +5 -1
  103. package/dist/session/thread/optimistic-turn.js +8 -6
  104. package/dist/session/thread/session-chat.d.ts +6 -1
  105. package/dist/session/thread/session-chat.js +22 -3
  106. package/dist/session/thread/session-thread.d.ts +10 -1
  107. package/dist/session/thread/session-thread.js +2 -2
  108. package/dist/session/turn/burst-step-body.js +4 -0
  109. package/dist/styles.css +1 -1
  110. package/dist/workspace/sidebar/project-session-list.js +52 -55
  111. package/dist/workspace/sidebar/session-brief-hover-card.d.ts +35 -18
  112. package/dist/workspace/sidebar/session-brief-hover-card.js +114 -37
  113. package/dist/workspace/sidebar/session-status-mark.d.ts +15 -1
  114. package/dist/workspace/sidebar/session-status-mark.js +63 -17
  115. package/package.json +7 -3
  116. package/skills/kortix-ui/SKILL.md +1 -1
  117. package/dist/customize/sections/view/sandbox-provider-result.d.ts +0 -47
  118. package/dist/customize/sections/view/sandbox-provider-result.js +0 -79
@@ -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, getProject, listProjectSnapshots, updateProjectSandboxProvider, } from '@kortix/sdk';
26
- import { contract, invalidateProject, qk } from '@kortix/sdk/react';
27
- import { ArrowClockwiseIcon, CheckCircleIcon, CircleDashedIcon, CpuIcon, CubeIcon, FileCodeIcon, HardDrivesIcon, MemoryIcon, PackageIcon, PencilSimpleIcon, PlusIcon, PushPinIcon, ShippingContainerIcon, ShuffleIcon, SquaresFourIcon, TrashIcon, XCircleIcon, } from '@phosphor-icons/react';
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, SandboxProviderLabel, availableProviderCoverage, describeProviderMode, latestObservedAt, } from '../../sections/view/sandbox-provider-coverage.js';
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
- /** One batched probe for the two leaves this tab gates on. */
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, providerMode, selectedProvider, formatObservedAt, runtimeLabel, checkedLabel, badgeCopy, }) {
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, selected: providerMode === 'pinned' && item.provider === selectedProvider, copy: badgeCopy }, item.provider))) }), observedAt ? (_jsx("span", { className: "text-muted-foreground ml-auto shrink-0 text-xs tabular-nums", children: checkedLabel(formatObservedAt(observedAt)) })) : null] }));
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, providerMode, selectedProvider, }) {
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
- const routing = describeRouting(providerMode, selectedProvider, tI18nComplete, {
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, sandboxProviderSlot, manifestVersion = null, templatesError = null, isEmpty = true, emptyAction, templatesSlot, copy: copyOverride, }) {
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 }), sandboxProviderSlot, 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)] }))] }) }));
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, sandboxProviderSlot: projectQuery.data ? (_jsx(SandboxProviderRow, { project: projectQuery.data, canManage: canEditProvider })) : 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, providerMode: providerMode, selectedProvider: selectedProvider }, t.template_id ?? `tpl-${t.slug}`))) })) }), _jsx(SandboxTemplateForm, { projectId: projectId, open: formOpen, onOpenChange: setFormOpen, template: editingTemplate })] }));
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 ? _jsx("div", { className: "shrink-0 pt-4", children: editButton }) : null] }));
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();