@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.
- package/README.md +8 -4
- package/dist/access/access/access-dialog.js +8 -5
- package/dist/access/access/copy-row.js +2 -2
- package/dist/access/sharing-picker.js +2 -1
- package/dist/browser/app-in-browser.js +5 -3
- package/dist/browser/browser-panel.js +6 -6
- package/dist/browser/sandbox-browser-chrome.d.ts +4 -1
- package/dist/browser/sandbox-browser-chrome.js +4 -3
- package/dist/customize/agents/agent-access.d.ts +20 -0
- package/dist/customize/agents/agent-access.js +17 -0
- package/dist/customize/agents/agent-grant-pages.js +5 -6
- package/dist/customize/agents/agent-page.js +2 -1
- package/dist/customize/agents/agents-page.js +18 -1
- package/dist/customize/connectors/add-mcp/add-mcp-server.js +4 -2
- package/dist/customize/connectors/connector-routes.d.ts +3 -1
- package/dist/customize/connectors/connector-routes.js +18 -0
- package/dist/customize/connectors/connectors-page.js +23 -4
- package/dist/customize/connectors/detail/connector-advanced-tools.js +4 -6
- package/dist/customize/providers/provider-accordion.js +2 -1
- package/dist/customize/providers/provider-connect.js +2 -1
- package/dist/customize/sections/component/telegram-channel-row.js +1 -1
- package/dist/customize/sections/connector-connection-modal.js +11 -9
- package/dist/customize/sections/connectors-view.js +11 -13
- package/dist/customize/sections/llm-provider/constants.d.ts +0 -1
- package/dist/customize/sections/llm-provider/constants.js +0 -2
- package/dist/customize/sections/llm-provider/models-tab-group.d.ts +1 -1
- package/dist/customize/sections/llm-provider/models-tab-group.js +7 -4
- package/dist/customize/sections/llm-provider/models-tab-row.d.ts +2 -18
- package/dist/customize/sections/llm-provider/models-tab-row.js +6 -30
- package/dist/customize/sections/llm-provider/models-tab.d.ts +0 -1
- package/dist/customize/sections/llm-provider/models-tab.js +1 -10
- package/dist/customize/sections/llm-provider/use-connected-providers.js +2 -2
- package/dist/customize/sections/llm-provider/utils.d.ts +1 -1
- package/dist/customize/sections/llm-provider/utils.js +7 -14
- package/dist/customize/sections/view/gateway/_shared.js +1 -2
- package/dist/customize/sections/view/gateway/gateway-routing.js +18 -20
- package/dist/customize/sections/view/sandbox-provider-coverage.d.ts +3 -18
- package/dist/customize/sections/view/sandbox-provider-coverage.js +7 -27
- package/dist/customize/settings/rail-copy-context.js +5 -3
- package/dist/customize/settings/tabs/sandbox-tab.d.ts +2 -27
- package/dist/customize/settings/tabs/sandbox-tab.js +13 -112
- package/dist/customize/settings/tabs/sandbox-template-form.js +9 -12
- package/dist/customize/shared/capability-tabs.js +2 -1
- package/dist/customize/shared/entity/entity-modal.d.ts +5 -1
- package/dist/customize/shared/entity/entity-modal.js +7 -4
- package/dist/customize/skills/remove-skill-button.d.ts +16 -0
- package/dist/customize/skills/remove-skill-button.js +42 -0
- package/dist/customize/skills/skills-page.js +4 -6
- package/dist/customize/triggers/schedule/event-trigger-copy.js +3 -2
- package/dist/customize/triggers/schedule/schedule-builder.js +7 -4
- package/dist/customize/triggers/schedule/trigger-composer.js +5 -2
- package/dist/customize/triggers/schedule/trigger-list.d.ts +0 -2
- package/dist/customize/triggers/schedule/trigger-list.js +1 -18
- package/dist/customize/triggers/schedule/trigger-tile.d.ts +1 -32
- package/dist/customize/triggers/schedule/trigger-tile.js +1 -16
- package/dist/customize/triggers/schedule/when-event.js +1 -1
- package/dist/diff/diff-view.js +9 -10
- package/dist/files/editor/code-editor.js +2 -3
- package/dist/files/project/components/change-request-detail-dialog.d.ts +3 -1
- package/dist/files/project/components/change-request-detail-dialog.js +3 -5
- package/dist/files/project/components/change-request-status-badge.d.ts +12 -0
- package/dist/files/project/components/change-request-status-badge.js +27 -0
- package/dist/files/project/components/change-requests-panel.js +1 -1
- package/dist/files/project/components/drive-explorer.js +2 -1
- package/dist/files/project/components/drive-grid-view.js +11 -7
- package/dist/files/project/components/drive-list-view.js +4 -4
- package/dist/files/project/components/file-breadcrumbs.js +1 -1
- package/dist/files/project/components/file-explorer-page.js +1 -1
- package/dist/files/project/components/file-history-popover.js +1 -6
- package/dist/files/project/components/file-search.js +6 -1
- package/dist/files/project/components/open-change-request-dialog.js +2 -1
- package/dist/files/project/components/version-selector-logic.d.ts +11 -0
- package/dist/files/project/components/version-selector-logic.js +26 -0
- package/dist/files/project/components/version-selector.js +11 -20
- package/dist/files/project/hooks/use-change-requests.d.ts +14 -1
- package/dist/files/project/hooks/use-change-requests.js +44 -25
- package/dist/files/project/hooks/use-server-health.js +17 -15
- package/dist/files/renderers/docx/docx-viewer.js +4 -6
- package/dist/files/renderers/image-renderer.js +6 -24
- package/dist/files/renderers/pdf/pdf-renderer.d.ts +3 -1
- package/dist/files/renderers/pdf/pdf-renderer.js +2 -2
- package/dist/files/renderers/pdf/pdf-viewer.d.ts +3 -0
- package/dist/files/renderers/pdf/pdf-viewer.js +16 -7
- package/dist/files/renderers/presentation/FullScreenPresentationViewer.js +2 -11
- package/dist/files/renderers/show-content-renderer.js +1 -1
- package/dist/files/renderers/sqlite-renderer.js +8 -3
- package/dist/files/renderers/xlsx/xlsx-viewer.js +15 -16
- package/dist/files/tree/components/file-tree-item.js +1 -1
- package/dist/files/viewer/file-content-renderer.d.ts +4 -1
- package/dist/files/viewer/file-content-renderer.js +2 -2
- package/dist/files/viewer/file-preview-modal.js +1 -1
- package/dist/files/viewer/use-file-content-state.js +1 -1
- package/dist/i18n/en.d.ts +36 -35
- package/dist/i18n/en.js +45 -44
- package/dist/index.d.ts +1 -1
- package/dist/internal/desktop/desktop-back-button.js +5 -3
- package/dist/internal/hooks/use-auto-scroll.js +5 -3
- package/dist/internal/hooks/use-notification-center.js +21 -15
- package/dist/internal/hooks/use-review-session-summary.d.ts +6 -0
- package/dist/internal/hooks/use-review-session-summary.js +11 -0
- package/dist/internal/lib/browser-error-noise.d.ts +1 -1
- package/dist/internal/lib/browser-error-noise.js +1 -1
- package/dist/internal/lib/browser-noise/rules/rejections.d.ts +10 -0
- package/dist/internal/lib/browser-noise/rules/rejections.js +27 -1
- package/dist/internal/lib/provider-branding.js +14 -3
- package/dist/internal/lib/trigger-clock.d.ts +42 -0
- package/dist/internal/lib/trigger-clock.js +34 -0
- package/dist/internal/lib/web-notifications.d.ts +6 -0
- package/dist/internal/lib/web-notifications.js +57 -7
- package/dist/internal/notifications/notification-bell.d.ts +13 -0
- package/dist/internal/notifications/notification-bell.js +1 -1
- package/dist/internal/notifications/notification-rows.js +2 -1
- package/dist/internal/outcomes/outcome-types.d.ts +5 -0
- package/dist/internal/reminders/reminder-filter.d.ts +12 -0
- package/dist/internal/reminders/reminder-filter.js +34 -0
- package/dist/internal/reminders/reminder-format.d.ts +6 -0
- package/dist/internal/reminders/reminder-format.js +20 -2
- package/dist/internal/stores/kortix-computer-store.d.ts +1 -1
- package/dist/internal/stores/session-hover-store.d.ts +20 -5
- package/dist/internal/stores/session-hover-store.js +31 -13
- package/dist/internal/stores/session-panel-memory-store.js +2 -0
- package/dist/markdown/code/inline-code.js +8 -0
- package/dist/markdown/code/output-paths-context.d.ts +18 -0
- package/dist/markdown/code/output-paths-context.js +35 -0
- package/dist/markdown/genui/components/charts.js +6 -5
- package/dist/primitives/count-badge.d.ts +3 -1
- package/dist/primitives/count-badge.js +3 -3
- package/dist/primitives/glyph-paths-source.js +1 -1
- package/dist/primitives/glyph-paths.generated.js +1 -1
- package/dist/primitives/map.js +2 -1
- package/dist/primitives/preview-image.js +4 -2
- package/dist/primitives/textarea.js +1 -1
- package/dist/primitives/typed-title.js +12 -14
- package/dist/primitives/typing-animation.js +4 -2
- package/dist/providers/ui-provider.d.ts +57 -0
- package/dist/providers/ui-provider.js +4 -0
- package/dist/session/composer/ComposerCard.d.ts +2 -0
- package/dist/session/composer/ComposerCard.js +5 -1
- package/dist/session/composer/agent-selector.js +17 -5
- package/dist/session/composer/composer-chat-input.js +5 -3
- package/dist/session/composer/composer-logic.js +8 -7
- package/dist/session/composer/composer-toolbar.js +4 -2
- package/dist/session/composer/composer-underbar.js +3 -1
- package/dist/session/composer/composer.js +34 -5
- package/dist/session/composer/draft/composer-draft.js +4 -4
- package/dist/session/composer/menus/slash-actions.d.ts +22 -2
- package/dist/session/composer/menus/slash-actions.js +20 -0
- package/dist/session/composer/menus/slash-items.js +19 -3
- package/dist/session/composer/pasted-text.js +3 -2
- package/dist/session/composer/stores/queued-draft-store.js +3 -2
- package/dist/session/header/reminder-parts.d.ts +294 -0
- package/dist/session/header/reminder-parts.js +120 -0
- package/dist/session/header/session-reminders-indicator.d.ts +13 -0
- package/dist/session/header/session-reminders-indicator.js +142 -35
- package/dist/session/media/sandbox-url-detector.js +0 -7
- package/dist/session/models/model-availability.d.ts +23 -0
- package/dist/session/models/model-availability.js +20 -0
- package/dist/session/models/model-grouping.js +1 -4
- package/dist/session/models/model-selector.js +23 -14
- package/dist/session/models/use-model-connection-gate.js +4 -1
- package/dist/session/outcomes/change-request-outcomes.js +24 -3
- package/dist/session/outcomes/session-outcomes-provider.d.ts +3 -1
- package/dist/session/outcomes/session-outcomes-provider.js +31 -3
- package/dist/session/overrides/provider-secret-pool-editor.js +2 -1
- package/dist/session/overrides/session-overrides-composer.d.ts +1 -1
- package/dist/session/overrides/session-overrides-composer.js +5 -1
- package/dist/session/panel/advanced/advanced-panel.js +3 -7
- package/dist/session/panel/easy/app-preview.js +12 -2
- package/dist/session/panel/easy/easy-panel-logic.d.ts +1 -1
- package/dist/session/panel/easy/easy-panel-logic.js +1 -1
- package/dist/session/panel/easy/outputs-card.js +2 -1
- package/dist/session/panel/easy/panel-card.js +5 -3
- package/dist/session/panel/easy/preview-load.d.ts +14 -0
- package/dist/session/panel/easy/preview-load.js +15 -0
- package/dist/session/panel/easy/step-detail-body.js +2 -7
- package/dist/session/panel/mobile-tool-drawer.js +4 -2
- package/dist/session/panel/open-session-quick-view.js +2 -1
- package/dist/session/panel/panel-fallback.js +8 -4
- package/dist/session/panel/session-panel-provider.d.ts +1 -0
- package/dist/session/panel/session-panel-provider.js +19 -3
- package/dist/session/panel/session-reminders-panel.d.ts +4 -0
- package/dist/session/panel/session-reminders-panel.js +109 -0
- package/dist/session/panel/shared/use-panel-derivations.js +6 -4
- package/dist/session/panel/shared/use-panel-handoffs.d.ts +2 -2
- package/dist/session/panel/shared/use-panel-handoffs.js +5 -1
- package/dist/session/panel/use-session-panel-layout.js +7 -5
- package/dist/session/print/session-print-header.d.ts +0 -10
- package/dist/session/print/session-print-header.js +25 -9
- package/dist/session/status/session-surface.js +1 -1
- package/dist/session/thread/optimistic-turn.d.ts +5 -1
- package/dist/session/thread/optimistic-turn.js +8 -6
- package/dist/session/thread/session-chat.d.ts +6 -1
- package/dist/session/thread/session-chat.js +34 -12
- package/dist/session/thread/session-history.js +3 -1
- package/dist/session/thread/session-thread.d.ts +10 -1
- package/dist/session/thread/session-thread.js +2 -2
- package/dist/session/thread/transcript.js +13 -7
- package/dist/session/tool/shared/file-list.js +19 -2
- package/dist/session/tool/shared/infrastructure-contexts.d.ts +7 -0
- package/dist/session/tool/shared/infrastructure-contexts.js +7 -0
- package/dist/session/tool/shared/infrastructure-output.js +9 -5
- package/dist/session/tool/shared/infrastructure-row-header.js +9 -1
- package/dist/session/tool/shared/infrastructure-rows.js +7 -5
- package/dist/session/tool/tools/bash-tool.js +10 -6
- package/dist/session/tool/tools/web-search-tool.js +25 -3
- package/dist/session/turn/burst-step-body.js +4 -0
- package/dist/session/turn/recovered-failures.d.ts +8 -0
- package/dist/session/turn/recovered-failures.js +32 -0
- package/dist/session/turn/streaming-cadence.js +5 -8
- package/dist/styles.css +1 -1
- package/dist/terminal/session-terminal-panel.js +3 -5
- package/dist/theme/theme.js +2 -1
- package/dist/workspace/projects/modal/github-import-pickers.js +7 -6
- package/dist/workspace/review/review-center.js +13 -10
- package/dist/workspace/session-workspace.js +5 -9
- package/dist/workspace/sessions/deleted-session-row.js +5 -2
- package/dist/workspace/sidebar/footer/project-computer-nav.js +6 -4
- package/dist/workspace/sidebar/footer/project-mcp-nav.js +6 -2
- package/dist/workspace/sidebar/modal/rename-session-modal.js +8 -3
- package/dist/workspace/sidebar/project-session-list.js +52 -55
- package/dist/workspace/sidebar/session-brief-hover-card.d.ts +35 -18
- package/dist/workspace/sidebar/session-brief-hover-card.js +114 -37
- package/dist/workspace/sidebar/session-status-mark.d.ts +15 -1
- package/dist/workspace/sidebar/session-status-mark.js +63 -17
- package/package.json +7 -3
- package/skills/kortix-ui/SKILL.md +1 -1
- package/dist/customize/sections/view/sandbox-provider-result.d.ts +0 -47
- package/dist/customize/sections/view/sandbox-provider-result.js +0 -79
|
@@ -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,
|
|
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,
|
|
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,
|
|
26
|
-
import { contract,
|
|
27
|
-
import { ArrowClockwiseIcon, CheckCircleIcon, CircleDashedIcon, CpuIcon, CubeIcon, FileCodeIcon, HardDrivesIcon, MemoryIcon, PackageIcon, PencilSimpleIcon, PlusIcon,
|
|
22
|
+
import { buildSandboxTemplate, deleteSandboxTemplate, listProjectSnapshots, } from '@kortix/sdk';
|
|
23
|
+
import { contract, qk } from '@kortix/sdk/react';
|
|
24
|
+
import { ArrowClockwiseIcon, CheckCircleIcon, CircleDashedIcon, CpuIcon, CubeIcon, FileCodeIcon, HardDrivesIcon, MemoryIcon, PackageIcon, PencilSimpleIcon, PlusIcon, ShippingContainerIcon, SquaresFourIcon, TrashIcon, XCircleIcon, } from '@phosphor-icons/react';
|
|
28
25
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
29
|
-
import { SandboxProviderBadge,
|
|
30
|
-
import { applySandboxProviderResult, pollSandboxProviderTransition, } from '../../sections/view/sandbox-provider-result.js';
|
|
31
|
-
import { formatSandboxProvider, isLegacySandboxProvider, sortSandboxProviders, } from '../../../internal/lib/sandbox-providers.js';
|
|
26
|
+
import { SandboxProviderBadge, availableProviderCoverage, latestObservedAt, } from '../../sections/view/sandbox-provider-coverage.js';
|
|
32
27
|
import { SettingsTabHeader } from '../settings-tab-header.js';
|
|
33
28
|
import { snapshotsQueryRetry } from './snapshots-query.js';
|
|
34
|
-
/**
|
|
35
|
-
const SANDBOX_TAB_ACTIONS = [
|
|
36
|
-
PROJECT_ACTIONS.PROJECT_SANDBOX_WRITE,
|
|
37
|
-
PROJECT_ACTIONS.PROJECT_WRITE,
|
|
38
|
-
];
|
|
29
|
+
/** The one leaf this tab gates on: template create, edit, delete and rebuild. */
|
|
30
|
+
const SANDBOX_TAB_ACTIONS = [PROJECT_ACTIONS.PROJECT_SANDBOX_WRITE];
|
|
39
31
|
/** Three, not five: each skeleton is now a full card, so five of them
|
|
40
32
|
* overflowed the panel and read as a wall of grey. */
|
|
41
33
|
const TEMPLATE_SKELETON_ROWS = [
|
|
@@ -112,34 +104,19 @@ export function describeBase(template, labels = {}) {
|
|
|
112
104
|
mono: false,
|
|
113
105
|
};
|
|
114
106
|
}
|
|
115
|
-
/** Where sessions built from this template actually run. Static configuration,
|
|
116
|
-
* not live status — which is why it belongs in the spec grid next to CPU and
|
|
117
|
-
* memory, and NOT in the runtime footer where a bare `Automatic` badge used
|
|
118
|
-
* to float with no label attached to it. */
|
|
119
|
-
export function describeRouting(providerMode, selectedProvider, tI18nComplete, labels = {}) {
|
|
120
|
-
const mode = describeProviderMode(providerMode, selectedProvider, tI18nComplete);
|
|
121
|
-
if (providerMode === 'automatic')
|
|
122
|
-
return { label: labels.automatic ?? 'Automatic', icon: ShuffleIcon };
|
|
123
|
-
return {
|
|
124
|
-
label: mode.selectedProvider
|
|
125
|
-
? (labels.pinnedWithProvider?.(mode.selectedProvider) ?? `Pinned · ${mode.selectedProvider}`)
|
|
126
|
-
: (labels.pinned ?? 'Pinned'),
|
|
127
|
-
icon: PushPinIcon,
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
107
|
/** Locale-aware age for the runtime footer's "Checked …" stamp. */
|
|
131
108
|
export function TemplateStateLine({ tone, label }) {
|
|
132
109
|
const toneClass = TEMPLATE_TONE_TEXT[tone];
|
|
133
110
|
return (_jsxs("span", { className: cn('flex items-center gap-1.5 text-xs font-medium', toneClass), children: [tone === 'busy' ? (_jsx(Loading, { variant: "spokes", className: cn('size-3.5 shrink-0', toneClass) })) : tone === 'ok' ? (_jsx(CheckCircleIcon, { weight: "fill", className: "size-3.5 shrink-0" })) : tone === 'fail' ? (_jsx(XCircleIcon, { weight: "fill", className: "size-3.5 shrink-0" })) : (_jsx(CircleDashedIcon, { className: "size-3.5 shrink-0" })), label] }));
|
|
134
111
|
}
|
|
135
|
-
export function TemplateRuntimeFooter({ coverage,
|
|
112
|
+
export function TemplateRuntimeFooter({ coverage, formatObservedAt, runtimeLabel, checkedLabel, badgeCopy, }) {
|
|
136
113
|
const available = availableProviderCoverage(coverage);
|
|
137
114
|
if (available.length === 0)
|
|
138
115
|
return null;
|
|
139
116
|
const observedAt = latestObservedAt(coverage);
|
|
140
|
-
return (_jsxs("div", { className: "border-border/60 bg-muted/25 flex flex-wrap items-center gap-x-3 gap-y-1.5 border-t px-4 py-2.5", children: [_jsx("span", { className: "text-muted-foreground shrink-0 text-xs", children: runtimeLabel }), _jsx("div", { className: "flex flex-wrap items-center gap-1.5", children: available.map((item) => (_jsx(SandboxProviderBadge, { item: item,
|
|
117
|
+
return (_jsxs("div", { className: "border-border/60 bg-muted/25 flex flex-wrap items-center gap-x-3 gap-y-1.5 border-t px-4 py-2.5", children: [_jsx("span", { className: "text-muted-foreground shrink-0 text-xs", children: runtimeLabel }), _jsx("div", { className: "flex flex-wrap items-center gap-1.5", children: available.map((item) => (_jsx(SandboxProviderBadge, { item: item, copy: badgeCopy }, item.provider))) }), observedAt ? (_jsx("span", { className: "text-muted-foreground ml-auto shrink-0 text-xs tabular-nums", children: checkedLabel(formatObservedAt(observedAt)) })) : null] }));
|
|
141
118
|
}
|
|
142
|
-
function TemplateCard({ projectId, template, canManage, onEdit,
|
|
119
|
+
function TemplateCard({ projectId, template, canManage, onEdit, }) {
|
|
143
120
|
const tI18nComplete = useTranslations('hardcodedUi.i18nComplete');
|
|
144
121
|
const t = useTranslations('settings.sandbox');
|
|
145
122
|
const locale = useLocale();
|
|
@@ -189,12 +166,7 @@ function TemplateCard({ projectId, template, canManage, onEdit, providerMode, se
|
|
|
189
166
|
builtFrom: t('facts.builtFrom'),
|
|
190
167
|
kortixDefault: t('facts.kortixDefault'),
|
|
191
168
|
});
|
|
192
|
-
|
|
193
|
-
automatic: t('routing.automatic'),
|
|
194
|
-
pinned: t('routing.pinned'),
|
|
195
|
-
pinnedWithProvider: (provider) => t('routing.pinnedWithProvider', { provider }),
|
|
196
|
-
});
|
|
197
|
-
return (_jsxs(_Fragment, { children: [_jsxs("li", { className: "bg-popover overflow-hidden rounded-md border", children: [_jsxs("div", { className: "flex flex-wrap items-start justify-between gap-3 px-4 pt-4 pb-3.5", children: [_jsxs("div", { className: "min-w-0 space-y-1.5", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-1.5", children: [_jsx("span", { className: "text-foreground truncate text-sm font-medium", children: template.name }), _jsx(Badge, { variant: "secondary", size: "sm", children: template.slug }), template.is_default ? (_jsx(Badge, { variant: "muted", size: "sm", children: t('defaultBadge') })) : null] }), _jsx(TemplateStateLine, { tone: stateInfo.tone, label: stateInfo.label })] }), canManage && (_jsxs("div", { className: "flex items-center gap-1", children: [templateId && !template.is_default && (_jsxs(_Fragment, { children: [_jsx(Hint, { label: t('actions.edit'), side: "top", children: _jsx(Button, { size: "icon-base", variant: "ghost", className: "transition-transform active:scale-[0.96]", onClick: () => onEdit(template), "aria-label": t('actions.edit'), children: _jsx(PencilSimpleIcon, { className: "size-3.5 shrink-0" }) }) }), _jsx(Hint, { label: t('actions.delete'), side: "top", children: _jsx(Button, { size: "icon-base", variant: "ghost", className: "text-destructive hover:text-destructive transition-transform active:scale-[0.96]", disabled: deleteMut.isPending, onClick: () => setConfirmDelete(true), "aria-label": t('actions.delete'), children: deleteMut.isPending ? (_jsx(Loading, { className: "size-3.5 shrink-0" })) : (_jsx(TrashIcon, { className: "size-3.5 shrink-0" })) }) })] })), templateId && (_jsxs(Button, { size: "sm", variant: "outline", className: "gap-1.5 transition-transform active:scale-[0.96]", disabled: buildMut.isPending, onClick: () => buildMut.mutate(), children: [buildMut.isPending ? (_jsx(Loading, { className: "size-3.5 shrink-0" })) : (_jsx(ArrowClockwiseIcon, { className: "size-3.5 shrink-0" })), t('actions.rebuild')] }))] }))] }), _jsxs("dl", { className: "border-border/60 grid grid-cols-2 gap-x-4 gap-y-4 border-t px-4 py-4 tabular-nums sm:grid-cols-3", children: [_jsx(FactCell, { icon: CpuIcon, label: t('facts.processor'), value: `${template.cpu} vCPU` }), _jsx(FactCell, { icon: MemoryIcon, label: t('facts.memory'), value: `${template.memory_gb} GiB` }), _jsx(FactCell, { icon: HardDrivesIcon, label: t('facts.storage'), value: `${template.disk_gb} GiB` }), _jsx(FactCell, { icon: base.icon, label: base.label, value: base.value, mono: base.mono }), _jsx(FactCell, { icon: source.icon, label: t('facts.definedIn'), value: source.label, mono: source.mono }), _jsx(FactCell, { icon: routing.icon, label: t('routing.title'), value: routing.label })] }), _jsx(TemplateRuntimeFooter, { coverage: template.provider_coverage, providerMode: providerMode, selectedProvider: selectedProvider, runtimeLabel: t('runtime'), checkedLabel: (value) => t('checked', { value }), formatObservedAt: (observedAt) => relativeTime(observedAt, locale, Date.now(), LONG_AGO), badgeCopy: {
|
|
169
|
+
return (_jsxs(_Fragment, { children: [_jsxs("li", { className: "bg-popover overflow-hidden rounded-md border", children: [_jsxs("div", { className: "flex flex-wrap items-start justify-between gap-3 px-4 pt-4 pb-3.5", children: [_jsxs("div", { className: "min-w-0 space-y-1.5", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-1.5", children: [_jsx("span", { className: "text-foreground truncate text-sm font-medium", children: template.name }), _jsx(Badge, { variant: "secondary", size: "sm", children: template.slug }), template.is_default ? (_jsx(Badge, { variant: "muted", size: "sm", children: t('defaultBadge') })) : null] }), _jsx(TemplateStateLine, { tone: stateInfo.tone, label: stateInfo.label })] }), canManage && (_jsxs("div", { className: "flex items-center gap-1", children: [templateId && !template.is_default && (_jsxs(_Fragment, { children: [_jsx(Hint, { label: t('actions.edit'), side: "top", children: _jsx(Button, { size: "icon-base", variant: "ghost", className: "transition-transform active:scale-[0.96]", onClick: () => onEdit(template), "aria-label": t('actions.edit'), children: _jsx(PencilSimpleIcon, { className: "size-3.5 shrink-0" }) }) }), _jsx(Hint, { label: t('actions.delete'), side: "top", children: _jsx(Button, { size: "icon-base", variant: "ghost", className: "text-destructive hover:text-destructive transition-transform active:scale-[0.96]", disabled: deleteMut.isPending, onClick: () => setConfirmDelete(true), "aria-label": t('actions.delete'), children: deleteMut.isPending ? (_jsx(Loading, { className: "size-3.5 shrink-0" })) : (_jsx(TrashIcon, { className: "size-3.5 shrink-0" })) }) })] })), templateId && (_jsxs(Button, { size: "sm", variant: "outline", className: "gap-1.5 transition-transform active:scale-[0.96]", disabled: buildMut.isPending, onClick: () => buildMut.mutate(), children: [buildMut.isPending ? (_jsx(Loading, { className: "size-3.5 shrink-0" })) : (_jsx(ArrowClockwiseIcon, { className: "size-3.5 shrink-0" })), t('actions.rebuild')] }))] }))] }), _jsxs("dl", { className: "border-border/60 grid grid-cols-2 gap-x-4 gap-y-4 border-t px-4 py-4 tabular-nums sm:grid-cols-3", children: [_jsx(FactCell, { icon: CpuIcon, label: t('facts.processor'), value: `${template.cpu} vCPU` }), _jsx(FactCell, { icon: MemoryIcon, label: t('facts.memory'), value: `${template.memory_gb} GiB` }), _jsx(FactCell, { icon: HardDrivesIcon, label: t('facts.storage'), value: `${template.disk_gb} GiB` }), _jsx(FactCell, { icon: base.icon, label: base.label, value: base.value, mono: base.mono }), _jsx(FactCell, { icon: source.icon, label: t('facts.definedIn'), value: source.label, mono: source.mono })] }), _jsx(TemplateRuntimeFooter, { coverage: template.provider_coverage, runtimeLabel: t('runtime'), checkedLabel: (value) => t('checked', { value }), formatObservedAt: (observedAt) => relativeTime(observedAt, locale, Date.now(), LONG_AGO), badgeCopy: {
|
|
198
170
|
states: {
|
|
199
171
|
ready: t('coverage.ready'),
|
|
200
172
|
building: t('coverage.building'),
|
|
@@ -203,71 +175,14 @@ function TemplateCard({ projectId, template, canManage, onEdit, providerMode, se
|
|
|
203
175
|
unavailable: t('coverage.unavailable'),
|
|
204
176
|
unknown: t('states.unknown'),
|
|
205
177
|
},
|
|
206
|
-
selected: t('coverage.selected'),
|
|
207
|
-
selectedAria: t('coverage.selectedAria'),
|
|
208
178
|
} })] }), _jsx(ConfirmDialog, { open: confirmDelete, onOpenChange: setConfirmDelete, title: t('deleteDialog.title', { name: template.name }), description: t('deleteDialog.description'), confirmLabel: t('deleteDialog.confirm'), confirmVariant: "destructive", isPending: deleteMut.isPending, onConfirm: () => deleteMut.mutate() })] }));
|
|
209
179
|
}
|
|
210
180
|
const TEMPLATE_SKELETON_HEIGHT = 'h-60';
|
|
211
|
-
const AUTO_PROVIDER = '__auto__';
|
|
212
|
-
function SandboxProviderRow({ project, canManage, }) {
|
|
213
|
-
const tI18nComplete = useTranslations('hardcodedUi.i18nComplete');
|
|
214
|
-
const t = useTranslations('settings.sandbox');
|
|
215
|
-
const queryClient = useQueryClient();
|
|
216
|
-
const available = sortSandboxProviders(project.available_sandbox_providers ?? []);
|
|
217
|
-
const current = project.default_sandbox_provider ?? null;
|
|
218
|
-
const mutation = useMutation({
|
|
219
|
-
mutationFn: (next) => updateProjectSandboxProvider(project.project_id, next),
|
|
220
|
-
onSuccess: (result, next) => {
|
|
221
|
-
// FIX-L: the PATCH returns EITHER the updated project (immediate) OR a
|
|
222
|
-
// preparation object (the prepare branch — a switch to a different enabled
|
|
223
|
-
// provider). Write the project cache ONLY for the immediate result; a
|
|
224
|
-
// preparation is a transition, not a project, and must not clobber the
|
|
225
|
-
// cached project shape.
|
|
226
|
-
const kind = applySandboxProviderResult(queryClient, project.project_id, result);
|
|
227
|
-
if (kind === 'preparation') {
|
|
228
|
-
successToast(tI18nComplete('text32f14aa9ea33', {
|
|
229
|
-
provider: next ? formatSandboxProvider(next) : tI18nComplete('text22412aa281cb'),
|
|
230
|
-
}));
|
|
231
|
-
// Poll the durable transition (bounded, backoff, terminal-stop, 404 = done)
|
|
232
|
-
// and refresh the project once it settles so the now-active provider shows.
|
|
233
|
-
void pollSandboxProviderTransition(project.project_id, {
|
|
234
|
-
onSettled: (state) => {
|
|
235
|
-
// invalidateProject() reaches qk.project.scope(project.project_id),
|
|
236
|
-
// which qk.project.summary(project.project_id) nests under — so it
|
|
237
|
-
// already covers the bare-project row too; no separate summary
|
|
238
|
-
// invalidation needed here.
|
|
239
|
-
void invalidateProject(queryClient, project.project_id);
|
|
240
|
-
// qk.projects.scope(): restores the reach the old bare
|
|
241
|
-
// projects-literal prefix match had. A sandbox-provider switch
|
|
242
|
-
// is rare — over-invalidating a few extra account lists costs
|
|
243
|
-
// nothing measurable.
|
|
244
|
-
queryClient.invalidateQueries({ queryKey: qk.projects.scope() });
|
|
245
|
-
const status = state?.latest?.status;
|
|
246
|
-
if (status === 'activated') {
|
|
247
|
-
successToast(tI18nComplete('text1c145081eddd', {
|
|
248
|
-
provider: formatSandboxProvider(state?.latest?.target_provider ?? ''),
|
|
249
|
-
}));
|
|
250
|
-
}
|
|
251
|
-
else if (status === 'failed') {
|
|
252
|
-
errorToast(state?.latest?.label || tI18nComplete('texte9ca8360a34d'));
|
|
253
|
-
}
|
|
254
|
-
},
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
},
|
|
258
|
-
onError: (error) => errorToast(error.message || tI18nComplete('text914be6d9bb89')),
|
|
259
|
-
});
|
|
260
|
-
if (available.length === 0)
|
|
261
|
-
return null;
|
|
262
|
-
return (_jsxs("section", { className: "space-y-3", children: [_jsx(SettingsSubsectionHeader, { title: t('routing.title') }), _jsx(SettingsRowGroup, { children: _jsx(SettingsRow, { label: _jsxs(_Fragment, { children: [t('provider.label'), _jsx(Badge, { variant: "highlight", size: "sm", children: t('provider.experimental') })] }), description: available.some(isLegacySandboxProvider)
|
|
263
|
-
? `${t('provider.description')} ${t('provider.legacyNote')}`
|
|
264
|
-
: t('provider.description'), children: _jsxs(Select, { value: current ?? AUTO_PROVIDER, onValueChange: (v) => mutation.mutate(v === AUTO_PROVIDER ? null : (available.find((provider) => provider === v) ?? null)), disabled: !canManage || mutation.isPending, children: [_jsx(SelectTrigger, { "aria-label": t('provider.label'), className: "h-8 w-48 shrink-0", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { align: "end", children: [_jsx(SelectItem, { value: AUTO_PROVIDER, children: t('routing.automatic') }), available.map((p) => (_jsx(SelectItem, { value: p, children: _jsx(SandboxProviderLabel, { provider: p }) }, p)))] })] }) }) })] }));
|
|
265
|
-
}
|
|
266
181
|
/** Presentational only — no hooks, no data fetching, no store or Supabase
|
|
267
182
|
* read. Kept separate from `SandboxTab` so this renders under
|
|
268
183
|
* `renderToStaticMarkup` with no `QueryClientProvider` — see
|
|
269
184
|
* `GeneralTabView` for the same split. */
|
|
270
|
-
export function SandboxTabView({ isLoading = false, isError = false, errorMessage = '', onRetry = () => { }, headerAction,
|
|
185
|
+
export function SandboxTabView({ isLoading = false, isError = false, errorMessage = '', onRetry = () => { }, headerAction, manifestVersion = null, templatesError = null, isEmpty = true, emptyAction, templatesSlot, copy: copyOverride, }) {
|
|
271
186
|
const t = useTranslations('settings.sandbox');
|
|
272
187
|
const copy = copyOverride ?? {
|
|
273
188
|
loadFailed: t('loadFailed'),
|
|
@@ -279,7 +194,7 @@ export function SandboxTabView({ isLoading = false, isError = false, errorMessag
|
|
|
279
194
|
emptyDescription: t('emptyDescription'),
|
|
280
195
|
};
|
|
281
196
|
const tI18nComplete = useTranslations('hardcodedUi.i18nComplete');
|
|
282
|
-
return (_jsx("div", { className: "mx-auto w-full max-w-2xl space-y-8", children: _jsxs("div", { className: "space-y-8", children: [_jsx(SettingsTabHeader, { tab: "sandbox", action: headerAction }),
|
|
197
|
+
return (_jsx("div", { className: "mx-auto w-full max-w-2xl space-y-8", children: _jsxs("div", { className: "space-y-8", children: [_jsx(SettingsTabHeader, { tab: "sandbox", action: headerAction }), isLoading ? (_jsx("div", { className: "space-y-2", children: TEMPLATE_SKELETON_ROWS.map((row) => (_jsx(Skeleton, { className: cn(TEMPLATE_SKELETON_HEIGHT, 'rounded-md') }, row))) })) : isError ? (_jsx(ErrorState, { size: "sm", title: copy.loadFailed, description: errorMessage, action: _jsx(Button, { variant: "outline", size: "sm", onClick: onRetry, children: copy.retry }) })) : (_jsxs("div", { className: "space-y-4", children: [_jsxs("p", { className: "text-muted-foreground text-sm text-pretty", children: [copy.descriptionStart, " ", _jsx("code", { className: "font-mono", children: "/workspace" }), ".", ' ', copy.descriptionEnd, ' ', manifestFilename(manifestVersion) === 'kortix.yaml' ? (_jsxs(_Fragment, { children: [_jsx("code", { className: "font-mono", children: "sandbox.templates" }), ' ', tI18nComplete.raw('text582967534d0f'), ' ', _jsx("code", { className: "font-mono", children: "kortix.yaml" })] })) : (_jsxs(_Fragment, { children: [_jsx("code", { className: "font-mono", children: "[[sandbox.templates]]" }), ' ', tI18nComplete.raw('text582967534d0f'), ' ', _jsx("code", { className: "font-mono", children: "kortix.toml" })] })), "."] }), templatesError ? (_jsxs(InfoBanner, { tone: "warning", children: [copy.configReadFailed, " ", templatesError] })) : null, isEmpty ? (_jsx(EmptyState, { icon: ShippingContainerIcon, size: "sm", title: copy.emptyTitle, description: copy.emptyDescription, action: emptyAction })) : (templatesSlot)] }))] }) }));
|
|
283
198
|
}
|
|
284
199
|
/** Container: owns every hook (project + snapshots queries, the template
|
|
285
200
|
* form's open/edit state) and renders `SandboxTabView` with real data +
|
|
@@ -289,17 +204,8 @@ export function SandboxTabView({ isLoading = false, isError = false, errorMessag
|
|
|
289
204
|
* `null` otherwise), so nothing here fetches on panel open. */
|
|
290
205
|
export function SandboxTab({ projectId }) {
|
|
291
206
|
const t = useTranslations('settings.sandbox');
|
|
292
|
-
const projectQuery = useQuery({
|
|
293
|
-
queryKey: qk.project.summary(projectId),
|
|
294
|
-
queryFn: () => getProject(projectId),
|
|
295
|
-
...contract('config'),
|
|
296
|
-
});
|
|
297
|
-
// Two different leaves, one batched probe. Template CRUD asserts
|
|
298
|
-
// `project.sandbox.write`; the provider row asserts `project.write`. They
|
|
299
|
-
// are deliberately NOT merged — a role may hold one without the other.
|
|
300
207
|
const caps = useProjectCans(projectId, SANDBOX_TAB_ACTIONS);
|
|
301
208
|
const canManage = caps[PROJECT_ACTIONS.PROJECT_SANDBOX_WRITE]?.allowed === true;
|
|
302
|
-
const canEditProvider = caps[PROJECT_ACTIONS.PROJECT_WRITE]?.allowed === true;
|
|
303
209
|
const { version: manifestVersion } = useProjectManifestVersion(projectId);
|
|
304
210
|
const snapshotsQuery = useQuery({
|
|
305
211
|
queryKey: qk.project.snapshots(projectId),
|
|
@@ -320,11 +226,6 @@ export function SandboxTab({ projectId }) {
|
|
|
320
226
|
const [editingTemplate, setEditingTemplate] = useState(null);
|
|
321
227
|
const data = snapshotsQuery.data;
|
|
322
228
|
const templates = Array.isArray(data?.templates) ? data.templates : [];
|
|
323
|
-
const pinnedProvider = projectQuery.data
|
|
324
|
-
? (projectQuery.data.default_sandbox_provider ?? null)
|
|
325
|
-
: (data?.selected_provider ?? null);
|
|
326
|
-
const providerMode = pinnedProvider ? 'pinned' : 'automatic';
|
|
327
|
-
const selectedProvider = pinnedProvider;
|
|
328
229
|
const openNewForm = () => {
|
|
329
230
|
setEditingTemplate(null);
|
|
330
231
|
setFormOpen(true);
|
|
@@ -334,5 +235,5 @@ export function SandboxTab({ projectId }) {
|
|
|
334
235
|
setFormOpen(true);
|
|
335
236
|
};
|
|
336
237
|
const newTemplateAction = canManage ? (_jsxs(Button, { size: "sm", variant: "outline", className: "gap-1.5 transition-transform active:scale-[0.96]", onClick: openNewForm, children: [_jsx(PlusIcon, { className: "size-3.5 shrink-0" }), t('actions.new')] })) : undefined;
|
|
337
|
-
return (_jsxs(_Fragment, { children: [_jsx(SandboxTabView, { isLoading: snapshotsQuery.isLoading, isError: snapshotsQuery.isError, errorMessage: snapshotsQuery.error?.message ?? '', onRetry: () => snapshotsQuery.refetch(), headerAction: canManage ? (_jsxs(Button, { size: "sm", variant: "secondary", className: "gap-1.5 transition-transform active:scale-[0.96]", onClick: openNewForm, children: [_jsx(PlusIcon, { className: "size-4 shrink-0" }), t('actions.new')] })) : undefined,
|
|
238
|
+
return (_jsxs(_Fragment, { children: [_jsx(SandboxTabView, { isLoading: snapshotsQuery.isLoading, isError: snapshotsQuery.isError, errorMessage: snapshotsQuery.error?.message ?? '', onRetry: () => snapshotsQuery.refetch(), headerAction: canManage ? (_jsxs(Button, { size: "sm", variant: "secondary", className: "gap-1.5 transition-transform active:scale-[0.96]", onClick: openNewForm, children: [_jsx(PlusIcon, { className: "size-4 shrink-0" }), t('actions.new')] })) : undefined, manifestVersion: manifestVersion, templatesError: data?.templates_error ?? null, isEmpty: templates.length === 0, emptyAction: newTemplateAction, templatesSlot: templates.length === 0 ? undefined : (_jsx("ul", { className: "space-y-2", children: templates.map((t) => (_jsx(TemplateCard, { projectId: projectId, template: t, canManage: canManage, onEdit: openEditForm }, t.template_id ?? `tpl-${t.slug}`))) })) }), _jsx(SandboxTemplateForm, { projectId: projectId, open: formOpen, onOpenChange: setFormOpen, template: editingTemplate })] }));
|
|
338
239
|
}
|
|
@@ -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 {
|
|
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 [
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
}
|
|
86
|
+
}
|
|
87
87
|
// Auto-slug from name when the user hasn't typed a slug manually yet.
|
|
88
|
-
|
|
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
|
|
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 ?
|
|
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 {
|
|
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
|
-
|
|
89
|
-
|
|
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
|
-
|
|
414
|
+
const takenSet = new Set(taken);
|
|
415
|
+
if (!takenSet.has(slug))
|
|
415
416
|
return slug;
|
|
416
417
|
for (let n = 2;; n++)
|
|
417
|
-
if (!
|
|
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,
|
|
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
|
-
|
|
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
|
-
}
|
|
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 =
|
|
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')
|