@kortix/ui 0.14.6 → 0.14.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/dist/_vendor/kortix-shared.js +490 -382
  2. package/dist/_vendor/shared/runtime-versions.d.ts +4 -0
  3. package/dist/access/access/access-dialog-share.js +4 -7
  4. package/dist/access/access/access-dialog.js +2 -1
  5. package/dist/access/access/principal-picker.js +6 -5
  6. package/dist/customize/agents/agent-authority-card.js +1 -2
  7. package/dist/customize/agents/agent-cover-card.d.ts +17 -0
  8. package/dist/customize/agents/agent-cover-card.js +13 -0
  9. package/dist/customize/agents/agent-detail-aside.js +15 -14
  10. package/dist/customize/agents/agent-drives-section.js +5 -6
  11. package/dist/customize/agents/agent-grant-pages.js +68 -70
  12. package/dist/customize/agents/agent-page.js +27 -19
  13. package/dist/customize/agents/agent-people-section.js +18 -17
  14. package/dist/customize/agents/agent-triggers-section.js +6 -5
  15. package/dist/customize/agents/agents-page.js +43 -49
  16. package/dist/customize/connectors/catalog/catalog-entry.d.ts +14 -0
  17. package/dist/customize/connectors/catalog/catalog-entry.js +18 -1
  18. package/dist/customize/connectors/catalog/connector-browse.d.ts +28 -0
  19. package/dist/customize/connectors/catalog/connector-browse.js +18 -5
  20. package/dist/customize/connectors/detail/app-page.js +19 -5
  21. package/dist/customize/connectors/detail/connector-modal.d.ts +10 -13
  22. package/dist/customize/connectors/detail/connector-modal.js +18 -101
  23. package/dist/customize/connectors/detail/connector-notice.js +6 -2
  24. package/dist/customize/connectors/detail/connector-page.d.ts +30 -0
  25. package/dist/customize/connectors/detail/connector-page.js +26 -13
  26. package/dist/customize/migrate-to-v2/migration-prompt.d.ts +1 -0
  27. package/dist/customize/migrate-to-v2/migration-prompt.js +1 -0
  28. package/dist/customize/sections/llm-provider/custom-provider-form.js +24 -7
  29. package/dist/customize/sections/llm-provider/custom-provider-panel.js +44 -8
  30. package/dist/customize/sections/llm-provider/types.d.ts +3 -0
  31. package/dist/customize/sections/llm-provider/utils.js +2 -2
  32. package/dist/customize/sections/view/agent-editor-access-fields.d.ts +9 -16
  33. package/dist/customize/sections/view/agent-editor-access-fields.js +30 -30
  34. package/dist/customize/sections/view/agent-editor-basics-fields.js +3 -10
  35. package/dist/customize/sections/view/agent-editor-primitives.d.ts +28 -10
  36. package/dist/customize/sections/view/agent-editor-primitives.js +32 -19
  37. package/dist/customize/sections/view/agent-editor.js +3 -3
  38. package/dist/customize/sections/view/channels-view.js +2 -6
  39. package/dist/customize/sections/view/gateway/gateway-routing.js +3 -3
  40. package/dist/customize/sections/view/grant-mode-field.d.ts +2 -5
  41. package/dist/customize/sections/view/grant-mode-field.js +10 -18
  42. package/dist/customize/sections/view/permission-editor.js +11 -8
  43. package/dist/customize/sections/view/secrets-view.js +2 -1
  44. package/dist/customize/settings/tabs/general-tab.d.ts +9 -2
  45. package/dist/customize/settings/tabs/general-tab.js +33 -11
  46. package/dist/customize/settings/tabs/transfer-workspace-dialog.d.ts +4 -0
  47. package/dist/customize/settings/tabs/transfer-workspace-dialog.js +82 -0
  48. package/dist/customize/shared/new-entity-menu.js +4 -1
  49. package/dist/customize/skills/skills-page.js +4 -5
  50. package/dist/customize/triggers/schedule/then-fields.js +1 -5
  51. package/dist/customize/triggers/schedule-view.js +4 -2
  52. package/dist/files/project/api/runtime-files.js +3 -2
  53. package/dist/files/project/file-source.js +18 -1
  54. package/dist/files/project/hooks/use-file-content.js +2 -0
  55. package/dist/files/viewer/file-header.js +1 -1
  56. package/dist/files/viewer/file-preview-modal.d.ts +1 -1
  57. package/dist/files/viewer/file-preview-modal.js +12 -2
  58. package/dist/files/viewer/file-source.d.ts +3 -1
  59. package/dist/files/viewer/use-file-content-state.js +26 -7
  60. package/dist/files/viewer/viewer-actions.js +2 -2
  61. package/dist/i18n/en.d.ts +393 -12
  62. package/dist/i18n/en.js +400 -19
  63. package/dist/internal/components/agent-mark.d.ts +38 -0
  64. package/dist/internal/components/agent-mark.js +38 -0
  65. package/dist/internal/files/types.d.ts +2 -0
  66. package/dist/internal/goals/goal-meta.d.ts +30 -0
  67. package/dist/internal/goals/goal-meta.js +55 -0
  68. package/dist/internal/lib/pixel-mark.d.ts +33 -0
  69. package/dist/internal/lib/pixel-mark.js +152 -0
  70. package/dist/internal/lib/query-client-singleton.d.ts +9 -3
  71. package/dist/internal/lib/query-client-singleton.js +18 -12
  72. package/dist/internal/lib/use-project-feature-flags.js +3 -3
  73. package/dist/internal/share/session-shares-modal.js +3 -7
  74. package/dist/internal/stores/kortix-computer-store.js +14 -2
  75. package/dist/internal/stores/session-hover-store.d.ts +32 -0
  76. package/dist/internal/stores/session-hover-store.js +38 -0
  77. package/dist/internal/stores/sidebar-nav-store.d.ts +1 -1
  78. package/dist/internal/stores/sidebar-nav-store.js +3 -1
  79. package/dist/lib/routes.d.ts +2 -0
  80. package/dist/lib/routes.js +1 -0
  81. package/dist/markdown/genui/components/inline.d.ts +1 -1
  82. package/dist/markdown/setup-links/app-credentials-form.d.ts +12 -0
  83. package/dist/markdown/setup-links/app-credentials-form.js +115 -0
  84. package/dist/markdown/setup-links/secret-intake-form.d.ts +10 -0
  85. package/dist/markdown/setup-links/secret-intake-form.js +1 -1
  86. package/dist/primitives/entity-avatar.d.ts +4 -1
  87. package/dist/primitives/entity-avatar.js +4 -2
  88. package/dist/primitives/settings-section-header.d.ts +1 -1
  89. package/dist/primitives/user-avatar.d.ts +3 -1
  90. package/dist/primitives/user-avatar.js +4 -2
  91. package/dist/providers/ui-provider.d.ts +1 -1
  92. package/dist/session/composer/agent-selector.d.ts +7 -1
  93. package/dist/session/composer/agent-selector.js +20 -10
  94. package/dist/session/composer/attachment-tile.d.ts +0 -2
  95. package/dist/session/composer/attachment-tile.js +0 -8
  96. package/dist/session/composer/clipboard-files.d.ts +2 -1
  97. package/dist/session/composer/composer-draft-recovery.d.ts +1 -2
  98. package/dist/session/composer/composer-draft-recovery.js +0 -13
  99. package/dist/session/composer/context-ring.d.ts +1 -1
  100. package/dist/session/composer/context-ring.js +2 -2
  101. package/dist/session/composer/context-usage.d.ts +8 -6
  102. package/dist/session/composer/context-usage.js +9 -7
  103. package/dist/session/composer/inbox-row-claims.d.ts +2 -2
  104. package/dist/session/composer/inbox-row-claims.js +1 -1
  105. package/dist/session/composer/queue-projection.d.ts +3 -2
  106. package/dist/session/composer/queued-prompt-edit.d.ts +1 -1
  107. package/dist/session/composer/reasoning-effort-selector.d.ts +2 -1
  108. package/dist/session/composer/session-composer-readiness.d.ts +7 -22
  109. package/dist/session/composer/session-composer-readiness.js +12 -40
  110. package/dist/session/composer/token-progress.d.ts +1 -1
  111. package/dist/session/composer/token-progress.js +6 -6
  112. package/dist/session/header/export-transcript-modal.d.ts +3 -2
  113. package/dist/session/header/export-transcript-modal.js +6 -3
  114. package/dist/session/header/session-goal-indicator.d.ts +1 -0
  115. package/dist/session/header/session-goal-indicator.js +41 -0
  116. package/dist/session/header/session-site-header.js +2 -1
  117. package/dist/session/models/model-selector.d.ts +3 -1
  118. package/dist/session/models/model-selector.js +3 -2
  119. package/dist/session/panel/easy/plan-card.d.ts +2 -0
  120. package/dist/session/panel/easy/plan-card.js +2 -0
  121. package/dist/session/panel/session-action-panel-column.js +11 -1
  122. package/dist/session/panel/session-audit-shared.d.ts +0 -4
  123. package/dist/session/panel/session-audit-shared.js +1 -1
  124. package/dist/session/panel/session-changes-shared.d.ts +2 -1
  125. package/dist/session/prompts/session-permission-prompt.js +6 -3
  126. package/dist/session/status/provisioning-failure.d.ts +2 -1
  127. package/dist/session/status/session-error-banner.js +8 -6
  128. package/dist/session/status/session-starting-loader.d.ts +24 -31
  129. package/dist/session/status/session-starting-loader.js +32 -81
  130. package/dist/session/thread/chat-minimap-items.d.ts +4 -4
  131. package/dist/session/thread/first-prompt-handover.d.ts +3 -2
  132. package/dist/session/thread/message-parsing.d.ts +3 -3
  133. package/dist/session/thread/plan-surface.d.ts +1 -0
  134. package/dist/session/thread/plan-surface.js +1 -0
  135. package/dist/session/thread/session-chat.js +5 -12
  136. package/dist/session/thread/session-context-metrics.d.ts +15 -0
  137. package/dist/session/thread/session-context-metrics.js +10 -1
  138. package/dist/session/thread/session-context-modal.d.ts +4 -2
  139. package/dist/session/thread/session-context-modal.js +16 -9
  140. package/dist/session/thread/session-history-scroll.d.ts +2 -1
  141. package/dist/session/thread/session-thread.js +1 -1
  142. package/dist/session/thread/transcript.d.ts +4 -0
  143. package/dist/session/thread/transcript.js +18 -5
  144. package/dist/session/thread/turn-share.d.ts +16 -0
  145. package/dist/session/thread/turn-share.js +162 -0
  146. package/dist/session/tool/tools/apply-patch-tool.d.ts +1 -0
  147. package/dist/session/tool/tools/apply-patch-tool.js +1 -0
  148. package/dist/session/tool/tools/pty-kill-tool.d.ts +1 -0
  149. package/dist/session/tool/tools/pty-kill-tool.js +1 -0
  150. package/dist/session/tool/tools/pty-read-tool.d.ts +2 -0
  151. package/dist/session/tool/tools/pty-read-tool.js +2 -0
  152. package/dist/session/tool/tools/pty-spawn-tool.d.ts +1 -0
  153. package/dist/session/tool/tools/pty-spawn-tool.js +1 -0
  154. package/dist/session/tool/tools/pty-write-tool.d.ts +1 -0
  155. package/dist/session/tool/tools/pty-write-tool.js +1 -0
  156. package/dist/session/tool/tools/todo-write-tool.d.ts +1 -0
  157. package/dist/session/tool/tools/todo-write-tool.js +1 -0
  158. package/dist/session/turn/message-author-label.js +6 -2
  159. package/dist/session/turn/plan-anchor.d.ts +3 -0
  160. package/dist/session/turn/plan-anchor.js +3 -0
  161. package/dist/session/turn/plan-card.d.ts +7 -0
  162. package/dist/session/turn/plan-card.js +7 -0
  163. package/dist/styles.css +1 -1
  164. package/dist/terminal/pty-connection.d.ts +5 -4
  165. package/dist/terminal/session-terminal-connect-bar.js +2 -1
  166. package/dist/workspace/index.d.ts +1 -0
  167. package/dist/workspace/index.js +1 -0
  168. package/dist/workspace/saved-session-skeleton-shape.d.ts +4 -3
  169. package/dist/workspace/saved-session-skeleton.d.ts +5 -1
  170. package/dist/workspace/saved-session-skeleton.js +5 -4
  171. package/dist/workspace/session-workspace.js +4 -5
  172. package/dist/workspace/sidebar/footer/project-goals-nav.d.ts +6 -0
  173. package/dist/workspace/sidebar/footer/project-goals-nav.js +26 -0
  174. package/dist/workspace/sidebar/modal/session-embed-section.d.ts +25 -0
  175. package/dist/workspace/sidebar/modal/session-embed-section.js +71 -0
  176. package/dist/workspace/sidebar/modal/share-session-modal.js +2 -2
  177. package/dist/workspace/sidebar/nav-row-meta.js +14 -2
  178. package/dist/workspace/sidebar/project-nav-items.js +3 -0
  179. package/dist/workspace/sidebar/project-session-list.js +12 -3
  180. package/dist/workspace/sidebar/session-brief-hover-card.js +33 -9
  181. package/package.json +12 -4
  182. package/skills/kortix-ui/references/modules.md +2 -0
  183. package/dist/session/status/use-readiness-settling.d.ts +0 -18
  184. package/dist/session/status/use-readiness-settling.js +0 -43
  185. package/dist/workspace/sidebar/modal/session-public-link-section.d.ts +0 -13
  186. package/dist/workspace/sidebar/modal/session-public-link-section.js +0 -66
