@kortix/ui 0.14.4 → 0.14.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/README.md +8 -4
  2. package/dist/access/access/access-dialog.js +8 -5
  3. package/dist/access/access/copy-row.js +2 -2
  4. package/dist/access/sharing-picker.js +2 -1
  5. package/dist/browser/app-in-browser.js +5 -3
  6. package/dist/browser/browser-panel.js +6 -6
  7. package/dist/browser/sandbox-browser-chrome.d.ts +4 -1
  8. package/dist/browser/sandbox-browser-chrome.js +4 -3
  9. package/dist/customize/agents/agent-access.d.ts +20 -0
  10. package/dist/customize/agents/agent-access.js +17 -0
  11. package/dist/customize/agents/agent-grant-pages.js +5 -6
  12. package/dist/customize/agents/agent-page.js +2 -1
  13. package/dist/customize/agents/agents-page.js +18 -1
  14. package/dist/customize/connectors/add-mcp/add-mcp-server.js +4 -2
  15. package/dist/customize/connectors/connector-routes.d.ts +3 -1
  16. package/dist/customize/connectors/connector-routes.js +18 -0
  17. package/dist/customize/connectors/connectors-page.js +23 -4
  18. package/dist/customize/connectors/detail/connector-advanced-tools.js +4 -6
  19. package/dist/customize/providers/provider-accordion.js +2 -1
  20. package/dist/customize/providers/provider-connect.js +2 -1
  21. package/dist/customize/sections/component/telegram-channel-row.js +1 -1
  22. package/dist/customize/sections/connector-connection-modal.js +11 -9
  23. package/dist/customize/sections/connectors-view.js +11 -13
  24. package/dist/customize/sections/llm-provider/constants.d.ts +0 -1
  25. package/dist/customize/sections/llm-provider/constants.js +0 -2
  26. package/dist/customize/sections/llm-provider/models-tab-group.d.ts +1 -1
  27. package/dist/customize/sections/llm-provider/models-tab-group.js +7 -4
  28. package/dist/customize/sections/llm-provider/models-tab-row.d.ts +2 -18
  29. package/dist/customize/sections/llm-provider/models-tab-row.js +6 -30
  30. package/dist/customize/sections/llm-provider/models-tab.d.ts +0 -1
  31. package/dist/customize/sections/llm-provider/models-tab.js +1 -10
  32. package/dist/customize/sections/llm-provider/use-connected-providers.js +2 -2
  33. package/dist/customize/sections/llm-provider/utils.d.ts +1 -1
  34. package/dist/customize/sections/llm-provider/utils.js +7 -14
  35. package/dist/customize/sections/view/gateway/_shared.js +1 -2
  36. package/dist/customize/sections/view/gateway/gateway-routing.js +18 -20
  37. package/dist/customize/sections/view/sandbox-provider-coverage.d.ts +3 -18
  38. package/dist/customize/sections/view/sandbox-provider-coverage.js +7 -27
  39. package/dist/customize/settings/rail-copy-context.js +5 -3
  40. package/dist/customize/settings/tabs/sandbox-tab.d.ts +2 -27
  41. package/dist/customize/settings/tabs/sandbox-tab.js +13 -112
  42. package/dist/customize/settings/tabs/sandbox-template-form.js +9 -12
  43. package/dist/customize/shared/capability-tabs.js +2 -1
  44. package/dist/customize/shared/entity/entity-modal.d.ts +5 -1
  45. package/dist/customize/shared/entity/entity-modal.js +7 -4
  46. package/dist/customize/skills/remove-skill-button.d.ts +16 -0
  47. package/dist/customize/skills/remove-skill-button.js +42 -0
  48. package/dist/customize/skills/skills-page.js +4 -6
  49. package/dist/customize/triggers/schedule/event-trigger-copy.js +3 -2
  50. package/dist/customize/triggers/schedule/schedule-builder.js +7 -4
  51. package/dist/customize/triggers/schedule/trigger-composer.js +5 -2
  52. package/dist/customize/triggers/schedule/trigger-list.d.ts +0 -2
  53. package/dist/customize/triggers/schedule/trigger-list.js +1 -18
  54. package/dist/customize/triggers/schedule/trigger-tile.d.ts +1 -32
  55. package/dist/customize/triggers/schedule/trigger-tile.js +1 -16
  56. package/dist/customize/triggers/schedule/when-event.js +1 -1
  57. package/dist/diff/diff-view.js +9 -10
  58. package/dist/files/editor/code-editor.js +2 -3
  59. package/dist/files/project/components/change-request-detail-dialog.d.ts +3 -1
  60. package/dist/files/project/components/change-request-detail-dialog.js +3 -5
  61. package/dist/files/project/components/change-request-status-badge.d.ts +12 -0
  62. package/dist/files/project/components/change-request-status-badge.js +27 -0
  63. package/dist/files/project/components/change-requests-panel.js +1 -1
  64. package/dist/files/project/components/drive-explorer.js +2 -1
  65. package/dist/files/project/components/drive-grid-view.js +11 -7
  66. package/dist/files/project/components/drive-list-view.js +4 -4
  67. package/dist/files/project/components/file-breadcrumbs.js +1 -1
  68. package/dist/files/project/components/file-explorer-page.js +1 -1
  69. package/dist/files/project/components/file-history-popover.js +1 -6
  70. package/dist/files/project/components/file-search.js +6 -1
  71. package/dist/files/project/components/open-change-request-dialog.js +2 -1
  72. package/dist/files/project/components/version-selector-logic.d.ts +11 -0
  73. package/dist/files/project/components/version-selector-logic.js +26 -0
  74. package/dist/files/project/components/version-selector.js +11 -20
  75. package/dist/files/project/hooks/use-change-requests.d.ts +14 -1
  76. package/dist/files/project/hooks/use-change-requests.js +44 -25
  77. package/dist/files/project/hooks/use-server-health.js +17 -15
  78. package/dist/files/renderers/docx/docx-viewer.js +4 -6
  79. package/dist/files/renderers/image-renderer.js +6 -24
  80. package/dist/files/renderers/pdf/pdf-renderer.d.ts +3 -1
  81. package/dist/files/renderers/pdf/pdf-renderer.js +2 -2
  82. package/dist/files/renderers/pdf/pdf-viewer.d.ts +3 -0
  83. package/dist/files/renderers/pdf/pdf-viewer.js +16 -7
  84. package/dist/files/renderers/presentation/FullScreenPresentationViewer.js +2 -11
  85. package/dist/files/renderers/show-content-renderer.js +1 -1
  86. package/dist/files/renderers/sqlite-renderer.js +8 -3
  87. package/dist/files/renderers/xlsx/xlsx-viewer.js +15 -16
  88. package/dist/files/tree/components/file-tree-item.js +1 -1
  89. package/dist/files/viewer/file-content-renderer.d.ts +4 -1
  90. package/dist/files/viewer/file-content-renderer.js +2 -2
  91. package/dist/files/viewer/file-preview-modal.js +1 -1
  92. package/dist/files/viewer/use-file-content-state.js +1 -1
  93. package/dist/i18n/en.d.ts +36 -35
  94. package/dist/i18n/en.js +45 -44
  95. package/dist/index.d.ts +1 -1
  96. package/dist/internal/desktop/desktop-back-button.js +5 -3
  97. package/dist/internal/hooks/use-auto-scroll.js +5 -3
  98. package/dist/internal/hooks/use-notification-center.js +21 -15
  99. package/dist/internal/hooks/use-review-session-summary.d.ts +6 -0
  100. package/dist/internal/hooks/use-review-session-summary.js +11 -0
  101. package/dist/internal/lib/browser-error-noise.d.ts +1 -1
  102. package/dist/internal/lib/browser-error-noise.js +1 -1
  103. package/dist/internal/lib/browser-noise/rules/rejections.d.ts +10 -0
  104. package/dist/internal/lib/browser-noise/rules/rejections.js +27 -1
  105. package/dist/internal/lib/provider-branding.js +14 -3
  106. package/dist/internal/lib/trigger-clock.d.ts +42 -0
  107. package/dist/internal/lib/trigger-clock.js +34 -0
  108. package/dist/internal/lib/web-notifications.d.ts +6 -0
  109. package/dist/internal/lib/web-notifications.js +57 -7
  110. package/dist/internal/notifications/notification-bell.d.ts +13 -0
  111. package/dist/internal/notifications/notification-bell.js +1 -1
  112. package/dist/internal/notifications/notification-rows.js +2 -1
  113. package/dist/internal/outcomes/outcome-types.d.ts +5 -0
  114. package/dist/internal/reminders/reminder-filter.d.ts +12 -0
  115. package/dist/internal/reminders/reminder-filter.js +34 -0
  116. package/dist/internal/reminders/reminder-format.d.ts +6 -0
  117. package/dist/internal/reminders/reminder-format.js +20 -2
  118. package/dist/internal/stores/kortix-computer-store.d.ts +1 -1
  119. package/dist/internal/stores/session-hover-store.d.ts +20 -5
  120. package/dist/internal/stores/session-hover-store.js +31 -13
  121. package/dist/internal/stores/session-panel-memory-store.js +2 -0
  122. package/dist/markdown/code/inline-code.js +8 -0
  123. package/dist/markdown/code/output-paths-context.d.ts +18 -0
  124. package/dist/markdown/code/output-paths-context.js +35 -0
  125. package/dist/markdown/genui/components/charts.js +6 -5
  126. package/dist/primitives/count-badge.d.ts +3 -1
  127. package/dist/primitives/count-badge.js +3 -3
  128. package/dist/primitives/glyph-paths-source.js +1 -1
  129. package/dist/primitives/glyph-paths.generated.js +1 -1
  130. package/dist/primitives/map.js +2 -1
  131. package/dist/primitives/preview-image.js +4 -2
  132. package/dist/primitives/textarea.js +1 -1
  133. package/dist/primitives/typed-title.js +12 -14
  134. package/dist/primitives/typing-animation.js +4 -2
  135. package/dist/providers/ui-provider.d.ts +57 -0
  136. package/dist/providers/ui-provider.js +4 -0
  137. package/dist/session/composer/ComposerCard.d.ts +2 -0
  138. package/dist/session/composer/ComposerCard.js +5 -1
  139. package/dist/session/composer/agent-selector.js +17 -5
  140. package/dist/session/composer/composer-chat-input.js +5 -3
  141. package/dist/session/composer/composer-logic.js +8 -7
  142. package/dist/session/composer/composer-toolbar.js +4 -2
  143. package/dist/session/composer/composer-underbar.js +3 -1
  144. package/dist/session/composer/composer.js +34 -5
  145. package/dist/session/composer/draft/composer-draft.js +4 -4
  146. package/dist/session/composer/menus/slash-actions.d.ts +22 -2
  147. package/dist/session/composer/menus/slash-actions.js +20 -0
  148. package/dist/session/composer/menus/slash-items.js +19 -3
  149. package/dist/session/composer/pasted-text.js +3 -2
  150. package/dist/session/composer/stores/queued-draft-store.js +3 -2
  151. package/dist/session/header/reminder-parts.d.ts +294 -0
  152. package/dist/session/header/reminder-parts.js +120 -0
  153. package/dist/session/header/session-reminders-indicator.d.ts +13 -0
  154. package/dist/session/header/session-reminders-indicator.js +142 -35
  155. package/dist/session/media/sandbox-url-detector.js +0 -7
  156. package/dist/session/models/model-availability.d.ts +23 -0
  157. package/dist/session/models/model-availability.js +20 -0
  158. package/dist/session/models/model-grouping.js +1 -4
  159. package/dist/session/models/model-selector.js +23 -14
  160. package/dist/session/models/use-model-connection-gate.js +4 -1
  161. package/dist/session/outcomes/change-request-outcomes.js +24 -3
  162. package/dist/session/outcomes/session-outcomes-provider.d.ts +3 -1
  163. package/dist/session/outcomes/session-outcomes-provider.js +31 -3
  164. package/dist/session/overrides/provider-secret-pool-editor.js +2 -1
  165. package/dist/session/overrides/session-overrides-composer.d.ts +1 -1
  166. package/dist/session/overrides/session-overrides-composer.js +5 -1
  167. package/dist/session/panel/advanced/advanced-panel.js +3 -7
  168. package/dist/session/panel/easy/app-preview.js +12 -2
  169. package/dist/session/panel/easy/easy-panel-logic.d.ts +1 -1
  170. package/dist/session/panel/easy/easy-panel-logic.js +1 -1
  171. package/dist/session/panel/easy/outputs-card.js +2 -1
  172. package/dist/session/panel/easy/panel-card.js +5 -3
  173. package/dist/session/panel/easy/preview-load.d.ts +14 -0
  174. package/dist/session/panel/easy/preview-load.js +15 -0
  175. package/dist/session/panel/easy/step-detail-body.js +2 -7
  176. package/dist/session/panel/mobile-tool-drawer.js +4 -2
  177. package/dist/session/panel/open-session-quick-view.js +2 -1
  178. package/dist/session/panel/panel-fallback.js +8 -4
  179. package/dist/session/panel/session-panel-provider.d.ts +1 -0
  180. package/dist/session/panel/session-panel-provider.js +19 -3
  181. package/dist/session/panel/session-reminders-panel.d.ts +4 -0
  182. package/dist/session/panel/session-reminders-panel.js +109 -0
  183. package/dist/session/panel/shared/use-panel-derivations.js +6 -4
  184. package/dist/session/panel/shared/use-panel-handoffs.d.ts +2 -2
  185. package/dist/session/panel/shared/use-panel-handoffs.js +5 -1
  186. package/dist/session/panel/use-session-panel-layout.js +7 -5
  187. package/dist/session/print/session-print-header.d.ts +0 -10
  188. package/dist/session/print/session-print-header.js +25 -9
  189. package/dist/session/status/session-surface.js +1 -1
  190. package/dist/session/thread/optimistic-turn.d.ts +5 -1
  191. package/dist/session/thread/optimistic-turn.js +8 -6
  192. package/dist/session/thread/session-chat.d.ts +6 -1
  193. package/dist/session/thread/session-chat.js +34 -12
  194. package/dist/session/thread/session-history.js +3 -1
  195. package/dist/session/thread/session-thread.d.ts +10 -1
  196. package/dist/session/thread/session-thread.js +2 -2
  197. package/dist/session/thread/transcript.js +13 -7
  198. package/dist/session/tool/shared/file-list.js +19 -2
  199. package/dist/session/tool/shared/infrastructure-contexts.d.ts +7 -0
  200. package/dist/session/tool/shared/infrastructure-contexts.js +7 -0
  201. package/dist/session/tool/shared/infrastructure-output.js +9 -5
  202. package/dist/session/tool/shared/infrastructure-row-header.js +9 -1
  203. package/dist/session/tool/shared/infrastructure-rows.js +7 -5
  204. package/dist/session/tool/tools/bash-tool.js +10 -6
  205. package/dist/session/tool/tools/web-search-tool.js +25 -3
  206. package/dist/session/turn/burst-step-body.js +4 -0
  207. package/dist/session/turn/recovered-failures.d.ts +8 -0
  208. package/dist/session/turn/recovered-failures.js +32 -0
  209. package/dist/session/turn/streaming-cadence.js +5 -8
  210. package/dist/styles.css +1 -1
  211. package/dist/terminal/session-terminal-panel.js +3 -5
  212. package/dist/theme/theme.js +2 -1
  213. package/dist/workspace/projects/modal/github-import-pickers.js +7 -6
  214. package/dist/workspace/review/review-center.js +13 -10
  215. package/dist/workspace/session-workspace.js +5 -9
  216. package/dist/workspace/sessions/deleted-session-row.js +5 -2
  217. package/dist/workspace/sidebar/footer/project-computer-nav.js +6 -4
  218. package/dist/workspace/sidebar/footer/project-mcp-nav.js +6 -2
  219. package/dist/workspace/sidebar/modal/rename-session-modal.js +8 -3
  220. package/dist/workspace/sidebar/project-session-list.js +52 -55
  221. package/dist/workspace/sidebar/session-brief-hover-card.d.ts +35 -18
  222. package/dist/workspace/sidebar/session-brief-hover-card.js +114 -37
  223. package/dist/workspace/sidebar/session-status-mark.d.ts +15 -1
  224. package/dist/workspace/sidebar/session-status-mark.js +63 -17
  225. package/package.json +7 -3
  226. package/skills/kortix-ui/SKILL.md +1 -1
  227. package/dist/customize/sections/view/sandbox-provider-result.d.ts +0 -47
  228. package/dist/customize/sections/view/sandbox-provider-result.js +0 -79
