@kortix/ui 0.14.6 → 0.14.7
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/dist/_vendor/kortix-shared.js +489 -381
- package/dist/customize/agents/agent-authority-card.js +1 -2
- package/dist/customize/agents/agent-detail-aside.js +15 -14
- package/dist/customize/agents/agent-drives-section.js +5 -6
- package/dist/customize/agents/agent-grant-pages.js +68 -70
- package/dist/customize/agents/agent-nav.prototype.d.ts +35 -0
- package/dist/customize/agents/agent-nav.prototype.js +95 -0
- package/dist/customize/agents/agent-page.js +57 -22
- package/dist/customize/agents/agent-people-section.js +18 -17
- package/dist/customize/agents/agent-triggers-section.js +6 -5
- package/dist/customize/connectors/catalog/catalog-entry.d.ts +14 -0
- package/dist/customize/connectors/catalog/catalog-entry.js +18 -1
- package/dist/customize/connectors/catalog/connector-browse.d.ts +28 -0
- package/dist/customize/connectors/catalog/connector-browse.js +18 -5
- package/dist/customize/connectors/detail/app-page.js +19 -5
- package/dist/customize/connectors/detail/connector-modal.d.ts +10 -13
- package/dist/customize/connectors/detail/connector-modal.js +18 -101
- package/dist/customize/connectors/detail/connector-notice.js +6 -2
- package/dist/customize/connectors/detail/connector-page.d.ts +30 -0
- package/dist/customize/connectors/detail/connector-page.js +26 -13
- package/dist/customize/sections/llm-provider/custom-provider-form.js +24 -7
- package/dist/customize/sections/llm-provider/custom-provider-panel.js +44 -8
- package/dist/customize/sections/llm-provider/types.d.ts +3 -0
- package/dist/customize/sections/llm-provider/utils.js +2 -2
- package/dist/customize/sections/view/agent-editor-access-fields.d.ts +9 -16
- package/dist/customize/sections/view/agent-editor-access-fields.js +30 -30
- package/dist/customize/sections/view/agent-editor-basics-fields.js +3 -10
- package/dist/customize/sections/view/agent-editor-primitives.d.ts +28 -10
- package/dist/customize/sections/view/agent-editor-primitives.js +32 -19
- package/dist/customize/sections/view/agent-editor.js +3 -3
- package/dist/customize/sections/view/gateway/gateway-routing.js +3 -3
- package/dist/customize/sections/view/grant-mode-field.d.ts +2 -5
- package/dist/customize/sections/view/grant-mode-field.js +10 -18
- package/dist/customize/sections/view/permission-editor.js +11 -8
- package/dist/customize/sections/view/secrets-view.js +2 -1
- package/dist/customize/settings/tabs/general-tab.d.ts +9 -2
- package/dist/customize/settings/tabs/general-tab.js +33 -11
- package/dist/customize/settings/tabs/transfer-workspace-dialog.d.ts +4 -0
- package/dist/customize/settings/tabs/transfer-workspace-dialog.js +82 -0
- package/dist/files/project/api/runtime-files.js +3 -2
- package/dist/files/project/file-source.js +18 -1
- package/dist/files/project/hooks/use-file-content.js +2 -0
- package/dist/files/viewer/file-header.js +1 -1
- package/dist/files/viewer/file-preview-modal.d.ts +1 -1
- package/dist/files/viewer/file-preview-modal.js +12 -2
- package/dist/files/viewer/file-source.d.ts +3 -1
- package/dist/files/viewer/use-file-content-state.js +26 -7
- package/dist/files/viewer/viewer-actions.js +2 -2
- package/dist/i18n/en.d.ts +345 -12
- package/dist/i18n/en.js +350 -17
- package/dist/internal/files/types.d.ts +2 -0
- package/dist/internal/goals/goal-meta.d.ts +30 -0
- package/dist/internal/goals/goal-meta.js +55 -0
- package/dist/internal/lib/query-client-singleton.d.ts +9 -3
- package/dist/internal/lib/query-client-singleton.js +18 -12
- package/dist/internal/share/session-shares-modal.js +3 -7
- package/dist/internal/stores/kortix-computer-store.js +14 -2
- package/dist/internal/stores/session-hover-store.d.ts +32 -0
- package/dist/internal/stores/session-hover-store.js +38 -0
- package/dist/internal/stores/sidebar-nav-store.d.ts +1 -1
- package/dist/internal/stores/sidebar-nav-store.js +3 -1
- package/dist/lib/routes.d.ts +2 -0
- package/dist/lib/routes.js +1 -0
- package/dist/primitives/settings-section-header.d.ts +1 -1
- package/dist/session/composer/attachment-tile.d.ts +0 -2
- package/dist/session/composer/attachment-tile.js +0 -8
- package/dist/session/composer/clipboard-files.d.ts +2 -1
- package/dist/session/composer/composer-draft-recovery.d.ts +1 -2
- package/dist/session/composer/composer-draft-recovery.js +0 -13
- package/dist/session/composer/context-ring.d.ts +1 -1
- package/dist/session/composer/context-ring.js +2 -2
- package/dist/session/composer/context-usage.d.ts +3 -6
- package/dist/session/composer/context-usage.js +3 -6
- package/dist/session/composer/inbox-row-claims.d.ts +2 -2
- package/dist/session/composer/inbox-row-claims.js +1 -1
- package/dist/session/composer/queue-projection.d.ts +3 -2
- package/dist/session/composer/queued-prompt-edit.d.ts +1 -1
- package/dist/session/composer/reasoning-effort-selector.d.ts +2 -1
- package/dist/session/composer/session-composer-readiness.d.ts +7 -22
- package/dist/session/composer/session-composer-readiness.js +12 -40
- package/dist/session/composer/token-progress.d.ts +1 -1
- package/dist/session/composer/token-progress.js +2 -2
- package/dist/session/header/session-goal-indicator.d.ts +1 -0
- package/dist/session/header/session-goal-indicator.js +41 -0
- package/dist/session/header/session-site-header.js +2 -1
- package/dist/session/panel/session-action-panel-column.js +11 -1
- package/dist/session/panel/session-audit-shared.d.ts +0 -4
- package/dist/session/panel/session-audit-shared.js +1 -1
- package/dist/session/panel/session-changes-shared.d.ts +2 -1
- package/dist/session/prompts/session-permission-prompt.js +6 -3
- package/dist/session/status/provisioning-failure.d.ts +2 -1
- package/dist/session/status/session-error-banner.js +8 -6
- package/dist/session/status/session-starting-loader.d.ts +24 -31
- package/dist/session/status/session-starting-loader.js +32 -81
- package/dist/session/thread/chat-minimap-items.d.ts +4 -4
- package/dist/session/thread/first-prompt-handover.d.ts +3 -2
- package/dist/session/thread/message-parsing.d.ts +3 -3
- package/dist/session/thread/session-chat.js +3 -10
- package/dist/session/thread/session-history-scroll.d.ts +2 -1
- package/dist/session/thread/session-thread.js +1 -1
- package/dist/styles.css +1 -1
- package/dist/terminal/pty-connection.d.ts +5 -4
- package/dist/workspace/index.d.ts +1 -0
- package/dist/workspace/index.js +1 -0
- package/dist/workspace/saved-session-skeleton-shape.d.ts +4 -3
- package/dist/workspace/saved-session-skeleton.d.ts +5 -1
- package/dist/workspace/saved-session-skeleton.js +5 -4
- package/dist/workspace/session-workspace.js +4 -5
- package/dist/workspace/sidebar/footer/project-goals-nav.d.ts +6 -0
- package/dist/workspace/sidebar/footer/project-goals-nav.js +26 -0
- package/dist/workspace/sidebar/modal/session-embed-section.d.ts +25 -0
- package/dist/workspace/sidebar/modal/session-embed-section.js +71 -0
- package/dist/workspace/sidebar/modal/share-session-modal.js +2 -2
- package/dist/workspace/sidebar/nav-row-meta.js +14 -2
- package/dist/workspace/sidebar/project-nav-items.js +3 -0
- package/dist/workspace/sidebar/project-session-list.js +12 -3
- package/dist/workspace/sidebar/session-brief-hover-card.js +5 -8
- package/package.json +4 -4
- package/skills/kortix-ui/references/modules.md +2 -0
- package/dist/session/status/use-readiness-settling.d.ts +0 -18
- package/dist/session/status/use-readiness-settling.js +0 -43
- package/dist/workspace/sidebar/modal/session-public-link-section.d.ts +0 -13
- package/dist/workspace/sidebar/modal/session-public-link-section.js +0 -66
|
@@ -14,6 +14,7 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge
|
|
|
14
14
|
import { Field, FieldDescription, FieldLabel } from '../../../primitives/field.js';
|
|
15
15
|
import Hint from '../../../primitives/hint.js';
|
|
16
16
|
import { HoverCard, HoverCardContent, HoverCardTrigger } from '../../../primitives/hover-card.js';
|
|
17
|
+
import { Alert, AlertDescription, AlertTitle } from '../../../primitives/alert.js';
|
|
17
18
|
import { InfoBanner } from '../../../primitives/info-banner.js';
|
|
18
19
|
import { Input } from '../../../primitives/input.js';
|
|
19
20
|
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupSearch, InputGroupSearchClear, InputGroupSearchIcon, InputGroupSearchInput, } from '../../../primitives/input-group.js';
|
|
@@ -666,7 +667,7 @@ function SecretDialog({ open, onOpenChange, projectId, row, connectors, connecto
|
|
|
666
667
|
/* Read-only, because another flow assigned it. Same treatment
|
|
667
668
|
the git-connection secret always had, now covering the LLM
|
|
668
669
|
gateway and connector rows it was always true of. */
|
|
669
|
-
_jsxs("div", { id: "secret-dialog-delivery", className: "border-border bg-sidebar rounded-md border p-3", children: [_jsx(Badge, { variant: selectedDelivery.tone, size: "sm", children: selectedDelivery.label }), _jsxs("p", { className: "text-muted-foreground mt-2 text-xs text-pretty", children: [selectedDelivery.description, " ", tI18nComplete.raw('text5740535823f9')] })] })) : (_jsxs(Select, { value: exposure, onValueChange: (next) => setPickedExposure(next), disabled: save.isPending, children: [_jsx(SelectTrigger, { id: "secret-dialog-delivery", className: "min-h-10 w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: exposureOptions.map((option) => (_jsx(SelectItem, { value: option.exposure, description: option.description, children: option.label }, option.exposure))) })] })), !usageAssigned && (_jsx(FieldDescription, { children: selectedDelivery.description }))] }), strategy === 'runtime' && (
|
|
670
|
+
_jsxs("div", { id: "secret-dialog-delivery", className: "border-border bg-sidebar rounded-md border p-3", children: [_jsx(Badge, { variant: selectedDelivery.tone, size: "sm", children: selectedDelivery.label }), _jsxs("p", { className: "text-muted-foreground mt-2 text-xs text-pretty", children: [selectedDelivery.description, " ", tI18nComplete.raw('text5740535823f9')] })] })) : (_jsxs(Select, { value: exposure, onValueChange: (next) => setPickedExposure(next), disabled: save.isPending, children: [_jsx(SelectTrigger, { id: "secret-dialog-delivery", className: "min-h-10 w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: exposureOptions.map((option) => (_jsx(SelectItem, { value: option.exposure, description: option.description, children: option.label }, option.exposure))) })] })), !usageAssigned && (_jsx(FieldDescription, { children: selectedDelivery.description }))] }), strategy === 'runtime' && (_jsxs(Alert, { variant: "warning", children: [_jsx(AlertTitle, { children: tI18nComplete.raw('text4fb8319405f3') }), _jsx(AlertDescription, { children: tI18nComplete.raw('textdb71b63b5778') })] })), grantNotice && (_jsxs(InfoBanner, { tone: "warning", icon: _jsx(DangerTriangleSolid, { weight: "fill" }), title: grantNotice.title, className: "[&_[data-slot=alert-content]]:min-w-0", children: [_jsx("span", { className: "block text-pretty", children: grantNotice.body }), grantPlan.candidates.length > 0 && (_jsxs("div", { className: "mt-3 flex flex-wrap items-center gap-2", children: [grantPlan.candidates.length > 1 && (_jsxs(Select, { value: selectedGrantAgent ?? '', onValueChange: setGrantAgent, disabled: grant.isPending, children: [_jsx(SelectTrigger, { "aria-label": tI18nComplete.raw('text33e745179bcb'), className: "h-8 w-48", children: _jsx(SelectValue, { placeholder: tI18nComplete.raw('textb6890bc23a9c') }) }), _jsx(SelectContent, { children: grantPlan.candidates.map((candidate) => (_jsx(SelectItem, { value: candidate.name, description: agentGrantCandidateHint(candidate) ?? undefined, children: candidate.name }, candidate.name))) })] })), _jsxs(Button, { type: "button", size: "sm", onClick: startGrant, disabled: !selectedGrantAgent || grant.isPending, children: [grant.isPending ? _jsx(Loading, { className: "size-3.5 shrink-0" }) : null, agentGrantActionLabel(grantPlan, selectedGrantAgent)] })] })), grantPlan.candidates.length === 0 && (_jsx("span", { className: "text-muted-foreground mt-2 block text-xs text-pretty", children: tI18nComplete.raw('textf23c70160b5b') })), selectedGrantHint && (_jsx("span", { className: "text-muted-foreground mt-2 block text-xs text-pretty", children: selectedGrantHint })), grant.error && (_jsx("span", { className: "text-kortix-red mt-2 block text-xs text-pretty", children: agentGrantErrorMessage(grant.error) })), _jsx("pre", { className: "border-border bg-muted mt-2 overflow-x-auto rounded-sm border p-2 font-mono text-xs leading-relaxed", children: grantManifest })] })), needsHosts && (_jsxs("div", { className: "border-border bg-sidebar space-y-4 rounded-md border p-3", children: [_jsxs("div", { className: "space-y-1", children: [_jsx("p", { className: "text-sm font-medium", children: tI18nComplete.raw('textd3297c00f7a2') }), _jsx("p", { className: "text-muted-foreground text-xs text-pretty", children: tI18nComplete.raw('text76f48924ee10') })] }), _jsxs(InfoBanner, { tone: "neutral", title: echoNotice.title, className: "[&_[data-slot=alert-content]]:min-w-0", children: [_jsx("span", { className: "block text-pretty", children: echoNotice.body }), _jsx("pre", { className: "border-border bg-muted mt-2 overflow-x-auto rounded-sm border p-2 font-mono text-xs leading-relaxed", children: echoNotice.probe }), _jsx(Link, { href: echoNotice.docsHref, target: "_blank", rel: "noreferrer", className: "text-muted-foreground hover:text-foreground mt-2 inline-block text-xs underline underline-offset-2", children: echoNotice.docsLabel })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "secret-dialog-hosts", children: tI18nComplete.raw('textbba9af131107') }), _jsx(Textarea, { id: "secret-dialog-hosts", value: hosts, onChange: (event) => setEditedHosts(event.target.value), placeholder: tI18nComplete.raw('text07de8e6e567f'), minHeight: 56, maxHeight: 112, variant: "outline", className: "font-mono text-xs", disabled: save.isPending }), _jsx(FieldDescription, { children: tI18nComplete.raw('text80befc2e0620') })] }), legacyDetail && (_jsxs("div", { className: "border-border bg-popover space-y-2 rounded-md border p-3", children: [_jsx("p", { className: "text-sm font-medium", children: legacyDetail.title }), _jsx("p", { className: "text-muted-foreground text-xs text-pretty", children: legacyDetail.body }), _jsx("dl", { className: "text-muted-foreground space-y-0.5 font-mono text-xs", children: legacyDetail.lines.map((line) => (_jsx("dd", { children: line }, line))) }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setLegacyInject(null), disabled: save.isPending, children: tI18nComplete.raw('text4a72652fb7e7') })] }))] })), row?.consumer === 'connector' && (_jsx("div", { className: "border-border bg-sidebar space-y-4 rounded-md border p-3", children: _jsxs(Field, { children: [_jsx(FieldLabel, { children: tI18nComplete.raw('textc3d2e79ebdd0') }), connectorsLoading ? (_jsxs("div", { className: "space-y-2", children: [_jsx(Skeleton, { className: "h-12 rounded-md" }), _jsx(Skeleton, { className: "h-12 rounded-md" })] })) : connectorOptions.length === 0 ? (_jsx(InfoBanner, { tone: "neutral", title: tI18nComplete.raw('text10798ef769fd'), children: tI18nComplete.raw('text0b834e60371f') })) : (_jsx("div", { className: "bg-popover overflow-hidden rounded-md border", children: connectorOptions.map((option, index) => (_jsxs("label", { className: cn('flex min-h-12 items-center gap-3 px-3 py-2', index > 0 && 'border-t', option.disabled ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'), children: [_jsx(Checkbox, { checked: effectiveSelectedConnectorSlugSet.has(option.slug), disabled: option.disabled || save.isPending, onCheckedChange: (checked) => {
|
|
670
671
|
setSelectedConnectorSlugs((current) => checked
|
|
671
672
|
? [
|
|
672
673
|
...new Set([
|
|
@@ -34,7 +34,7 @@ export interface GeneralTabViewProps {
|
|
|
34
34
|
* `useQuery`/mutations, so it can't render under `renderToStaticMarkup`
|
|
35
35
|
* with no providers. */
|
|
36
36
|
/** Whether the Delete-workspace section shows at all — gated on
|
|
37
|
-
* `project.delete`, NOT the broader `project.write` gate
|
|
37
|
+
* `project.delete`, NOT the broader `project.settings.write` gate
|
|
38
38
|
* `generalFieldsSlot`'s fields use. Deleting and editing are separate
|
|
39
39
|
* permissions, so a custom role can grant one without the other. */
|
|
40
40
|
canDelete?: boolean;
|
|
@@ -44,6 +44,13 @@ export interface GeneralTabViewProps {
|
|
|
44
44
|
onCloseArchiveDialog?: () => void;
|
|
45
45
|
onConfirmArchive?: () => void;
|
|
46
46
|
isArchivePending?: boolean;
|
|
47
|
+
/** `TransferWorkspaceEntry` — the second danger-zone row and its dialog.
|
|
48
|
+
* A slot for the same reason the others are: the dialog owns its own
|
|
49
|
+
* accounts query and mutation, so it cannot render under
|
|
50
|
+
* `renderToStaticMarkup` with no providers. Shares the Delete section's
|
|
51
|
+
* `canDelete` gate — the API's source gate for a transfer is the same
|
|
52
|
+
* destructive tier that gates delete. */
|
|
53
|
+
transferSlot?: ReactNode;
|
|
47
54
|
}
|
|
48
55
|
export interface GeneralTabCopy {
|
|
49
56
|
loadFailed: string;
|
|
@@ -64,7 +71,7 @@ export interface GeneralTabCopy {
|
|
|
64
71
|
* `ProfileTabView`/`ConnectedAccountsTabView` for the same split. Every
|
|
65
72
|
* prop is optional with a safe default so the bare `<GeneralTabView />`
|
|
66
73
|
* the test file renders shows the Delete-workspace section fully formed. */
|
|
67
|
-
export declare function GeneralTabView({ copy, isLoading, isError, errorMessage, onRetry, generalFieldsSlot, canDelete, workspaceName, archiveOpen, onOpenArchiveDialog, onCloseArchiveDialog, onConfirmArchive, isArchivePending, }: GeneralTabViewProps): import("react").JSX.Element;
|
|
74
|
+
export declare function GeneralTabView({ copy, isLoading, isError, errorMessage, onRetry, generalFieldsSlot, canDelete, workspaceName, archiveOpen, onOpenArchiveDialog, onCloseArchiveDialog, onConfirmArchive, isArchivePending, transferSlot, }: GeneralTabViewProps): import("react").JSX.Element;
|
|
68
75
|
export interface RunProjectArchiveClient {
|
|
69
76
|
archiveProject: (projectId: string) => Promise<unknown>;
|
|
70
77
|
}
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
import { useEffect, useRef, useState } from 'react';
|
|
4
4
|
import { Button } from '../../../primitives/button.js';
|
|
5
5
|
import { Input } from '../../../primitives/input.js';
|
|
6
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../../primitives/select.js';
|
|
6
7
|
import { SettingsRow, SettingsRowGroup } from '../../../primitives/settings-row.js';
|
|
7
8
|
import { SettingsSubsectionHeader } from '../../../primitives/settings-subsection-header.js';
|
|
8
9
|
import { Skeleton } from '../../../primitives/skeleton.js';
|
|
@@ -19,17 +20,30 @@ import { forgetLastProjectId } from '../../../internal/lib/onboarding/last-proje
|
|
|
19
20
|
import { PROJECT_ACTIONS } from '../../../internal/lib/project-actions.js';
|
|
20
21
|
import { useProjectCans } from '../../../internal/lib/use-project-can.js';
|
|
21
22
|
import { useSettingsPanelStore } from '../../../internal/stores/settings-panel-store.js';
|
|
22
|
-
import {
|
|
23
|
+
import { TransferWorkspaceEntry } from './transfer-workspace-dialog.js';
|
|
24
|
+
import { archiveProject, getProject, setDefaultReasoningEffort, updateProject, } from '@kortix/sdk';
|
|
23
25
|
import { contract, invalidateProjectIdentity, qk } from '@kortix/sdk/react';
|
|
24
26
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
25
27
|
import { useUiRouter, useUiViewerId } from '../../../providers/ui-provider.js';
|
|
26
28
|
import { SettingsTabHeader } from '../settings-tab-header.js';
|
|
27
|
-
/** Asked for in ONE batched probe. `project.delete` and `project.write` are
|
|
29
|
+
/** Asked for in ONE batched probe. `project.delete` and `project.settings.write` are
|
|
28
30
|
* separate permissions — see `ProjectSettingsGeneralProps.canDelete`. */
|
|
29
31
|
const GENERAL_TAB_ACTIONS = [
|
|
30
|
-
PROJECT_ACTIONS.
|
|
32
|
+
PROJECT_ACTIONS.PROJECT_SETTINGS_WRITE,
|
|
31
33
|
PROJECT_ACTIONS.PROJECT_DELETE,
|
|
32
34
|
];
|
|
35
|
+
/** The effort ladder the default-effort picker offers. These are models.dev
|
|
36
|
+
* variant ids — technical values, shown capitalized like the composer's
|
|
37
|
+
* own effort selector does (its `label`), never translated.
|
|
38
|
+
*
|
|
39
|
+
* The API clamps the stored default at delivery to what the session's model
|
|
40
|
+
* actually publishes (`effortVariantForPrompt`), so a pick the session's
|
|
41
|
+
* model does not carry degrades to Automatic rather than breaking the turn;
|
|
42
|
+
* the picker still offers the superset the catalog carries. */
|
|
43
|
+
const DEFAULT_EFFORT_OPTIONS = ['none', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
44
|
+
const DEFAULT_EFFORT_AUTOMATIC = '';
|
|
45
|
+
/** `medium` → `Medium` — same casing rule as the composer's effort selector. */
|
|
46
|
+
const effortOptionLabel = (value) => value.charAt(0).toUpperCase() + value.slice(1);
|
|
33
47
|
export const DELETE_WORKSPACE_CONSEQUENCES = [
|
|
34
48
|
'Every session in this workspace, with its files, history, and outputs',
|
|
35
49
|
'All scheduled runs and triggers — they stop firing straight away',
|
|
@@ -61,8 +75,8 @@ const DEFAULT_GENERAL_COPY = {
|
|
|
61
75
|
* `ProfileTabView`/`ConnectedAccountsTabView` for the same split. Every
|
|
62
76
|
* prop is optional with a safe default so the bare `<GeneralTabView />`
|
|
63
77
|
* the test file renders shows the Delete-workspace section fully formed. */
|
|
64
|
-
export function GeneralTabView({ copy = DEFAULT_GENERAL_COPY, isLoading = false, isError = false, errorMessage = '', onRetry = () => { }, generalFieldsSlot, canDelete = true, workspaceName = '', archiveOpen = false, onOpenArchiveDialog = () => { }, onCloseArchiveDialog = () => { }, onConfirmArchive = () => { }, isArchivePending = false, }) {
|
|
65
|
-
return (_jsxs("div", { className: "mx-auto w-full max-w-2xl space-y-8", children: [_jsx(SettingsTabHeader, { tab: "general" }), isLoading ? (_jsxs("div", { className: "space-y-5", children: [_jsx(Skeleton, { className: "h-40 rounded-md" }), _jsx(Skeleton, { className: "h-20 rounded-md" })] })) : isError ? (_jsx(ErrorState, { size: "sm", title: copy.loadFailed, description: errorMessage, action: _jsx(Button, { variant: "outline", size: "sm", onClick: onRetry, children: copy.retry }) })) : (_jsxs(_Fragment, { children: [generalFieldsSlot, canDelete ? (_jsxs("section", { className: "space-y-3", children: [_jsx(SettingsSubsectionHeader, { title: copy.dangerZone }),
|
|
78
|
+
export function GeneralTabView({ copy = DEFAULT_GENERAL_COPY, isLoading = false, isError = false, errorMessage = '', onRetry = () => { }, generalFieldsSlot, canDelete = true, workspaceName = '', archiveOpen = false, onOpenArchiveDialog = () => { }, onCloseArchiveDialog = () => { }, onConfirmArchive = () => { }, isArchivePending = false, transferSlot, }) {
|
|
79
|
+
return (_jsxs("div", { className: "mx-auto w-full max-w-2xl space-y-8", children: [_jsx(SettingsTabHeader, { tab: "general" }), isLoading ? (_jsxs("div", { className: "space-y-5", children: [_jsx(Skeleton, { className: "h-40 rounded-md" }), _jsx(Skeleton, { className: "h-20 rounded-md" })] })) : isError ? (_jsx(ErrorState, { size: "sm", title: copy.loadFailed, description: errorMessage, action: _jsx(Button, { variant: "outline", size: "sm", onClick: onRetry, children: copy.retry }) })) : (_jsxs(_Fragment, { children: [generalFieldsSlot, canDelete ? (_jsxs("section", { className: "space-y-3", children: [_jsx(SettingsSubsectionHeader, { title: copy.dangerZone }), _jsxs(SettingsRowGroup, { children: [_jsx(SettingsRow, { label: copy.deleteWorkspace, description: copy.deleteDescription, children: _jsx(Button, { variant: "ghost", size: "sm", className: "text-destructive hover:bg-destructive/10 hover:text-destructive", onClick: onOpenArchiveDialog, children: copy.deleteWorkspace }) }), transferSlot] })] })) : null] })), _jsx(TypeToConfirmDialog, { open: archiveOpen, onOpenChange: (open) => {
|
|
66
80
|
if (!open)
|
|
67
81
|
onCloseArchiveDialog();
|
|
68
82
|
}, title: copy.deleteTitle, description: copy.deleteDialog(workspaceName), consequencesTitle: copy.consequencesTitle, consequences: copy.consequences, reassurance: copy.reassurance,
|
|
@@ -125,7 +139,7 @@ export function withoutProject(data, projectId) {
|
|
|
125
139
|
* `GeneralProjectCard` (name — see this file's header comment for what's
|
|
126
140
|
* byte-identical) and extended with icon editing, reusing `ProjectIconField`
|
|
127
141
|
* / `buildProjectEditPatch` unmodified — see this file's header comment. */
|
|
128
|
-
function GeneralWorkspaceCard({ project, canManage, }) {
|
|
142
|
+
function GeneralWorkspaceCard({ project, canManage, canManageEffort, }) {
|
|
129
143
|
const tI18nComplete = useTranslations('hardcodedUi.i18nComplete');
|
|
130
144
|
const t = useTranslations('settings.workspace');
|
|
131
145
|
const queryClient = useQueryClient();
|
|
@@ -157,6 +171,13 @@ function GeneralWorkspaceCard({ project, canManage, }) {
|
|
|
157
171
|
onSettled: () => renameOnSettled(queryClient, project.project_id),
|
|
158
172
|
});
|
|
159
173
|
const { mutate: mutateName, isPending: isRenamePending } = renameMutation;
|
|
174
|
+
const effortMutation = useMutation({
|
|
175
|
+
mutationFn: (effort) => setDefaultReasoningEffort(project.project_id, effort),
|
|
176
|
+
onSuccess: (updated) => {
|
|
177
|
+
queryClient.setQueryData(qk.project.summary(project.project_id), updated);
|
|
178
|
+
},
|
|
179
|
+
onError: (error) => errorToast(error.message || t('updateFailed')),
|
|
180
|
+
});
|
|
160
181
|
useEffect(() => {
|
|
161
182
|
if (!canManage || isRenamePending)
|
|
162
183
|
return;
|
|
@@ -215,7 +236,7 @@ function GeneralWorkspaceCard({ project, canManage, }) {
|
|
|
215
236
|
// Not `variant="popover"` any more: the group it now sits in is
|
|
216
237
|
// itself `bg-popover`, so a popover-tinted input would vanish into
|
|
217
238
|
// it. The default `bg-input` is what makes it read as a control.
|
|
218
|
-
className: "h-8 w-56" })] })] }));
|
|
239
|
+
className: "h-8 w-56" })] }), _jsx(SettingsRow, { label: t('defaultEffort'), description: t('defaultEffortDescription'), children: _jsxs(Select, { value: project.default_reasoning_effort ?? DEFAULT_EFFORT_AUTOMATIC, onValueChange: (v) => effortMutation.mutate(v === DEFAULT_EFFORT_AUTOMATIC ? null : v), disabled: !canManageEffort || effortMutation.isPending, children: [_jsx(SelectTrigger, { "aria-label": t('defaultEffort'), className: "h-8 w-40 shrink-0", children: _jsx(SelectValue, { placeholder: t('defaultEffortAutomatic') }) }), _jsxs(SelectContent, { align: "end", children: [_jsx(SelectItem, { value: DEFAULT_EFFORT_AUTOMATIC, children: t('defaultEffortAutomatic') }), DEFAULT_EFFORT_OPTIONS.map((effort) => (_jsx(SelectItem, { value: effort, children: effortOptionLabel(effort) }, effort)))] })] }) })] }));
|
|
219
240
|
}
|
|
220
241
|
/** Container: owns every hook (React Query, IAM probe, archive dialog state)
|
|
221
242
|
* and renders `GeneralTabView` with real data + slots. Only ever mounted
|
|
@@ -235,12 +256,13 @@ export function GeneralTab({ projectId }) {
|
|
|
235
256
|
});
|
|
236
257
|
const project = projectQuery.data;
|
|
237
258
|
// Two DIFFERENT permissions, asked for separately. `project.delete` gates the
|
|
238
|
-
// Delete-workspace section; `project.write` gates the fields. Reading a
|
|
259
|
+
// Delete-workspace section; `project.settings.write` gates the fields. Reading a
|
|
239
260
|
// `manager` role label instead would have hidden Delete from a custom role
|
|
240
261
|
// that holds `project.delete`, and shown it to one that does not.
|
|
241
262
|
const caps = useProjectCans(projectId, GENERAL_TAB_ACTIONS);
|
|
242
263
|
const canDelete = caps[PROJECT_ACTIONS.PROJECT_DELETE]?.allowed === true;
|
|
243
|
-
const canEdit = caps[PROJECT_ACTIONS.
|
|
264
|
+
const canEdit = caps[PROJECT_ACTIONS.PROJECT_SETTINGS_WRITE]?.allowed === true;
|
|
265
|
+
const canEditEffort = canEdit;
|
|
244
266
|
const archiveMutation = useMutation({
|
|
245
267
|
mutationFn: () => runProjectArchive(projectId, { archiveProject },
|
|
246
268
|
// The archived project must stop being where `/` and the settings
|
|
@@ -283,8 +305,8 @@ export function GeneralTab({ projectId }) {
|
|
|
283
305
|
],
|
|
284
306
|
reassurance: t('reassurance'),
|
|
285
307
|
keepWorkspace: t('keepWorkspace'),
|
|
286
|
-
}, isLoading: projectQuery.isLoading, isError: projectQuery.isError, errorMessage: projectQuery.error?.message ?? '', onRetry: () => projectQuery.refetch(), generalFieldsSlot: project ? _jsx(GeneralWorkspaceCard, { project: project, canManage: canEdit }) : undefined,
|
|
308
|
+
}, isLoading: projectQuery.isLoading, isError: projectQuery.isError, errorMessage: projectQuery.error?.message ?? '', onRetry: () => projectQuery.refetch(), generalFieldsSlot: project ? _jsx(GeneralWorkspaceCard, { project: project, canManage: canEdit, canManageEffort: canEditEffort }) : undefined,
|
|
287
309
|
// No git-repo slot: Git repo is its own Settings section since
|
|
288
310
|
// 2026-09-03 (`project-settings-sections.ts`).
|
|
289
|
-
canDelete: canDelete, workspaceName: project?.name, archiveOpen: archiveOpen, onOpenArchiveDialog: () => setArchiveOpen(true), onCloseArchiveDialog: () => setArchiveOpen(false), onConfirmArchive: () => archiveMutation.mutate(), isArchivePending: archiveMutation.isPending }));
|
|
311
|
+
canDelete: canDelete, workspaceName: project?.name, archiveOpen: archiveOpen, onOpenArchiveDialog: () => setArchiveOpen(true), onCloseArchiveDialog: () => setArchiveOpen(false), onConfirmArchive: () => archiveMutation.mutate(), isArchivePending: archiveMutation.isPending, transferSlot: _jsx(TransferWorkspaceEntry, { projectId: projectId, currentAccountId: project?.account_id }) }));
|
|
290
312
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* Transfer-workspace entry point — the settings row and its dialog.
|
|
5
|
+
*
|
|
6
|
+
* The API (POST /v1/projects/:id/transfer) refuses a caller who holds the
|
|
7
|
+
* destructive tier on the project but no rights on the TARGET organization,
|
|
8
|
+
* and answers with the target's project cap when the move would exceed its
|
|
9
|
+
* plan — so this dialog only has to pick a target and say plainly what
|
|
10
|
+
* moves: sessions, files, settings and member grants follow the project;
|
|
11
|
+
* the usage ledger stays with the organization that consumed it.
|
|
12
|
+
*
|
|
13
|
+
* The project may land in an organization the mover cannot see afterwards
|
|
14
|
+
* (they are not a member there) — the same outcome the Delete flow produces,
|
|
15
|
+
* so success leaves the project the same way: forget the remembered landing
|
|
16
|
+
* target, close the settings overlay, replace into the landing door.
|
|
17
|
+
*/
|
|
18
|
+
import { useMemo, useState } from 'react';
|
|
19
|
+
import { Button } from '../../../primitives/button.js';
|
|
20
|
+
import { ConfirmDialog } from '../../../primitives/confirm-dialog.js';
|
|
21
|
+
import { SettingsRow } from '../../../primitives/settings-row.js';
|
|
22
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '../../../primitives/select.js';
|
|
23
|
+
import { errorToast, successToast } from '../../../primitives/toast.js';
|
|
24
|
+
import { useSettingsPanelStore } from '../../../internal/stores/settings-panel-store.js';
|
|
25
|
+
import { useUiRouter, useUiViewerId } from '../../../providers/ui-provider.js';
|
|
26
|
+
import { useAccountsList } from '../../../internal/hooks/account/use-accounts-list.js';
|
|
27
|
+
import { PROJECT_LANDING_PATH } from '../../../internal/lib/onboarding/landing-destination.js';
|
|
28
|
+
import { forgetLastProjectId } from '../../../internal/lib/onboarding/last-project-cookie.js';
|
|
29
|
+
import { transferProject } from '@kortix/sdk';
|
|
30
|
+
import { qk } from '@kortix/sdk/react';
|
|
31
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
32
|
+
import { useTranslations } from '../../../i18n/index.js';
|
|
33
|
+
export function TransferWorkspaceEntry({ projectId, currentAccountId, }) {
|
|
34
|
+
const t = useTranslations('settings.workspace');
|
|
35
|
+
const [open, setOpen] = useState(false);
|
|
36
|
+
return (_jsxs(_Fragment, { children: [_jsx(SettingsRow, { label: t('transferWorkspace'), description: t('transferDescription'), children: _jsx(Button, { variant: "ghost", size: "sm", className: "text-destructive hover:bg-destructive/10 hover:text-destructive", onClick: () => setOpen(true), children: t('transferWorkspace') }) }), _jsx(TransferWorkspaceDialog, { projectId: projectId, currentAccountId: currentAccountId, open: open, onOpenChange: setOpen })] }));
|
|
37
|
+
}
|
|
38
|
+
function TransferWorkspaceDialog({ projectId, currentAccountId, open, onOpenChange, }) {
|
|
39
|
+
const t = useTranslations('settings.workspace');
|
|
40
|
+
const queryClient = useQueryClient();
|
|
41
|
+
const router = useUiRouter();
|
|
42
|
+
const viewerId = useUiViewerId();
|
|
43
|
+
const [targetId, setTargetId] = useState('');
|
|
44
|
+
// Same freshness contract as every other account-list reader: fetch only
|
|
45
|
+
// while the dialog is open.
|
|
46
|
+
const accountsQuery = useAccountsList({ enabled: open });
|
|
47
|
+
// The target cannot be the account the project already lives in — the API
|
|
48
|
+
// refuses that anyway — and a stale `currentAccountId` (project query still
|
|
49
|
+
// in flight) is harmless: the API re-checks.
|
|
50
|
+
const targetOptions = useMemo(() => (accountsQuery.data ?? []).filter((account) => account.account_id !== currentAccountId), [accountsQuery.data, currentAccountId]);
|
|
51
|
+
const transferMutation = useMutation({
|
|
52
|
+
mutationFn: () => transferProject(projectId, targetId),
|
|
53
|
+
onSuccess: () => {
|
|
54
|
+
successToast(t('transferSuccess'));
|
|
55
|
+
// Same leave-the-project flow as Delete: the moved project must stop
|
|
56
|
+
// being the remembered landing target, the global overlay must close
|
|
57
|
+
// before the navigation lands, and Back must not return to a project
|
|
58
|
+
// the viewer may no longer see.
|
|
59
|
+
forgetLastProjectId(viewerId, projectId);
|
|
60
|
+
useSettingsPanelStore.getState().close();
|
|
61
|
+
// qk.projects.scope(): lists across every account plus the accountless
|
|
62
|
+
// slot — the project just changed accounts, so every list form is
|
|
63
|
+
// stale at once. The moved project's own caches are unreachable after
|
|
64
|
+
// the replace below (the old id's entries gcTime out).
|
|
65
|
+
queryClient.invalidateQueries({ queryKey: qk.projects.scope() });
|
|
66
|
+
onOpenChange(false);
|
|
67
|
+
router.replace(PROJECT_LANDING_PATH);
|
|
68
|
+
},
|
|
69
|
+
onError: (error) => errorToast(error.message || t('transferFailed')),
|
|
70
|
+
});
|
|
71
|
+
const targetName = targetOptions.find((account) => account.account_id === targetId)?.name;
|
|
72
|
+
const changeOpen = (next) => {
|
|
73
|
+
if (transferMutation.isPending)
|
|
74
|
+
return;
|
|
75
|
+
if (!next) {
|
|
76
|
+
setTargetId('');
|
|
77
|
+
transferMutation.reset();
|
|
78
|
+
}
|
|
79
|
+
onOpenChange(next);
|
|
80
|
+
};
|
|
81
|
+
return (_jsx(ConfirmDialog, { open: open, onOpenChange: changeOpen, title: t('transferTitle'), description: _jsxs("div", { className: "space-y-4", children: [_jsx("p", { children: t('transferDialog') }), accountsQuery.isLoading ? (_jsx("p", { children: t('transferLoadingTargets') })) : accountsQuery.isError ? (_jsx(Button, { variant: "outline", onClick: () => accountsQuery.refetch(), children: t('retry') })) : targetOptions.length === 0 ? (_jsx("p", { children: t('transferNoTargets') })) : (_jsxs(Select, { value: targetId, onValueChange: setTargetId, children: [_jsx(SelectTrigger, { className: "w-full", "aria-label": t('transferTargetLabel'), children: _jsx(SelectValue, { placeholder: t('transferTargetPlaceholder') }) }), _jsx(SelectContent, { children: targetOptions.map((account) => (_jsx(SelectItem, { value: account.account_id, children: account.name }, account.account_id))) })] }))] }), cancelLabel: t('keepWorkspace'), confirmLabel: targetName ? t('transferMoveTo', { name: targetName }) : t('transferMove'), confirmVariant: "destructive", confirmDisabled: !targetName || accountsQuery.isError, isPending: transferMutation.isPending, onConfirm: () => transferMutation.mutate() }));
|
|
82
|
+
}
|
|
@@ -194,12 +194,13 @@ export async function readFile(projectId, ref, filePath) {
|
|
|
194
194
|
// text, and the JSON content read is byte-safe for it.
|
|
195
195
|
if (hasExtension && getLanguageFromExt(name) !== 'plaintext') {
|
|
196
196
|
const result = await readProjectFile(projectId, relativePath, ref);
|
|
197
|
-
return { type: 'text', content: result.content };
|
|
197
|
+
return { type: 'text', content: result.content, revision: result.revision };
|
|
198
198
|
}
|
|
199
199
|
// Unknown or missing extension: the bytes decide, text or binary.
|
|
200
200
|
const bytes = await readRawBytes(projectId, relativePath, ref);
|
|
201
201
|
if (isTextBytes(bytes)) {
|
|
202
|
-
|
|
202
|
+
const result = await readProjectFile(projectId, relativePath, ref);
|
|
203
|
+
return { type: 'text', content: result.content, revision: result.revision, mimeType: 'text/plain; charset=utf-8' };
|
|
203
204
|
}
|
|
204
205
|
return { type: 'binary', content: '', mimeType: 'application/octet-stream' };
|
|
205
206
|
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
3
|
+
import { writeProjectFile } from '@kortix/sdk';
|
|
4
|
+
import { fileContentKeys } from './hooks/use-file-content.js';
|
|
5
|
+
import { useProjectCan } from '../../internal/lib/use-project-can.js';
|
|
6
|
+
import { PROJECT_ACTIONS } from '../../internal/lib/project-actions.js';
|
|
2
7
|
import { useMemo } from 'react';
|
|
3
8
|
import { downloadFile, uploadFile } from './api/runtime-files.js';
|
|
4
9
|
import { FilePathBreadcrumbs } from './components/file-breadcrumbs.js';
|
|
@@ -16,6 +21,8 @@ import { useBinaryBlob } from './hooks/use-binary-blob.js';
|
|
|
16
21
|
*/
|
|
17
22
|
export function useProjectFileSource() {
|
|
18
23
|
const ctx = useProjectContext();
|
|
24
|
+
const queryClient = useQueryClient();
|
|
25
|
+
const canWrite = useProjectCan(ctx?.projectId ?? '', PROJECT_ACTIONS.PROJECT_FILE_WRITE).allowed === true;
|
|
19
26
|
return useMemo(() => ({
|
|
20
27
|
id: 'project-ref',
|
|
21
28
|
useFileContent,
|
|
@@ -25,6 +32,16 @@ export function useProjectFileSource() {
|
|
|
25
32
|
? downloadFile(ctx.projectId, ctx.ref, filePath, fileName)
|
|
26
33
|
: Promise.reject(new Error('No project context for download')),
|
|
27
34
|
upload: uploadFile,
|
|
35
|
+
editablePreview: canWrite,
|
|
36
|
+
save: ctx && canWrite ? async (filePath, file, revision) => {
|
|
37
|
+
if (!revision)
|
|
38
|
+
throw new Error('Reload the file before saving.');
|
|
39
|
+
await writeProjectFile(ctx.projectId, {
|
|
40
|
+
path: filePath.replace(/^\/workspace\//, '').replace(/^\//, ''), ref: ctx.ref,
|
|
41
|
+
content: await file.text(), expected_revision: revision,
|
|
42
|
+
});
|
|
43
|
+
await queryClient.invalidateQueries({ queryKey: fileContentKeys.all });
|
|
44
|
+
} : undefined,
|
|
28
45
|
Breadcrumbs: FilePathBreadcrumbs,
|
|
29
|
-
}), [ctx]);
|
|
46
|
+
}), [ctx, canWrite, queryClient]);
|
|
30
47
|
}
|
|
@@ -20,6 +20,8 @@ export function useFileContent(filePath, options) {
|
|
|
20
20
|
staleTime: options?.staleTime ?? 10_000,
|
|
21
21
|
gcTime: 5 * 60_000,
|
|
22
22
|
refetchOnWindowFocus: false,
|
|
23
|
+
// A revision used for edits must remain paired with the displayed text.
|
|
24
|
+
refetchOnMount: false,
|
|
23
25
|
retry: (failureCount, error) => {
|
|
24
26
|
const msg = error.message.toLowerCase();
|
|
25
27
|
if (msg.includes('404') ||
|
|
@@ -9,7 +9,7 @@ import { cn } from '../../lib/utils.js';
|
|
|
9
9
|
import { WarningIcon as AlertTriangle, BracketsCurlyIcon as Braces, CheckIcon as Check, WarningCircleIcon as CircleAlert, CodeIcon as Code, EyeIcon as Eye, GlobeIcon as Globe, ArrowCounterClockwiseIcon as RotateCcw, FloppyDiskIcon as Save } from '@phosphor-icons/react';
|
|
10
10
|
import { Download } from '../../internal/icons/download.js';
|
|
11
11
|
export function FileHeader({ showHeader, Breadcrumbs, filePath, readOnly, hasUnsavedChanges, saveFlash, fileDiagErrorCount, fileDiagWarningCount, fileContent, isSaving, handleSave, latestContentRef, discardLabel, handleDiscard, isHtmlFile, isHtmlPreview, setIsHtmlPreview, isJsonFile, isJsonTreeView, setIsJsonTreeView, hasPreviewToggle, isMarkdownPreview, setIsMarkdownPreview, headerActions, handleDownload, blobUrl, rawBlob, tI18nHardcoded, tHardcodedUi }) {
|
|
12
|
-
return (_jsx(_Fragment, { children: showHeader && (_jsxs("div", { className: "border-border/50 flex h-10 shrink-0 items-center gap-2 border-b px-3 py-1.5", children: [_jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-1", children: [Breadcrumbs && _jsx(Breadcrumbs, { filePath: filePath }), !readOnly && hasUnsavedChanges && (_jsxs(Badge, { variant: "warning", size: "sm", className: "shrink-0", children: [_jsx(StatusDot, { tone: "warning", pulse: true }), tI18nHardcoded.raw('i18nComplete.text7117f0807129')] })), !readOnly && saveFlash && !hasUnsavedChanges && (_jsxs(Badge, { variant: "success", size: "sm", className: "shrink-0", children: [_jsx(Check, { className: "h-3 w-3" }), tI18nHardcoded.raw('i18nComplete.textb5c120b316c2')] })), readOnly && (_jsx(Badge, { variant: "muted", size: "sm", className: "shrink-0 tracking-wider uppercase", children: tHardcodedUi.raw('featuresFilesComponentsFileContentRenderer.line554JsxTextViewOnly') })), (fileDiagErrorCount > 0 || fileDiagWarningCount > 0) && (_jsxs("span", { className: "inline-flex shrink-0 items-center gap-1.5", children: [fileDiagErrorCount > 0 && (_jsxs("span", { className: "text-destructive inline-flex items-center gap-0.5 text-xs font-medium tabular-nums", children: [_jsx(CircleAlert, { className: "h-3 w-3" }), fileDiagErrorCount] })), fileDiagWarningCount > 0 && (_jsxs("span", { className: "text-kortix-orange inline-flex items-center gap-0.5 text-xs font-medium tabular-nums", children: [_jsx(AlertTriangle, { className: "h-3 w-3" }), fileDiagWarningCount] }))] }))] }), _jsxs("div", { className: "flex shrink-0 items-center gap-0.5", children: [!readOnly && hasUnsavedChanges && fileContent?.type === 'text' && (_jsxs(_Fragment, { children: [_jsxs(Button, { variant: "default", size: "sm", className: "h-7 gap-1.5 px-3 text-xs font-medium", onClick: () => handleSave(latestContentRef.current), disabled: isSaving, title: tHardcodedUi.raw('featuresFilesComponentsFileContentRenderer.line586JsxAttrTitleSaveS'), children: [isSaving ? (_jsx(Loading, { className: "h-3.5 w-3.5" })) : (_jsx(Save, { className: "h-3.5 w-3.5" })), tI18nHardcoded.raw('i18nComplete.text1509f561f241')] }), _jsx(Hint, { label: discardLabel, side: "bottom", children: _jsx(Button, { variant: "ghost", size: "icon", "aria-label": discardLabel, className: "text-muted-foreground hover:text-foreground h-7 w-7 active:scale-[0.96]", onClick: handleDiscard, children: _jsx(RotateCcw, { className: "h-3.5 w-3.5" }) }) })] })), isHtmlFile && (_jsx(Hint, { label: isHtmlPreview ? tI18nHardcoded.raw('i18nComplete.text6ee818aa2de3') : 'Preview', side: "bottom", children: _jsx(Button, { variant: "ghost", size: "icon", "aria-label": isHtmlPreview ? tI18nHardcoded.raw('i18nComplete.text6ee818aa2de3') : 'Preview', "aria-pressed": isHtmlPreview, className: cn('h-7 w-7 active:scale-[0.96]', isHtmlPreview && 'text-primary'), onClick: () => setIsHtmlPreview((v) => !v), children: isHtmlPreview ? _jsx(Code, { className: "h-4 w-4" }) : _jsx(Globe, { className: "h-4 w-4" }) }) })), isJsonFile && fileContent?.type === 'text' && (_jsx(Hint, { label: isJsonTreeView
|
|
12
|
+
return (_jsx(_Fragment, { children: showHeader && (_jsxs("div", { className: "border-border/50 flex h-10 shrink-0 items-center gap-2 border-b px-3 py-1.5", children: [_jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-1", children: [Breadcrumbs && _jsx(Breadcrumbs, { filePath: filePath }), !readOnly && hasUnsavedChanges && (_jsxs(Badge, { variant: "warning", size: "sm", className: "shrink-0", children: [_jsx(StatusDot, { tone: "warning", pulse: true }), tI18nHardcoded.raw('i18nComplete.text7117f0807129')] })), !readOnly && saveFlash && !hasUnsavedChanges && (_jsxs(Badge, { variant: "success", size: "sm", className: "shrink-0", children: [_jsx(Check, { className: "h-3 w-3" }), tI18nHardcoded.raw('i18nComplete.textb5c120b316c2')] })), readOnly && (_jsx(Badge, { variant: "muted", size: "sm", className: "shrink-0 tracking-wider uppercase", children: tHardcodedUi.raw('featuresFilesComponentsFileContentRenderer.line554JsxTextViewOnly') })), (fileDiagErrorCount > 0 || fileDiagWarningCount > 0) && (_jsxs("span", { className: "inline-flex shrink-0 items-center gap-1.5", children: [fileDiagErrorCount > 0 && (_jsxs("span", { className: "text-destructive inline-flex items-center gap-0.5 text-xs font-medium tabular-nums", children: [_jsx(CircleAlert, { className: "h-3 w-3" }), fileDiagErrorCount] })), fileDiagWarningCount > 0 && (_jsxs("span", { className: "text-kortix-orange inline-flex items-center gap-0.5 text-xs font-medium tabular-nums", children: [_jsx(AlertTriangle, { className: "h-3 w-3" }), fileDiagWarningCount] }))] }))] }), _jsxs("div", { className: "flex shrink-0 items-center gap-0.5", children: [!readOnly && hasUnsavedChanges && fileContent?.type === 'text' && (_jsxs(_Fragment, { children: [_jsxs(Button, { variant: "default", size: "sm", className: "h-7 gap-1.5 px-3 text-xs font-medium", onClick: () => { void handleSave(latestContentRef.current).catch(() => undefined); }, disabled: isSaving, title: tHardcodedUi.raw('featuresFilesComponentsFileContentRenderer.line586JsxAttrTitleSaveS'), children: [isSaving ? (_jsx(Loading, { className: "h-3.5 w-3.5" })) : (_jsx(Save, { className: "h-3.5 w-3.5" })), tI18nHardcoded.raw('i18nComplete.text1509f561f241')] }), _jsx(Hint, { label: discardLabel, side: "bottom", children: _jsx(Button, { variant: "ghost", size: "icon", "aria-label": discardLabel, className: "text-muted-foreground hover:text-foreground h-7 w-7 active:scale-[0.96]", onClick: handleDiscard, children: _jsx(RotateCcw, { className: "h-3.5 w-3.5" }) }) })] })), isHtmlFile && (_jsx(Hint, { label: isHtmlPreview ? tI18nHardcoded.raw('i18nComplete.text6ee818aa2de3') : 'Preview', side: "bottom", children: _jsx(Button, { variant: "ghost", size: "icon", "aria-label": isHtmlPreview ? tI18nHardcoded.raw('i18nComplete.text6ee818aa2de3') : 'Preview', "aria-pressed": isHtmlPreview, className: cn('h-7 w-7 active:scale-[0.96]', isHtmlPreview && 'text-primary'), onClick: () => setIsHtmlPreview((v) => !v), children: isHtmlPreview ? _jsx(Code, { className: "h-4 w-4" }) : _jsx(Globe, { className: "h-4 w-4" }) }) })), isJsonFile && fileContent?.type === 'text' && (_jsx(Hint, { label: isJsonTreeView
|
|
13
13
|
? tI18nHardcoded.raw('i18nComplete.text6ee818aa2de3')
|
|
14
14
|
: tI18nHardcoded.raw('i18nComplete.text4f50bda41e87'), side: "bottom", children: _jsx(Button, { variant: "ghost", size: "icon", "aria-label": isJsonTreeView
|
|
15
15
|
? tI18nHardcoded.raw('i18nComplete.text6ee818aa2de3')
|
|
@@ -60,4 +60,4 @@ export interface FilePreviewModalProps extends FilePreviewState {
|
|
|
60
60
|
* hit that path, which is why only the mouse appeared broken.)
|
|
61
61
|
* Z-index is dialog-depth aware so it always stacks above its host overlay.
|
|
62
62
|
*/
|
|
63
|
-
export declare function FilePreviewModal({ selectedFilePath, panelMode, filePathList, currentFileIndex, onClose, onNext, onPrev, source, HistoryContent, statusSlot, extraActions, shareContext, historyLabel, embedded, targetLine, }: FilePreviewModalProps): import("react").JSX.Element | null;
|
|
63
|
+
export declare function FilePreviewModal({ selectedFilePath, panelMode, filePathList, currentFileIndex, onClose: closePreview, onNext: nextPreview, onPrev: prevPreview, source, HistoryContent, statusSlot, extraActions, shareContext, historyLabel, embedded, targetLine, }: FilePreviewModalProps): import("react").JSX.Element | null;
|
|
@@ -54,9 +54,19 @@ const NO_REFRESH = {
|
|
|
54
54
|
* hit that path, which is why only the mouse appeared broken.)
|
|
55
55
|
* Z-index is dialog-depth aware so it always stacks above its host overlay.
|
|
56
56
|
*/
|
|
57
|
-
export function FilePreviewModal({ selectedFilePath, panelMode, filePathList, currentFileIndex, onClose, onNext, onPrev, source, HistoryContent, statusSlot, extraActions, shareContext, historyLabel = "History", embedded = false, targetLine = null, }) {
|
|
57
|
+
export function FilePreviewModal({ selectedFilePath, panelMode, filePathList, currentFileIndex, onClose: closePreview, onNext: nextPreview, onPrev: prevPreview, source, HistoryContent, statusSlot, extraActions, shareContext, historyLabel = "History", embedded = false, targetLine = null, }) {
|
|
58
58
|
const tI18nHardcoded = useTranslations("hardcodedUi");
|
|
59
59
|
const isOpen = panelMode === "viewer" && !!selectedFilePath;
|
|
60
|
+
const [dirty, setDirty] = useState(false);
|
|
61
|
+
const leave = (action) => {
|
|
62
|
+
if (dirty && !window.confirm('Discard unsaved changes?'))
|
|
63
|
+
return;
|
|
64
|
+
setDirty(false);
|
|
65
|
+
action();
|
|
66
|
+
};
|
|
67
|
+
const onClose = () => leave(closePreview);
|
|
68
|
+
const onNext = () => leave(nextPreview);
|
|
69
|
+
const onPrev = () => leave(prevPreview);
|
|
60
70
|
// Embedded viewers start inline (in the side panel); "Expand" pops them to
|
|
61
71
|
// the full-screen overlay. Non-embedded viewers are always full-screen.
|
|
62
72
|
const [expanded, setExpanded] = useState(false);
|
|
@@ -264,7 +274,7 @@ export function FilePreviewModal({ selectedFilePath, panelMode, filePathList, cu
|
|
|
264
274
|
: "Expand", side: "bottom", children: _jsx(Button, { variant: "ghost", size: "icon", "aria-label": expanded
|
|
265
275
|
? tI18nHardcoded.raw("i18nComplete.textde12e3c7c7a8")
|
|
266
276
|
: "Expand", "aria-pressed": expanded, className: "text-muted-foreground hover:text-foreground h-8 w-8 active:scale-[0.96]", onClick: () => setExpanded((v) => !v), children: expanded ? (_jsx(Minimize2, { className: "h-4 w-4" })) : (_jsx(Maximize2, { className: "h-4 w-4" })) }) })), _jsx(ButtonGroup, { className: "ml-1.5", children: _jsx(Hint, { label: tI18nHardcoded.raw("autoFeaturesFileViewerFilePreviewModalJsxAttrTitleClosea40d0510"), side: "bottom", children: _jsx(Button, { variant: "secondary", size: "icon-base", "aria-label": tI18nHardcoded.raw("autoFeaturesFileViewerFilePreviewModalJsxAttrTitleClosea40d0510"), className: "active:scale-[0.96]", onClick: onClose, children: _jsx(X, { className: "h-4 w-4" }) }) }) })] })] }));
|
|
267
|
-
const body = (_jsxs(_Fragment, { children: [_jsx("div", { className: "h-full w-full overflow-hidden px-3 pt-3 pb-2", children: _jsx(FileSourceProvider, { value: source, children: _jsx(FileContentRenderer, { filePath: selectedFilePath, reloadKey: reloadKey, showHeader:
|
|
277
|
+
const body = (_jsxs(_Fragment, { children: [_jsx("div", { className: "h-full w-full overflow-hidden px-3 pt-3 pb-2", children: _jsx(FileSourceProvider, { value: source, children: _jsx(FileContentRenderer, { filePath: selectedFilePath, reloadKey: reloadKey, onUnsavedChange: setDirty, showHeader: !!source.editablePreview, readOnly: !source.editablePreview, targetLine: targetLine, markdownPreview: markdownPreview, onMarkdownPreviewChange: setMarkdownPreview }) }) }), historyPath && (_jsx("div", { className: "bg-popover border-border/60 animate-in slide-in-from-bottom-4 fade-in-0 absolute right-4 bottom-4 z-30 overflow-hidden rounded-md border shadow-md duration-normal", children: _jsx(HistoryContent, { filePath: historyPath, onClose: () => setHistoryPath(null) }) }))] }));
|
|
268
278
|
const panelInner = (_jsxs(_Fragment, { children: [_jsx("div", { ref: toolbarRef, className: "border-border/40 bg-background/95 flex h-12 shrink-0 items-center gap-2 border-b px-3 backdrop-blur-sm", children: toolbar }), _jsx("div", { ref: contentRef, className: "relative min-h-0 flex-1 overflow-hidden", children: body })] }));
|
|
269
279
|
// Inline (embedded, not expanded): fill the relative parent — the side panel —
|
|
270
280
|
// instead of portaling to a full-screen overlay. No backdrop, no body lock.
|
|
@@ -5,6 +5,7 @@ import { type ComponentType, type ReactNode } from 'react';
|
|
|
5
5
|
* their own typed hooks without conversion.
|
|
6
6
|
*/
|
|
7
7
|
export interface FileContent {
|
|
8
|
+
revision?: string;
|
|
8
9
|
type: 'text' | 'binary';
|
|
9
10
|
content: string;
|
|
10
11
|
patch?: FilePatch;
|
|
@@ -79,7 +80,8 @@ export interface FileSource {
|
|
|
79
80
|
* instead of `upload`, so a source can refuse a save over a file someone
|
|
80
81
|
* changed since it was read (a conditional write) rather than clobber it.
|
|
81
82
|
*/
|
|
82
|
-
|
|
83
|
+
editablePreview?: boolean;
|
|
84
|
+
save?: (filePath: string, file: File, revision?: string) => Promise<unknown>;
|
|
83
85
|
/**
|
|
84
86
|
* Every byte the viewer shows comes from this source, which owns the
|
|
85
87
|
* authorization. Previewers that otherwise read a sandbox path themselves
|
|
@@ -61,6 +61,13 @@ export function useFileContentState({ filePath, onUnsavedChange, onSaved, readOn
|
|
|
61
61
|
const contentRevision = useContentRevision(fileContent);
|
|
62
62
|
const fetchRevision = useContentRevision(dataUpdatedAt || undefined);
|
|
63
63
|
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
|
64
|
+
// A refetch may replace server content while CodeEditor retains a dirty draft.
|
|
65
|
+
// CAS must compare against the revision that draft started from.
|
|
66
|
+
const draftRevisionRef = useRef(fileContent?.revision);
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (!hasUnsavedChanges)
|
|
69
|
+
draftRevisionRef.current = fileContent?.revision;
|
|
70
|
+
}, [hasUnsavedChanges, fileContent?.revision]);
|
|
64
71
|
const [isSaving, setIsSaving] = useState(false);
|
|
65
72
|
const [saveFlash, setSaveFlash] = useState(false);
|
|
66
73
|
// Tracks the latest editor content so we can save from the header button.
|
|
@@ -120,12 +127,12 @@ export function useFileContentState({ filePath, onUnsavedChange, onSaved, readOn
|
|
|
120
127
|
// HEIC conversion — converts the raw HEIC blob to a renderable JPEG URL
|
|
121
128
|
const { url: heicImageUrl, isConverting: heicConverting } = useHeicBlob(isHeicImage ? rawBlob : null, fileName);
|
|
122
129
|
const displayContent = fileContent?.content ?? '';
|
|
123
|
-
//
|
|
130
|
+
// Refresh clean content only; header Save must keep the visible dirty draft.
|
|
124
131
|
useEffect(() => {
|
|
125
|
-
if (
|
|
126
|
-
latestContentRef.current = fileContent
|
|
132
|
+
if (!hasUnsavedChanges) {
|
|
133
|
+
latestContentRef.current = fileContent?.content ?? '';
|
|
127
134
|
}
|
|
128
|
-
}, [fileContent?.content]);
|
|
135
|
+
}, [fileContent?.content, hasUnsavedChanges]);
|
|
129
136
|
// Reset state when the FILE changes — markdown defaults to rendered preview.
|
|
130
137
|
//
|
|
131
138
|
// `setIsMarkdownPreview` MUST NOT be a dependency here, and this effect must
|
|
@@ -167,15 +174,17 @@ export function useFileContentState({ filePath, onUnsavedChange, onSaved, readOn
|
|
|
167
174
|
// Save handler — called by CodeEditor (Cmd+S) and by the header Save button.
|
|
168
175
|
// When called from the header button we pass latestContentRef.current.
|
|
169
176
|
// When called from CodeEditor's Cmd+S, CodeEditor passes its own localContent.
|
|
177
|
+
const draftGeneration = useRef(0);
|
|
170
178
|
const handleSave = useCallback(async (content) => {
|
|
171
179
|
if (readOnly)
|
|
172
180
|
return;
|
|
181
|
+
const generation = draftGeneration.current;
|
|
173
182
|
setIsSaving(true);
|
|
174
183
|
try {
|
|
175
184
|
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
|
|
176
185
|
const file = new File([blob], fileName, { type: 'text/plain' });
|
|
177
186
|
if (source.save) {
|
|
178
|
-
await source.save(filePath, file);
|
|
187
|
+
await source.save(filePath, file, draftRevisionRef.current);
|
|
179
188
|
}
|
|
180
189
|
else {
|
|
181
190
|
const parentPath = filePath.substring(0, filePath.lastIndexOf('/'));
|
|
@@ -184,7 +193,15 @@ export function useFileContentState({ filePath, onUnsavedChange, onSaved, readOn
|
|
|
184
193
|
// Refetch so fileContent.content (= originalContent for CodeEditor) updates.
|
|
185
194
|
// CodeEditor's originalContent effect will then sync savedContent.current
|
|
186
195
|
// to match localContent, clearing its internal hasChanges flag.
|
|
187
|
-
await refetch();
|
|
196
|
+
const refreshed = await refetch();
|
|
197
|
+
if (refreshed && typeof refreshed === 'object' && 'data' in refreshed) {
|
|
198
|
+
const data = refreshed.data;
|
|
199
|
+
if (data && typeof data === 'object' && 'revision' in data && typeof data.revision === 'string') {
|
|
200
|
+
draftRevisionRef.current = data.revision;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (draftGeneration.current !== generation)
|
|
204
|
+
return;
|
|
188
205
|
setHasUnsavedChanges(false);
|
|
189
206
|
setSaveFlash(true);
|
|
190
207
|
if (saveFlashTimerRef.current)
|
|
@@ -199,6 +216,7 @@ export function useFileContentState({ filePath, onUnsavedChange, onSaved, readOn
|
|
|
199
216
|
? err.message
|
|
200
217
|
: tI18nHardcoded.raw('i18nComplete.text27c2ccd962c2'),
|
|
201
218
|
}));
|
|
219
|
+
throw err;
|
|
202
220
|
}
|
|
203
221
|
finally {
|
|
204
222
|
setIsSaving(false);
|
|
@@ -216,6 +234,7 @@ export function useFileContentState({ filePath, onUnsavedChange, onSaved, readOn
|
|
|
216
234
|
const handleEditorChange = useCallback((content) => {
|
|
217
235
|
if (readOnly)
|
|
218
236
|
return;
|
|
237
|
+
draftGeneration.current++;
|
|
219
238
|
latestContentRef.current = content;
|
|
220
239
|
}, [readOnly]);
|
|
221
240
|
// Cmd+S handler for when CodeEditor is not mounted (e.g. markdown preview)
|
|
@@ -226,7 +245,7 @@ export function useFileContentState({ filePath, onUnsavedChange, onSaved, readOn
|
|
|
226
245
|
if ((e.metaKey || e.ctrlKey) && e.key === 's') {
|
|
227
246
|
e.preventDefault();
|
|
228
247
|
if (hasUnsavedChanges && latestContentRef.current) {
|
|
229
|
-
handleSave(latestContentRef.current);
|
|
248
|
+
void handleSave(latestContentRef.current).catch(() => undefined);
|
|
230
249
|
}
|
|
231
250
|
}
|
|
232
251
|
};
|
|
@@ -148,8 +148,8 @@ function expiryLabel(expiresAt) {
|
|
|
148
148
|
/**
|
|
149
149
|
* `Copy link` and everything about the link behind it, in one popover.
|
|
150
150
|
*
|
|
151
|
-
* A file or app mint always makes a NEW public token
|
|
152
|
-
*
|
|
151
|
+
* A file or app mint always makes a NEW public token, so the popover reads
|
|
152
|
+
* the session's live shares first:
|
|
153
153
|
*
|
|
154
154
|
* - a live link for this exact file or port → show it, copy it, see when it
|
|
155
155
|
* expires, revoke it;
|