@@ -84,7 +84,9 @@ export function TriggerToolbarMenu({ canManage, paused, isPending, onToggle, onS
84
84
  const tI18nComplete = useTranslations('hardcodedUi.i18nComplete');
85
85
  if (!canManage)
86
86
  return null;
87
- return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "secondary", size: "icon-base", disabled: isPending, "aria-label": tI18nComplete.raw('textd47d7cb0e4f8'), children: _jsx(DotsThreeIcon, { className: "text-muted-foreground size-4 shrink-0" }) }) }), _jsxs(DropdownMenuContent, { align: "end", className: "w-76", children: [_jsx(DropdownMenuItem, { className: RICH_ROW_CLASS, disabled: paused, onSelect: () => onToggle(true), children: _jsx(RichRowBody, { icon: PauseIcon, title: tI18nComplete.raw('text0b6258b50010'), description: tI18nComplete.raw('textdae75dce541f') }) }), _jsx(DropdownMenuItem, { className: RICH_ROW_CLASS, disabled: !paused, onSelect: () => onToggle(false), children: _jsx(RichRowBody, { icon: PlayIcon, title: tI18nComplete.raw('text43e673c5503d'), description: tI18nComplete.raw('textced57bebd466') }) }), onSelectMode ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onSelect: onSelectMode, children: [_jsx(CheckSquareIcon, {}), tI18nComplete.raw('text2a78025de6aa')] })] })) : null] })] }));
87
+ return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "secondary", size: "icon-base",
88
+ // `size-9`: the New trigger button and the search input beside it are `h-9`.
89
+ className: "size-9", disabled: isPending, "aria-label": tI18nComplete.raw('textd47d7cb0e4f8'), children: _jsx(DotsThreeIcon, { className: "text-muted-foreground size-4 shrink-0" }) }) }), _jsxs(DropdownMenuContent, { align: "end", className: "w-76", children: [_jsx(DropdownMenuItem, { className: RICH_ROW_CLASS, disabled: paused, onSelect: () => onToggle(true), children: _jsx(RichRowBody, { icon: PauseIcon, title: tI18nComplete.raw('text0b6258b50010'), description: tI18nComplete.raw('textdae75dce541f') }) }), _jsx(DropdownMenuItem, { className: RICH_ROW_CLASS, disabled: !paused, onSelect: () => onToggle(false), children: _jsx(RichRowBody, { icon: PlayIcon, title: tI18nComplete.raw('text43e673c5503d'), description: tI18nComplete.raw('textced57bebd466') }) }), onSelectMode ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onSelect: onSelectMode, children: [_jsx(CheckSquareIcon, {}), tI18nComplete.raw('text2a78025de6aa')] })] })) : null] })] }));
88
90
  }