@@ -1,8 +1,6 @@
1
1
  import { type ReactNode } from 'react';
2
- import type { UiTranslator } from '../../../i18n/index.js';
3
2
  import { type SandboxTemplate } from '@kortix/sdk';
4
3
  import { type Icon as PhosphorIcon } from '@phosphor-icons/react';
5
- import { type SandboxProvider, type SandboxProviderMode } from '../../sections/view/sandbox-provider-coverage.js';
6
4
  type TemplateTone = 'ok' | 'busy' | 'fail' | 'idle';
7
5
  /** Exported for test only — `TemplateCard` itself owns mutations and cannot
8
6
  * render under `renderToStaticMarkup`, so the presentation decisions are
@@ -36,27 +34,13 @@ export declare function describeBase(template: SandboxTemplate, labels?: {
36
34
  icon: PhosphorIcon;
37
35
  mono: boolean;
38
36
  };
39
- /** Where sessions built from this template actually run. Static configuration,
40
- * not live status — which is why it belongs in the spec grid next to CPU and
41
- * memory, and NOT in the runtime footer where a bare `Automatic` badge used
42
- * to float with no label attached to it. */
43
- export declare function describeRouting(providerMode: SandboxProviderMode, selectedProvider: SandboxProvider | null, tI18nComplete: UiTranslator, labels?: {
44
- automatic?: string;
45
- pinned?: string;
46
- pinnedWithProvider?: (provider: string) => string;
47
- }): {
48
- label: string;
49
- icon: PhosphorIcon;
50
- };
51
37
  /** Locale-aware age for the runtime footer's "Checked …" stamp. */