89
91
  /**
90
92
  * The project-wide pause switch (`PATCH /projects/:id/triggers/activation`):
@@ -387,7 +389,7 @@ export function ScheduleView({ projectId }) {
387
389
  Both are hidden until the list has loaded — neither means anything
388
390
  on an error or a 403. */
389
391
  showContent ? (
390
- // Search, then one group: ⋯ and New trigger, both `secondary` at `h-8`.
392
+ // Search, then one group: ⋯ and New trigger, both `secondary` at `h-9`, the search input's height.
391
393
  _jsxs(ButtonGroup, { children: [triggers.length > 0 || paused ? (_jsx(TriggerActivationMenu, { projectId: projectId, onSelectMode: triggers.length > 0 ? enterSelectMode : undefined })) : null, canWrite ? (_jsx(NewEntityMenu, { label: copy.createLabel, pending: configure.pending, onChat: () => configure.start(newConfigPrompt('trigger')), manual: { onSelect: () => openCreate() }, caret: false })) : null] })) : undefined, children: [_jsxs("div", { className: "space-y-4", children: [accountPaused && showContent && (_jsxs(Alert, { variant: "warning", children: [_jsx(AlertTitle, { children: tAutomations('bannerTitle') }), _jsx(AlertDescription, { children: tAutomations('bannerDescription') })] })), paused && showContent && (_jsxs(Alert, { variant: "warning", children: [_jsx(AlertTitle, { children: tI18nComplete.raw('textb18b93a52cd2') }), _jsx(AlertDescription, { children: tI18nComplete.raw('textc9e9d972eda2') }), controls.canUpdate ? (_jsx(AlertActions, { children: _jsxs(Button, { variant: "outline", size: "sm", disabled: resumeAll.isPending, onClick: () => resumeAll.mutate(false), children: [resumeAll.isPending ? _jsx(Loading, { className: "size-3.5 shrink-0" }) : null, tI18nComplete.raw('text43e673c5503d')] }) })) : null] })), triggersQuery.isLoading ? (_jsx("div", { className: "space-y-1", children: Array.from({ length: 5 }).map((_, i) => (
392
394
  // biome-ignore lint/suspicious/noArrayIndexKey: fixed-length placeholder
393
395
  _jsx(Skeleton, { className: "h-10 rounded-md" }, i))) })) : isForbidden ? (_jsxs(Alert, { variant: "warning", children: [_jsx(AlertTitle, { children: tI18nComplete.raw('textabb84be05cfa') }), _jsxs(AlertDescription, { children: [tI18nComplete.raw('text22cd1f210415'), " ", copy.noun, tI18nComplete.raw('text382584f2456e')] })] })) : triggersQuery.isError ? (_jsx(ErrorState, { size: "sm", title: tI18nComplete('text7ce02a51e3dd', { value0: copy.noun }), description: triggersQuery.error?.message ?? tI18nComplete.raw('text0c953ab32c60'), action: _jsx(Button, { variant: "outline", size: "sm", onClick: () => triggersQuery.refetch(), children: tI18nComplete.raw('textd8b8392e2c54') }) })) : filter !== 'all' && ofKind.length === 0 ? (_jsx(EmptyState, { size: "sm", title: kindCopy[filter].emptyTitle })) : triggers.length === 0 ? (_jsx(EmptyState, { icon: LightningIcon, title: copy.emptyTitle, description: copy.emptyBody, className: "py-12", action: canWrite ? (_jsxs(Button, { variant: "outline", size: "sm", className: "gap-1.5", onClick: () => openCreate(), children: [_jsx(PlusIcon, { className: "size-4 shrink-0" }), copy.createLabel] })) : undefined })) : filtered.length === 0 ? (_jsxs("p", { className: "text-muted-foreground px-3 py-6 text-center text-xs", children: [tI18nComplete.raw('text965b516df3ba'), ' ', _jsx("span", { className: "text-foreground font-medium", children: debouncedQuery }), "."] })) : (_jsx(TriggerList, { triggers: filtered, groups: appGroups, eventNames: eventNames, projectPaused: paused, selectedSlug: selected?.slug ?? null, runningSlug: run.isPending ? (run.variables?.slug ?? null) : liveSlug, onOpen: (t) => openTrigger(t.slug), selection: selectMode
@@ -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
- return { type: 'text', content: new TextDecoder().decode(bytes), mimeType: 'text/plain; charset=utf-8' };
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: false, readOnly: true, 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) }) }))] }));
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
- save?: (filePath: string, file: File) => Promise<unknown>;
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
- // Keep latestContentRef in sync with loaded content
130
+ // Refresh clean content only; header Save must keep the visible dirty draft.
124
131
  useEffect(() => {
125
- if (fileContent?.content) {
126
- latestContentRef.current = fileContent.content;
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 (only transcripts are
152
- * reused server-side), so the popover reads the session's live shares first:
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;