52
38
  export declare function TemplateStateLine({ tone, label }: {
53
39
  tone: TemplateTone;
54
40
  label: string;
55
41
  }): import("react").JSX.Element;
56
- export declare function TemplateRuntimeFooter({ coverage, providerMode, selectedProvider, formatObservedAt, runtimeLabel, checkedLabel, badgeCopy, }: {
42
+ export declare function TemplateRuntimeFooter({ coverage, formatObservedAt, runtimeLabel, checkedLabel, badgeCopy, }: {
57
43
  coverage: SandboxTemplate['provider_coverage'] | null | undefined;
58
- providerMode: SandboxProviderMode;
59
- selectedProvider: SandboxProvider | null;
60
44
  formatObservedAt: (observedAt: string) => string;
61
45
  runtimeLabel: string;
62
46
  checkedLabel: (value: string) => string;
@@ -71,15 +55,6 @@ export interface SandboxTabViewProps {
71
55
  * by the container from the same `canManage` gate as everything else in
72
56
  * this tab. `undefined` (not rendered) for a non-manager. */
73
57
  headerAction?: ReactNode;
74
- /** `SandboxProviderRow` — the per-project provider pin, moved here from
75
- * `general-tab.tsx`. A slot for the same reason `templatesSlot` is one: it
76
- * owns a `useMutation`/`useQueryClient` and cannot render under
77
- * `renderToStaticMarkup` with no `QueryClientProvider`.
78
- *
79
- * Rendered ABOVE the loading/error branch on purpose. It is fed by the
80
- * PROJECT query, not the snapshots query, so a slow or failed template read
81
- * must not hide a control that has nothing to do with it. */
82
- sandboxProviderSlot?: ReactNode;
83
58
  /** `1 | 2 | null` — drives whether the manifest hint below reads
84
59
  * `kortix.toml` or `kortix.yaml`. `null`/anything else falls back to
85
60
  * `kortix.toml`, matching `sandbox-view.tsx`'s own ternary default. */
@@ -110,7 +85,7 @@ export interface SandboxTabViewProps {
110
85
  * read. Kept separate from `SandboxTab` so this renders under
111
86
  * `renderToStaticMarkup` with no `QueryClientProvider` — see
112
87
  * `GeneralTabView` for the same split. */
113
- export declare function SandboxTabView({ isLoading, isError, errorMessage, onRetry, headerAction, sandboxProviderSlot, manifestVersion, templatesError, isEmpty, emptyAction, templatesSlot, copy: copyOverride, }: SandboxTabViewProps): import("react").JSX.Element;
88
+ export declare function SandboxTabView({ isLoading, isError, errorMessage, onRetry, headerAction, manifestVersion, templatesError, isEmpty, emptyAction, templatesSlot, copy: copyOverride, }: SandboxTabViewProps): import("react").JSX.Element;
114
89
  /** Container: owns every hook (project + snapshots queries, the template
115
90
  * form's open/edit state) and renders `SandboxTabView` with real data +
116
91
  * the templates slot, plus `SandboxTemplateForm` as a sibling (it owns its
@@ -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
  }
@@ -11,7 +11,7 @@ import { useTranslations } from '../../../i18n/index.js';
11
11
  */
12
12
  import { ShippingContainerIcon as Container, FileCodeIcon as FileCode, PackageIcon as Package, } from '@phosphor-icons/react';
13
13
  import { useMutation, useQueryClient } from '@tanstack/react-query';
14
- import { useEffect, useMemo, useState } from 'react';
14
+ import { useMemo, useState } from 'react';
15
15
  import { Button } from '../../../primitives/button.js';
16
16
  import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '../../../primitives/dialog.js';
17
17
  import { Input } from '../../../primitives/input.js';
@@ -39,7 +39,7 @@ export function SandboxTemplateForm({ projectId, open, onOpenChange, template, }
39
39
  const isEdit = !!template;
40
40
  const queryClient = useQueryClient();
41
41
  const [name, setName] = useState('');
42
- const [slug, setSlug] = useState('');
42
+ const [typedSlug, setSlug] = useState('');
43
43
  const [slugManuallyEdited, setSlugManuallyEdited] = useState(false);
44
44
  const [mode, setMode] = useState('image');
45
45
  const [image, setImage] = useState('');
@@ -49,10 +49,10 @@ export function SandboxTemplateForm({ projectId, open, onOpenChange, template, }
49
49
  const [memoryGb, setMemoryGb] = useState('4');
50
50
  const [diskGb, setDiskGb] = useState('20');
51
51
  // Reset / hydrate when opening
52
- useEffect(() => {
53
- if (!open)
54
- return;
55
- if (template) {
52
+ const [hydratedFor, setHydratedFor] = useState(null);
53
+ if (!hydratedFor || hydratedFor[0] !== open || hydratedFor[1] !== template) {
54
+ setHydratedFor([open, template]);
55
+ if (open && template) {
56
56
  setName(template.name);
57
57
  setSlug(template.slug);
58
58
  setSlugManuallyEdited(true);
@@ -71,7 +71,7 @@ export function SandboxTemplateForm({ projectId, open, onOpenChange, template, }
71
71
  setMemoryGb(String(template.memory_gb));
72
72
  setDiskGb(String(template.disk_gb));
73
73
  }
74
- else {
74
+ else if (open) {
75
75
  setName('');
76
76
  setSlug('');
77
77
  setSlugManuallyEdited(false);
@@ -83,12 +83,9 @@ export function SandboxTemplateForm({ projectId, open, onOpenChange, template, }
83
83
  setMemoryGb('6');
84
84
  setDiskGb('20');
85
85
  }
86
- }, [open, template]);
86
+ }
87
87
  // Auto-slug from name when the user hasn't typed a slug manually yet.
88
- useEffect(() => {
89
- if (!slugManuallyEdited)
90
- setSlug(slugify(name));
91
- }, [name, slugManuallyEdited]);
88
+ const slug = slugManuallyEdited ? typedSlug : slugify(name);
92
89
  const slugError = useMemo(() => {
93
90
  if (!slug)
94
91
  return null;
@@ -126,7 +126,8 @@ export function CapabilityTabs({ projectId }) {
126
126
  // once the project turns its flag on. `capabilityAccess()` applies the flags
127
127
  // and the leaves; before it settles the bar is invisible and both are left out.
128
128
  const access = useCapabilityAccess(projectId);
129
- const tabs = allTabs.filter((tab) => access.settled ? access.tabs.includes(tab.key) : !FLAG_GATED_TABS.includes(tab.key));
129
+ const allowedTabs = new Set(access.tabs);
130
+ const tabs = allTabs.filter((tab) => access.settled ? allowedTabs.has(tab.key) : !FLAG_GATED_TABS.includes(tab.key));
130
131
  const leading = tabs.filter((tab) => !TRAILING_TABS.includes(tab.key));
131
132
  const primary = leading.filter((tab) => PRIMARY_TABS.includes(tab.key));
132
133
  const library = leading.filter((tab) => !PRIMARY_TABS.includes(tab.key));
@@ -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
+ }
@@ -2,7 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { NewEntityMenu } from '../shared/new-entity-menu.js';
4
4
  import { useTranslations as useI18nTranslations } from '../../i18n/index.js';
5
- import { useEffect, useMemo, useRef, useState } from 'react';
5
+ import { useMemo, useRef, useState } from 'react';
6
6
  import { Button } from '../../primitives/button.js';
7
7
  import { InputGroupSearch, InputGroupSearchClear, InputGroupSearchIcon, InputGroupSearchInput, } from '../../primitives/input-group.js';
8
8
  import { Tabs, TabsList, TabsTrigger } from '../../primitives/tabs.js';
@@ -85,10 +85,8 @@ export function SkillsPage({ projectId }) {
85
85
  });
86
86
  // The one honest auto-close: the config came back and this skill is not in
87
87
  // it, so it really was deleted or renamed.
88
- useEffect(() => {
89
- if (detail.isMissing)
90
- setSelectedPath(null);
91
- }, [detail.isMissing]);
88
+ if (detail.isMissing)
89
+ setSelectedPath(null);
92
90
  // `null` = render the grid. Otherwise which "nothing to show" copy applies:
93
91
  // genuinely zero skills vs. skills exist but this filter/search hid all of
94
92
  // them. Telling the user "No skills yet" in the second case is false and
@@ -110,7 +108,7 @@ export function SkillsPage({ projectId }) {
110
108
  // The copy invites an action, so the action is here — not only in
111
109
  // the header. Docs stays as the secondary, for the reader who has
112
110
  // 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) => {
111
+ 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
112
  if (!next)
115
113
  setSelectedPath(null);
116
114
  } }), _jsx(NewSkillModal, { projectId: projectId, open: formOpen, onOpenChange: setFormOpen })] }));
@@ -411,10 +411,11 @@ export function newConnectorSlug(app, taken) {
411
411
  .toLowerCase()
412
412
  .replace(/[^a-z0-9_-]+/g, '-')
413
413
  .replace(/^-+|-+$/g, '') || 'app';
414
- if (!taken.includes(slug))
414
+ const takenSet = new Set(taken);
415
+ if (!takenSet.has(slug))
415
416
  return slug;
416
417
  for (let n = 2;; n++)
417
- if (!taken.includes(`${slug}-${n}`))
418
+ if (!takenSet.has(`${slug}-${n}`))
418
419
  return `${slug}-${n}`;
419
420
  }
420
421
  /* ─── Delivery ──────────────────────────────────────────────────────────── */
@@ -2,7 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { CalendarDotsIcon as CalendarClock, CaretDownIcon as ChevronDown } from '@phosphor-icons/react';
4
4
  import { useTranslations } from '../../../i18n/index.js';
5
- import { useCallback, useEffect, useState } from 'react';
5
+ import { useCallback, useState } from 'react';
6
6
  import { Button } from '../../../primitives/button.js';
7
7
  import { Input } from '../../../primitives/input.js';
8
8
  import { Label } from '../../../primitives/label.js';
@@ -195,13 +195,15 @@ export function ScheduleBuilder({ value, onChange, disabled, allowOnce, runAt, o
195
195
  const onceMode = Boolean(allowOnce && onRunAtChange && runAt != null);
196
196
  const isCustom = cronToState(rawCron) === null;
197
197
  const hasPastRunAt = Boolean(runAt && new Date(runAt).getTime() <= Date.now());
198
- useEffect(() => {
198
+ const [syncedValue, setSyncedValue] = useState(value);
199
+ if (value !== syncedValue) {
200
+ setSyncedValue(value);
199
201
  const parsed = cronToState(value);
200
202
  if (parsed)
201
203
  setState(parsed);
202
204
  setRawCron(value);
203
205
  setShowCron((visible) => visible || !parsed);
204
- }, [value]);
206
+ }
205
207
  const update = useCallback((partial) => {
206
208
  const next = { ...state, ...partial };
207
209
  setState(next);
@@ -256,6 +258,7 @@ export function ScheduleBuilder({ value, onChange, disabled, allowOnce, runAt, o
256
258
  const canOnce = Boolean(allowOnce && onRunAtChange);
257
259
  const cadences = localizeUiCatalog(CADENCES, tI18nComplete, APP_REGISTRY_TRANSLATION_KEYS);
258
260
  const weekdayButtons = localizeUiCatalog(WEEKDAY_BUTTONS, tI18nComplete, APP_REGISTRY_TRANSLATION_KEYS);
261
+ const selectedWeekdays = new Set(state.weekdays);
259
262
  const intervalUnit = (value) => state.frequency === 'minutes' ? 'minutes' : `hour${value === 1 ? '' : 's'}`;
260
263
  return (_jsxs("div", { className: cn('space-y-3', disabled && 'pointer-events-none opacity-60 select-none'), children: [_jsxs("div", { className: "flex flex-wrap items-end gap-3", children: [_jsx(ScheduleField, { label: tI18nComplete.raw('textb6b7a0065808'), className: "w-full sm:w-44", children: _jsxs(Select, { value: onceMode ? 'once' : cadence, onValueChange: (next) => next === 'once' ? selectOnce() : selectCadence(next), disabled: disabled, children: [_jsx(SelectTrigger, { className: "h-9 w-full", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [cadences.map((option) => (_jsx(SelectItem, { value: option.value, children: option.label }, option.value))), canOnce ? (_jsx(SelectItem, { value: "once", children: tI18nComplete.raw('textd88f6d8372f4') })) : null] })] }) }), onceMode ? (_jsx(ScheduleField, { label: tI18nComplete.raw('text4b4c31294fb5'), className: "w-full sm:w-auto", children: _jsx(Input, { type: "datetime-local", value: runAt ? isoToLocalInput(runAt) : '', min: isoToLocalInput(new Date().toISOString()), onChange: (event) => onRunAtChange?.(localInputToIso(event.target.value)), onBlur: () => {
261
264
  if (hasPastRunAt)
@@ -265,7 +268,7 @@ export function ScheduleBuilder({ value, onChange, disabled, allowOnce, runAt, o
265
268
  : [1, 2, 3, 4, 6, 8, 12]).map((value) => (_jsxs(SelectItem, { value: String(value), children: [value, " ", intervalUnit(value)] }, value))) })] }) })), state.frequency === 'monthly' && (_jsx(ScheduleField, { label: tTrigger('dayOfMonth'), className: "w-28", children: _jsxs(Select, { value: String(state.monthDay), onValueChange: (value) => update({ monthDay: Number(value) }), disabled: disabled, children: [_jsx(SelectTrigger, { className: "h-9 w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: Array.from({ length: 31 }, (_, index) => index + 1).map((value) => (_jsx(SelectItem, { value: String(value), children: value }, value))) })] }) })), state.frequency !== 'minutes' && (_jsx(ScheduleField, { label: state.frequency === 'hourly'
266
269
  ? tI18nComplete.raw('text5f2ed72dfcbc')
267
270
  : tI18nComplete.raw('text33b93476cf59'), children: _jsxs("div", { className: "flex items-center gap-1.5", children: [state.frequency !== 'hourly' && (_jsxs(_Fragment, { children: [_jsxs(Select, { value: String(state.hour), onValueChange: (value) => update({ hour: Number(value) }), disabled: disabled, children: [_jsx(SelectTrigger, { className: "h-9 w-18 tabular-nums", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: HOURS.map((value) => (_jsx(SelectItem, { value: String(value), children: pad2(value) }, value))) })] }), _jsx("span", { className: "text-muted-foreground", children: ":" })] })), _jsxs(Select, { value: String(state.minute), onValueChange: (value) => update({ minute: Number(value) }), disabled: disabled, children: [_jsx(SelectTrigger, { className: "h-9 w-18 tabular-nums", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: MINUTE_OPTIONS.map((value) => (_jsx(SelectItem, { value: String(value), children: state.frequency === 'hourly' ? `:${pad2(value)}` : pad2(value) }, value))) })] })] }) })), trailing] }))] }), !onceMode && cadence === 'weekly' && (_jsx("div", { className: "flex flex-wrap gap-1.5", "aria-label": tI18nComplete.raw('text2069f4549744'), children: weekdayButtons.map(({ value, label }) => {
268
- const selected = state.weekdays.includes(value);
271
+ const selected = selectedWeekdays.has(value);
269
272
  return (_jsx(Button, { type: "button", variant: selected ? 'secondary' : 'outline', size: "sm", "aria-pressed": selected, onClick: () => toggleWeekday(value), disabled: disabled, className: "min-w-10 transition-transform active:scale-[0.96]", children: label }, value));
270
273
  }) })), onceMode && runAt && hasPastRunAt ? (_jsx(SchedulePreview, { text: describeRunAt(runAt, tI18nComplete), error: true })) : null, !onceMode && isCustom ? (_jsx(SchedulePreview, { text: tI18nComplete.raw('text7afd348d0d4b') })) : null, !onceMode && (_jsxs("div", { className: "space-y-2", children: [_jsxs(Button, { type: "button", variant: "text", size: "xs", onClick: () => setShowCron((visible) => !visible), disabled: disabled, className: "text-muted-foreground hover:text-foreground -ml-1 transition-transform active:scale-[0.96]", children: [_jsx(ChevronDown, { className: cn('size-3 transition-transform duration-(--duration-normal)', showCron && 'rotate-180') }), showCron
271
274
  ? tI18nComplete.raw('text6c848b936d1d')