@kortix/ui 0.14.9 → 0.14.11

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 (104) hide show
  1. package/dist/_vendor/kortix-shared.js +55 -2
  2. package/dist/_vendor/shared/artifact-comments.d.ts +235 -0
  3. package/dist/_vendor/shared/channel-descriptors.d.ts +96 -0
  4. package/dist/_vendor/shared/channel-message.d.ts +13 -2
  5. package/dist/_vendor/shared/index.d.ts +1 -0
  6. package/dist/_vendor/shared/runtime-versions.d.ts +9 -0
  7. package/dist/apps/app-preview-pool.js +19 -2
  8. package/dist/artifacts/artifact-frame.js +7 -3
  9. package/dist/customize/sections/component/channel-access-panel.js +4 -4
  10. package/dist/customize/sections/component/telegram-channel-row.js +7 -5
  11. package/dist/customize/sections/view/agent-editor-catalog.js +5 -0
  12. package/dist/customize/sections/view/channels-view.d.ts +2 -1
  13. package/dist/customize/sections/view/channels-view.js +12 -7
  14. package/dist/customize/settings/rail.js +9 -0
  15. package/dist/customize/settings/settings-tabs.d.ts +1 -1
  16. package/dist/customize/settings/settings-tabs.js +1 -0
  17. package/dist/customize/settings/tabs/chrome-connections-tab.d.ts +6 -0
  18. package/dist/customize/settings/tabs/chrome-connections-tab.js +99 -0
  19. package/dist/customize/settings/tabs/sandbox-template-form.js +8 -1
  20. package/dist/customize/shared/catalog/catalog-empty-state.d.ts +3 -3
  21. package/dist/customize/shared/catalog/catalog-empty-state.js +6 -4
  22. package/dist/files/renderers/image-frame.d.ts +3 -1
  23. package/dist/files/renderers/image-frame.js +2 -2
  24. package/dist/files/renderers/show-content-renderer.d.ts +2 -2
  25. package/dist/files/renderers/show-content-renderer.js +2 -1
  26. package/dist/i18n/en.d.ts +206 -0
  27. package/dist/i18n/en.js +219 -13
  28. package/dist/i18n/ui-catalog/remaining-ui-translation-keys.generated.js +3 -3
  29. package/dist/internal/apps-and-extensions/apps-modal.d.ts +9 -0
  30. package/dist/internal/apps-and-extensions/apps-modal.js +107 -0
  31. package/dist/internal/branding/branding-provider.js +3 -3
  32. package/dist/internal/chrome/browser-icon.d.ts +24 -0
  33. package/dist/internal/chrome/browser-icon.js +58 -0
  34. package/dist/internal/chrome/chrome-card-state.d.ts +16 -0
  35. package/dist/internal/chrome/chrome-card-state.js +9 -0
  36. package/dist/internal/chrome/chrome-connect.d.ts +69 -0
  37. package/dist/internal/chrome/chrome-connect.js +178 -0
  38. package/dist/internal/chrome/chrome-mock.d.ts +4 -0
  39. package/dist/internal/chrome/chrome-mock.js +15 -0
  40. package/dist/internal/chrome/chrome-names.d.ts +15 -0
  41. package/dist/internal/chrome/chrome-names.js +35 -0
  42. package/dist/internal/chrome/extension.d.ts +47 -0
  43. package/dist/internal/chrome/extension.js +91 -0
  44. package/dist/internal/chrome/use-chrome-offered.d.ts +11 -0
  45. package/dist/internal/chrome/use-chrome-offered.js +22 -0
  46. package/dist/internal/connector-logo/connector-logo-source.d.ts +2 -1
  47. package/dist/internal/connector-logo/connector-logo-source.js +7 -6
  48. package/dist/internal/hooks/use-notification-center.d.ts +10 -6
  49. package/dist/internal/hooks/use-notification-center.js +32 -10
  50. package/dist/internal/icons/edge.d.ts +5 -0
  51. package/dist/internal/icons/edge.js +10 -0
  52. package/dist/internal/lib/browser-noise/rules/extensions.js +17 -3
  53. package/dist/internal/lib/detect-os.d.ts +43 -0
  54. package/dist/internal/lib/detect-os.js +70 -0
  55. package/dist/internal/lib/site-metadata.js +4 -4
  56. package/dist/internal/lib/use-project-feature-flags.js +5 -1
  57. package/dist/internal/notifications/notification-bell.js +4 -3
  58. package/dist/internal/notifications/notification-rows.d.ts +1 -1
  59. package/dist/internal/notifications/notification-rows.js +2 -0
  60. package/dist/internal/stores/apps-modal-store.d.ts +8 -0
  61. package/dist/internal/stores/apps-modal-store.js +6 -0
  62. package/dist/internal/stores/composer-prefill-store.d.ts +12 -5
  63. package/dist/internal/stores/kortix-computer-store.d.ts +1 -1
  64. package/dist/markdown/setup-links/approval-decision.js +3 -1
  65. package/dist/primitives/empty-state.d.ts +2 -2
  66. package/dist/primitives/empty-state.js +6 -5
  67. package/dist/primitives/mermaid-renderer.js +1 -1
  68. package/dist/session/composer/send-stop-control.js +8 -3
  69. package/dist/session/header/session-site-header.js +3 -1
  70. package/dist/session/panel/easy/detail-view.js +4 -0
  71. package/dist/session/panel/mobile-tool-drawer.js +5 -3
  72. package/dist/session/panel/open-session-quick-view.js +2 -2
  73. package/dist/session/panel/panel-fallback.js +10 -7
  74. package/dist/session/panel/session-audit-shared.js +3 -1
  75. package/dist/session/panel/session-desktop.d.ts +6 -0
  76. package/dist/session/panel/session-desktop.js +74 -0
  77. package/dist/session/panel/session-panel-provider.d.ts +1 -0
  78. package/dist/session/panel/session-panel-provider.js +17 -1
  79. package/dist/session/panel/shared/use-panel-handoffs.d.ts +2 -2
  80. package/dist/session/panel/shared/use-panel-handoffs.js +5 -1
  81. package/dist/session/panel/use-session-panel-layout.js +8 -0
  82. package/dist/session/participants/session-participants.d.ts +3 -1
  83. package/dist/session/participants/session-participants.js +2 -2
  84. package/dist/session/prompts/approval-request.js +2 -0
  85. package/dist/session/status/session-tab-title.d.ts +1 -1
  86. package/dist/session/status/session-tab-title.js +1 -1
  87. package/dist/session/thread/session-chat.js +12 -0
  88. package/dist/session/thread/session-context-sub-sessions.js +1 -1
  89. package/dist/session/thread/session-thread.d.ts +13 -1
  90. package/dist/session/thread/session-thread.js +15 -5
  91. package/dist/session/thread/session-transcript-class.d.ts +5 -1
  92. package/dist/session/thread/session-transcript-class.js +5 -1
  93. package/dist/session/tool/tools/image-gen-tool.d.ts +1 -1
  94. package/dist/session/tool/tools/image-gen-tool.js +21 -11
  95. package/dist/session/turn/user-message.d.ts +7 -5
  96. package/dist/session/turn/user-message.js +14 -8
  97. package/dist/styles.css +1 -1
  98. package/dist/workspace/session-workspace.js +24 -5
  99. package/dist/workspace/sessions/session-label-badges.js +2 -1
  100. package/dist/workspace/sidebar/footer/project-chrome-nav.d.ts +10 -0
  101. package/dist/workspace/sidebar/footer/project-chrome-nav.js +56 -0
  102. package/dist/workspace/sidebar/project-sidebar.js +5 -1
  103. package/package.json +43 -3
  104. package/skills/kortix-ui/references/modules.md +1 -1
@@ -108,6 +108,7 @@ import { useEffect, useMemo, useState } from 'react';
108
108
  import { useTunnelConnections } from '@kortix/sdk/react';
109
109
  import { ComputerHero } from '../../connectors/detail/computer-app-page.js';
110
110
  import { FeatureGateScreen } from '../../shared/feature-gate-screen.js';
111
+ import { CHANNEL_PLATFORM_IDS, CHANNELS, isChannelPlatform, } from '../../../_vendor/kortix-shared.js';
111
112
  export function ChannelsSection({ projectId, platform }) {
112
113
  const tI18nComplete = useTranslations('hardcodedUi.i18nComplete');
113
114
  useTeamsInstallReturnToast();
@@ -137,7 +138,7 @@ export function ChannelsSection({ projectId, platform }) {
137
138
  // platform, the Channels tab every platform, one under the other.
138
139
  const platforms = platform
139
140
  ? [platform]
140
- : ['slack', 'teams', ...(emailEnabled ? ['email'] : []), 'telegram'];
141
+ : CHANNEL_PLATFORM_IDS.filter((pf) => pf !== 'email' || emailEnabled);
141
142
  // The Kortix tab also holds the computer, where this deployment can pair one
142
143
  // (the machine list answers 503 where computers are off).
143
144
  const computersEnabled = useTunnelConnections({ refetchInterval: false }).isSuccess;
@@ -153,6 +154,11 @@ export function ChannelsSection({ projectId, platform }) {
153
154
  return _jsx(EmailHero, { projectId: projectId, canWrite: canWrite });
154
155
  case 'telegram':
155
156
  return _jsx(TelegramChannelRow, { projectId: projectId, canWrite: canWrite });
157
+ default: {
158
+ // A new channel id fails here until it has a hero.
159
+ const _exhaustive = pf;
160
+ return _exhaustive;
161
+ }
156
162
  }
157
163
  };
158
164
  // The Email page with the flag off: the standard flag-off screen, with its enable action.
@@ -190,14 +196,15 @@ function ChannelsButton({ count, onOpen }) {
190
196
  }
191
197
  /**
192
198
  * What follows a connected Slack before its first channel: the one-time "now
193
- * do this in Slack" nudge. It retires itself as soon as `bindings.length > 0`:
194
- * a bound channel is proof the user already invited the bot and mentioned it,
199
+ * do this in Slack" nudge. It retires itself as soon as a Slack channel is
200
+ * bound: that is proof the user already invited the bot and mentioned it,
195
201
  * and from then on the Slack row's "N channels" opens the bindings dialog.
202
+ * Another platform's row (a Telegram chat's `/model`) proves nothing about Slack.
196
203
  */
197
204
  function SlackFollowUp({ projectId }) {
198
205
  const tI18nComplete = useTranslations('hardcodedUi.i18nComplete');
199
206
  const bindingsQuery = useChannelBindings(projectId);
200
- const bindings = bindingsQuery.data?.bindings ?? [];
207
+ const bindings = (bindingsQuery.data?.bindings ?? []).filter((b) => b.platform === 'slack');
201
208
  return bindings.length === 0 && !bindingsQuery.isLoading ? (_jsxs(InfoBanner, { tone: "neutral", icon: AtIcon, title: tI18nComplete.raw('textd63af8edd3d6'), children: [tI18nComplete.raw('text1fd9ae1607aa'), ' ', _jsx("span", { className: "text-foreground font-medium", children: tI18nComplete.raw('text476b90bdc143') }), ' ', tI18nComplete.raw('textb81b729b4538')] })) : null;
202
209
  }
203
210
  /**
@@ -249,7 +256,7 @@ function ChannelBindingsDialog({ projectId, canWrite, platform, open, onOpenChan
249
256
  return (_jsx(Modal, { open: open, onOpenChange: onOpenChange, children: selected ? (_jsx(ChannelSettingsModalContent, { projectId: projectId, binding: selected.binding, name: selected.name, scope: selected.scope, projectDefaultAgent: projectDefaultAgent, canWrite: canWrite, onBack: back })) : (_jsxs(ModalContent, { className: "lg:max-w-lg", children: [_jsxs(ModalHeader, { children: [_jsx(ModalTitle, { children: tI18nComplete.raw('text5f61b63c2c4a') }), _jsx(ModalDescription, { children: tI18nComplete.raw('text1f2550ed44dc') })] }), _jsxs(ModalBody, { className: "space-y-3 pt-4", children: [tabs.length > 1 ? (_jsx(Tabs, { value: active, onValueChange: (value) => {
250
257
  setTab(value);
251
258
  setQuery('');
252
- }, children: _jsx(TabsListCompact, { children: tabs.map((t) => (_jsxs(TabsTriggerCompact, { value: t.platform, className: "gap-1.5", children: [PLATFORM_NAMES[t.platform] ?? t.platform, _jsx("span", { className: "text-muted-foreground tabular-nums", children: t.count })] }, t.platform))) }) })) : null, inTab.length >= BINDING_SEARCH_MIN ? (_jsxs(InputGroupSearch, { children: [_jsx(InputGroupSearchIcon, { children: _jsx(MagnifyingGlassIcon, {}) }), _jsx(InputGroupSearchInput, { placeholder: tI18nComplete.raw('text49c266baaaa7'), value: query, onChange: (e) => setQuery(e.target.value), size: "sm" }), _jsx(InputGroupSearchClear, { onClick: () => setQuery('') })] })) : null, bindingsQuery.isLoading ? (_jsxs("div", { className: "space-y-2", children: [_jsx(Skeleton, { className: "h-12 rounded-md" }), _jsx(Skeleton, { className: "h-12 rounded-md" })] })) : visible.length === 0 ? (_jsx("p", { className: "text-muted-foreground px-3 py-6 text-center text-xs", children: tI18nComplete.raw('texte8dd87902b91') })) : (
259
+ }, children: _jsx(TabsListCompact, { children: tabs.map((t) => (_jsxs(TabsTriggerCompact, { value: t.platform, className: "gap-1.5", children: [isChannelPlatform(t.platform) ? CHANNELS[t.platform].label : t.platform, _jsx("span", { className: "text-muted-foreground tabular-nums", children: t.count })] }, t.platform))) }) })) : null, inTab.length >= BINDING_SEARCH_MIN ? (_jsxs(InputGroupSearch, { children: [_jsx(InputGroupSearchIcon, { children: _jsx(MagnifyingGlassIcon, {}) }), _jsx(InputGroupSearchInput, { placeholder: tI18nComplete.raw('text49c266baaaa7'), value: query, onChange: (e) => setQuery(e.target.value), size: "sm" }), _jsx(InputGroupSearchClear, { onClick: () => setQuery('') })] })) : null, bindingsQuery.isLoading ? (_jsxs("div", { className: "space-y-2", children: [_jsx(Skeleton, { className: "h-12 rounded-md" }), _jsx(Skeleton, { className: "h-12 rounded-md" })] })) : visible.length === 0 ? (_jsx("p", { className: "text-muted-foreground px-3 py-6 text-center text-xs", children: tI18nComplete.raw('texte8dd87902b91') })) : (
253
260
  // The list scrolls; the tabs and search above it stay put. Capped at
254
261
  // the viewport less ~21rem of header, tabs, search and footer, so
255
262
  // the footer stays on screen at the 720 × 480 desktop minimum,
@@ -262,8 +269,6 @@ function ChannelBindingsDialog({ projectId, canWrite, platform, open, onOpenChan
262
269
  _jsx(BindingOverrides, { binding: binding, policies: conversationPolicies }, "overrides"),
263
270
  ], onClick: () => setSelectedId(binding.bindingId) }, binding.bindingId))) }))] }), _jsx(ModalFooter, { className: "pt-2 pb-5", children: _jsx(Button, { type: "button", variant: "outline-ghost", onClick: () => onOpenChange(false), children: tI18nComplete.raw('text11a6767d5674') }) })] })) }));
264
271
  }
265
- /** Brand names, as the platform rows spell them. */
266
- const PLATFORM_NAMES = { slack: 'Slack', teams: 'Microsoft Teams' };
267
272
  /** A row's settings at a glance: only what overrides the project, else "Project default". */
268
273
  function BindingOverrides({ binding, policies, }) {
269
274
  const tI18nComplete = useTranslations('hardcodedUi.i18nComplete');
@@ -1,5 +1,6 @@
1
1
  import { ArrowCircleUpIcon as ArrowUpCircle, ChatCircleIcon as ChatCircle, CoinsIcon as Coins, ShippingContainerIcon as Container, CreditCardIcon as CreditCard, FlaskIcon as Flask, KeyIcon as Key, LaptopIcon as Laptop, LinkIcon as Link, PaletteIcon as Palette, ShieldCheckIcon as ShieldCheck, SlidersHorizontalIcon as SlidersHorizontal, SquaresFourIcon as SquaresFour, UserIcon as User, } from '@phosphor-icons/react';
2
2
  import { Mcp } from '../../internal/icons/mcp.js';
3
+ import { ChromeRailIcon } from '../../internal/chrome/browser-icon.js';
3
4
  /**
4
5
  * Whether a rail item is the active one for the current settings tab.
5
6
  *
@@ -64,6 +65,14 @@ const STATIC_GROUPS = [
64
65
  description: 'Language and keyboard shortcuts.',
65
66
  icon: SlidersHorizontal,
66
67
  },
68
+ {
69
+ tab: 'chrome',
70
+ label: 'Kortix in Chrome',
71
+ description: 'Your sessions use Chrome with your logins. You see every step.',
72
+ docsHref: '/docs/connect/chrome',
73
+ // Monochrome like its neighbours; `ChromeRailIcon` takes only `className`.
74
+ icon: ChromeRailIcon,
75
+ },
67
76
  // Labelled "API keys", routed at `/settings/tokens`. The id has to be
68
77
  // `tokens` — it is the URL segment, and the account page already spends
69
78
  // `api-keys` on a legacy redirect (`RENAMED` in `settings-tabs.ts`) —
@@ -1,4 +1,4 @@
1
- export type SettingsTab = 'profile' | 'security' | 'appearance' | 'sessions' | 'preferences' | 'connected' | 'tokens' | 'plan' | 'credits' | 'workspace' | 'sandbox' | 'feature-flags' | 'upgrades' | 'mcp' | 'computer';
1
+ export type SettingsTab = 'profile' | 'security' | 'appearance' | 'sessions' | 'preferences' | 'connected' | 'chrome' | 'tokens' | 'plan' | 'credits' | 'workspace' | 'sandbox' | 'feature-flags' | 'upgrades' | 'mcp' | 'computer';
2
2
  /**
3
3
  * The default must be a tab that survives every gate, or the overlay opens on
4
4
  * nothing. It was `general` until project configuration moved to
@@ -54,6 +54,7 @@ export const SETTINGS_TABS = [
54
54
  'appearance',
55
55
  'sessions',
56
56
  'preferences',
57
+ 'chrome',
57
58
  'tokens',
58
59
  'mcp',
59
60
  'computer',
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Settings → Kortix in Chrome: the browsers this person connected with the
3
+ * Kortix extension. With none, the three connect steps stand inline and the
4
+ * connect action sits in the page header.
5
+ */
6
+ export declare function ChromeConnectionsTab(): import("react").JSX.Element;
@@ -0,0 +1,99 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useChromeConnections, useDeleteChromeConnection, useUpdateChromeConnection } from '@kortix/sdk/react';
4
+ import { PencilSimpleIcon } from '@phosphor-icons/react';
5
+ import { useState } from 'react';
6
+ import { Button } from '../../../primitives/button.js';
7
+ import { ConfirmDialog } from '../../../primitives/confirm-dialog.js';
8
+ import { InlineMeta } from '../../../primitives/inline-meta.js';
9
+ import { Input } from '../../../primitives/input.js';
10
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '../../../primitives/select.js';
11
+ import { SettingsRow, SettingsRowGroup } from '../../../primitives/settings-row.js';
12
+ import { SettingsSubsectionHeader } from '../../../primitives/settings-subsection-header.js';
13
+ import { Skeleton } from '../../../primitives/skeleton.js';
14
+ import { errorToast, successToast } from '../../../primitives/toast.js';
15
+ import { ErrorState } from '../../../primitives/error-state.js';
16
+ import { SettingsTabHeader } from '../settings-tab-header.js';
17
+ import { useLocale, useTranslations } from '../../../i18n/index.js';
18
+ import { relativeTime } from '../../../internal/lib/relative-time.js';
19
+ import { ChromeButtonMark, ChromeConnectModal, ChromeConnectSteps, ChromeCopyLink, ChromeInstallActions, ChromeOnlineDot, useChromeConnect, } from '../../../internal/chrome/chrome-connect.js';
20
+ import { BrowserLogoTile } from '../../../internal/chrome/browser-icon.js';
21
+ import { chromeDisplayNames, offlineLongerThan } from '../../../internal/chrome/chrome-names.js';
22
+ import { ChromeMock } from '../../../internal/chrome/chrome-mock.js';
23
+ const ACCESS_MODES = ['always', 'ask', 'off'];
24
+ // `all` first: it is the recommended default.
25
+ const TAB_SCOPES = ['all', 'own'];
26
+ /** Offline longer than this shows when the browser was last seen. Shorter is just "Offline". */
27
+ const LAST_SEEN_AFTER_MS = 10 * 60_000;
28
+ /**
29
+ * Settings → Kortix in Chrome: the browsers this person connected with the
30
+ * Kortix extension. With none, the three connect steps stand inline and the
31
+ * connect action sits in the page header.
32
+ */
33
+ export function ChromeConnectionsTab() {
34
+ const t = useTranslations('chrome');
35
+ const connections = useChromeConnections();
36
+ const [connectOpen, setConnectOpen] = useState(false);
37
+ const list = connections.data ?? [];
38
+ if (connections.isSuccess && list.length === 0)
39
+ return _jsx(ChromeEmptyTab, {});
40
+ const names = chromeDisplayNames(list);
41
+ return (
42
+ // `max-w-2xl`: the form tier every person-scoped pane in this dialog uses.
43
+ _jsxs("div", { className: "mx-auto w-full max-w-2xl space-y-5", children: [_jsx(SettingsTabHeader, { tab: "chrome", action: list.length > 0 ? (_jsxs(Button, { size: "sm", variant: "secondary", className: "gap-1.5", onClick: () => setConnectOpen(true), children: [_jsx(ChromeButtonMark, {}), t('connectAnother')] })) : undefined }), connections.isPending ? (_jsx("div", { className: "space-y-2", children: _jsx(Skeleton, { className: "h-24 rounded-md" }) })) : connections.isError ? (_jsx(ErrorState, { size: "sm", title: t('loadFailed'), action: _jsx(Button, { variant: "outline", size: "sm", onClick: () => connections.refetch(), children: t('retry') }) })) : (_jsxs(_Fragment, { children: [_jsxs("section", { className: "space-y-3", children: [_jsx(SettingsSubsectionHeader, { title: t('listTitle') }), _jsx("ul", { className: "space-y-3", children: list.map((chrome) => (_jsx(ChromeRow, { chrome: chrome, displayName: names.get(chrome.id) ?? chrome.name }, chrome.id))) })] }), _jsxs("section", { className: "space-y-2", children: [_jsx(SettingsSubsectionHeader, { title: t('how.title') }), _jsxs("ul", { className: "text-muted-foreground list-disc space-y-1 pl-5 text-sm text-pretty", children: [_jsx("li", { children: t('how.group') }), _jsx("li", { children: t('how.control') }), _jsx("li", { children: t('how.own') })] })] })] })), _jsx(ChromeConnectModal, { open: connectOpen, onOpenChange: setConnectOpen, fresh: true })] }));
44
+ }
45
+ /** No browser yet: the steps stand inline, the one action is in the header. */
46
+ function ChromeEmptyTab() {
47
+ const state = useChromeConnect();
48
+ return (_jsxs("div", { className: "mx-auto w-full max-w-2xl space-y-5", children: [_jsx(SettingsTabHeader, { tab: "chrome", action: state.environment === 'unsupported' ? (_jsx(ChromeCopyLink, {})) : (_jsx(ChromeInstallActions, { state: state, showGuide: false })) }), _jsxs("div", { className: "space-y-4", children: [_jsx(ChromeMock, { className: "mx-auto max-w-md" }), _jsx("div", { className: "bg-popover flex flex-col gap-5 rounded-md border px-4 py-5", children: _jsx(ChromeConnectSteps, { state: state, showActions: false }) })] })] }));
49
+ }
50
+ /** "Chromium on macOS · Work · v1.0.0": what tells two browsers apart. The name is not repeated. */
51
+ function chromeMeta(chrome, displayName) {
52
+ // The profile label already sits in the display name of a duplicate: say it once.
53
+ const profile = chrome.profileLabel && !displayName.includes(chrome.profileLabel) ? chrome.profileLabel : null;
54
+ return [profile, chrome.extensionVersion ? `v${chrome.extensionVersion}` : null].filter((part) => Boolean(part));
55
+ }
56
+ function ChromeRow({ chrome, displayName }) {
57
+ const t = useTranslations('chrome');
58
+ const locale = useLocale();
59
+ const update = useUpdateChromeConnection();
60
+ const remove = useDeleteChromeConnection();
61
+ const [confirmOpen, setConfirmOpen] = useState(false);
62
+ const [draft, setDraft] = useState(null);
63
+ const save = (change) => update.mutate({ id: chrome.id, ...change }, { onError: () => errorToast(t('updateFailed')) });
64
+ const commitName = () => {
65
+ const name = draft?.trim();
66
+ // An empty name is not a name: keep the field open and flag it. Blur reverts.
67
+ if (!name)
68
+ return false;
69
+ setDraft(null);
70
+ if (name !== chrome.name)
71
+ save({ name });
72
+ return true;
73
+ };
74
+ const sessions = chrome.activeSessions.length;
75
+ // "Last seen just now" beside "Offline" contradicts itself: say it only once it is a while ago.
76
+ const longOffline = !chrome.online && offlineLongerThan(chrome.lastSeenAt, LAST_SEEN_AFTER_MS);
77
+ const status = chrome.online
78
+ ? t('online')
79
+ : longOffline
80
+ ? t('lastSeen', { time: relativeTime(chrome.lastSeenAt, locale) })
81
+ : t('offline');
82
+ const meta = [...chromeMeta(chrome, displayName), status];
83
+ return (_jsxs("li", { children: [_jsxs(SettingsRowGroup, { children: [_jsxs("div", { className: "flex items-center gap-3 px-4 py-3", children: [_jsx(BrowserLogoTile, { brand: chrome.brand }), _jsxs("div", { className: "min-w-0 flex-1 space-y-0.5", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(ChromeOnlineDot, { online: chrome.online }), draft === null ? (_jsxs("button", { type: "button", "aria-label": t('rename', { name: displayName }), title: displayName, className: "focus-visible:ring-ring/50 -mx-1 flex min-w-0 items-center gap-1.5 rounded-sm px-1 text-left outline-none focus-visible:ring-3", onClick: () => setDraft(chrome.name), children: [_jsx("span", { className: "truncate text-sm font-medium", children: displayName }), _jsx(PencilSimpleIcon, { className: "text-muted-foreground size-3.5 shrink-0" })] })) : (_jsx(Input, { autoFocus: true, value: draft, maxLength: 255, "aria-label": t('renameLabel'), className: "h-6 py-0", "aria-invalid": draft.trim() === '' ? true : undefined, onChange: (event) => setDraft(event.target.value), onBlur: () => {
84
+ if (!commitName())
85
+ setDraft(null);
86
+ }, onKeyDown: (event) => {
87
+ if (event.key === 'Enter')
88
+ commitName();
89
+ // Closes the field only. The settings dialog ignores Escape from a text field.
90
+ if (event.key === 'Escape')
91
+ setDraft(null);
92
+ } }))] }), _jsxs(InlineMeta, { children: [meta, sessions > 0 ? t('usedBy', { count: sessions }) : ''] })] }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => setConfirmOpen(true), children: t('disconnect') })] }), _jsx(SettingsRow, { label: t('access.label'), description: t('access.description'), children: _jsxs(Select, { value: chrome.accessMode, disabled: update.isPending, onValueChange: (value) => save({ accessMode: value }), children: [_jsx(SelectTrigger, { size: "sm", className: "w-44", "aria-label": t('access.label'), children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: ACCESS_MODES.map((mode) => (_jsx(SelectItem, { value: mode, children: t(`access.${mode}`) }, mode))) })] }) }), _jsx(SettingsRow, { label: t('scope.label'), description: t('scope.description'), children: _jsxs(Select, { value: chrome.tabScope, disabled: update.isPending, onValueChange: (value) => save({ tabScope: value }), children: [_jsx(SelectTrigger, { size: "sm", className: "w-44", "aria-label": t('scope.label'), children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: TAB_SCOPES.map((scope) => (_jsx(SelectItem, { value: scope, children: t(`scope.${scope}`) }, scope))) })] }) })] }), _jsx(ConfirmDialog, { open: confirmOpen, onOpenChange: setConfirmOpen, title: t('disconnectTitle', { name: displayName }), description: _jsxs(_Fragment, { children: [_jsx("span", { className: "text-muted-foreground mb-2 block text-xs", children: meta.join(' · ') }), t('disconnectDescription')] }), confirmLabel: t('disconnect'), confirmVariant: "destructive", isPending: remove.isPending, onConfirm: () => remove.mutate(chrome.id, {
93
+ onSuccess: () => {
94
+ setConfirmOpen(false);
95
+ successToast(t('disconnected'));
96
+ },
97
+ onError: () => errorToast(t('disconnectFailed')),
98
+ }) })] }));
99
+ }
@@ -13,6 +13,7 @@ import { ShippingContainerIcon as Container, FileCodeIcon as FileCode, PackageIc
13
13
  import { useMutation, useQueryClient } from '@tanstack/react-query';
14
14
  import { useMemo, useState } from 'react';
15
15
  import { Button } from '../../../primitives/button.js';
16
+ import { Checkbox } from '../../../primitives/checkbox.js';
16
17
  import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '../../../primitives/dialog.js';
17
18
  import { Input } from '../../../primitives/input.js';
18
19
  import { Label } from '../../../primitives/label.js';
@@ -48,6 +49,7 @@ export function SandboxTemplateForm({ projectId, open, onOpenChange, template, }
48
49
  const [cpu, setCpu] = useState('2');
49
50
  const [memoryGb, setMemoryGb] = useState('4');
50
51
  const [diskGb, setDiskGb] = useState('20');
52
+ const [residentialProxy, setResidentialProxy] = useState(false);
51
53
  // Reset / hydrate when opening
52
54
  const [hydratedFor, setHydratedFor] = useState(null);
53
55
  if (!hydratedFor || hydratedFor[0] !== open || hydratedFor[1] !== template) {
@@ -70,6 +72,7 @@ export function SandboxTemplateForm({ projectId, open, onOpenChange, template, }
70
72
  setCpu(String(template.cpu));
71
73
  setMemoryGb(String(template.memory_gb));
72
74
  setDiskGb(String(template.disk_gb));
75
+ setResidentialProxy(!!template.residential_proxy);
73
76
  }
74
77
  else if (open) {
75
78
  setName('');
@@ -82,6 +85,7 @@ export function SandboxTemplateForm({ projectId, open, onOpenChange, template, }
82
85
  setCpu('2');
83
86
  setMemoryGb('6');
84
87
  setDiskGb('20');
88
+ setResidentialProxy(false);
85
89
  }
86
90
  }
87
91
  // Auto-slug from name when the user hasn't typed a slug manually yet.
@@ -117,6 +121,7 @@ export function SandboxTemplateForm({ projectId, open, onOpenChange, template, }
117
121
  cpu: parsePosInt(cpu),
118
122
  memory_gb: parsePosInt(memoryGb),
119
123
  disk_gb: parsePosInt(diskGb),
124
+ ...(residentialProxy ? { residential_proxy: true } : {}),
120
125
  }),
121
126
  onSuccess: () => {
122
127
  successToast(t('toasts.created'));
@@ -135,6 +140,8 @@ export function SandboxTemplateForm({ projectId, open, onOpenChange, template, }
135
140
  cpu: parsePosInt(cpu) ?? null,
136
141
  memory_gb: parsePosInt(memoryGb) ?? null,
137
142
  disk_gb: parsePosInt(diskGb) ?? null,
143
+ // Keep a country set through kortix.yaml or the API while it stays on.
144
+ residential_proxy: residentialProxy ? (template?.residential_proxy ?? true) : null,
138
145
  }),
139
146
  onSuccess: () => {
140
147
  successToast(t('toasts.updated'));
@@ -148,7 +155,7 @@ export function SandboxTemplateForm({ projectId, open, onOpenChange, template, }
148
155
  return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-w-xl", children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { className: "flex items-center gap-2", children: [_jsx(Container, { className: "size-4" }), isEdit ? t('editTitle', { name: template?.name ?? '' }) : t('newTitle')] }), _jsx(DialogDescription, { children: t('description') })] }), _jsxs("div", { className: "space-y-4 py-2", children: [_jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("div", { children: [_jsx(Label, { htmlFor: "tpl-name", children: t('name') }), _jsx(Input, { id: "tpl-name", placeholder: t('namePlaceholder'), value: name, onChange: (e) => setName(e.target.value) })] }), _jsxs("div", { children: [_jsx(Label, { htmlFor: "tpl-slug", children: t('slug') }), _jsx(Input, { id: "tpl-slug", placeholder: tI18nComplete.raw('text5d58d41913d9'), value: slug, onChange: (e) => {
149
156
  setSlugManuallyEdited(true);
150
157
  setSlug(e.target.value.toLowerCase());
151
- }, disabled: isEdit, "aria-invalid": !!slugError }), slugError && _jsx("p", { className: "text-destructive mt-1 text-xs", children: slugError })] })] }), _jsxs("div", { children: [_jsx(Label, { children: t('imageSource') }), _jsxs("div", { className: "mt-2 grid grid-cols-2 gap-2", children: [_jsxs("button", { type: "button", onClick: () => setMode('image'), className: cn('border-border/60 flex flex-col items-start gap-1 rounded-md border p-3 text-left text-sm transition-colors', mode === 'image' && 'border-foreground/30 bg-muted/40'), children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Package, { className: "size-4" }), _jsx("span", { className: "font-medium", children: t('publicImage') })] }), _jsxs("span", { className: "text-muted-foreground text-xs", children: [t('example'), " ", _jsx("code", { className: "font-mono", children: "python:3.12-slim" })] })] }), _jsxs("button", { type: "button", onClick: () => setMode('dockerfile'), className: cn('border-border/60 flex flex-col items-start gap-1 rounded-md border p-3 text-left text-sm transition-colors', mode === 'dockerfile' && 'border-foreground/30 bg-muted/40'), children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(FileCode, { className: "size-4" }), _jsx("span", { className: "font-medium", children: tI18nComplete.raw('textdd2c0eb6ea5c') })] }), _jsx("span", { className: "text-muted-foreground text-xs", children: t('pathInsideRepo') })] })] }), _jsxs("div", { className: "mt-3", children: [mode === 'image' ? (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: "tpl-image", children: t('image') }), _jsx(Input, { id: "tpl-image", placeholder: tI18nComplete.raw('text1d0447543fac'), value: image, onChange: (e) => setImage(e.target.value) }), _jsxs("p", { className: "text-muted-foreground mt-1 text-xs", children: [t('specificTag'), " (", _jsx("code", { className: "font-mono", children: "latest" }), ")."] })] })) : (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: "tpl-df", children: t('dockerfilePath') }), _jsx(Input, { id: "tpl-df", placeholder: tI18nComplete.raw('text1bc7a47dada1'), value: dockerfilePath, onChange: (e) => setDockerfilePath(e.target.value) }), _jsx("p", { className: "text-muted-foreground mt-1 text-xs", children: t('relativeToRoot') })] })), sourceError && _jsx("p", { className: "text-destructive mt-1 text-xs", children: sourceError })] })] }), _jsxs("div", { children: [_jsx(Label, { children: t('resources') }), _jsxs("div", { className: "mt-2 grid grid-cols-3 gap-3", children: [_jsx(NumericField, { id: "cpu", label: tI18nComplete.raw('text082591a16602'), value: cpu, onChange: setCpu, min: 1, max: 32 }), _jsx(NumericField, { id: "mem", label: t('memory'), value: memoryGb, onChange: setMemoryGb, min: 1, max: 128 }), _jsx(NumericField, { id: "disk", label: t('disk'), value: diskGb, onChange: setDiskGb, min: 1, max: 500 })] })] }), _jsxs("div", { children: [_jsxs(Label, { htmlFor: "tpl-entry", children: [t('entrypoint'), " ", _jsxs("span", { className: "text-muted-foreground", children: ["(", t('optional'), ")"] })] }), _jsx(Textarea, { id: "tpl-entry", placeholder: t('entrypointPlaceholder'), value: entrypoint, onChange: (e) => setEntrypoint(e.target.value), className: "font-mono text-xs", rows: 2 })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: submitting, children: t('cancel') }), _jsxs(Button, { onClick: () => (isEdit ? editMut.mutate() : createMut.mutate()), disabled: !canSubmit || submitting, children: [submitting && _jsx(Loading, { className: "mr-2 size-4" }), isEdit ? t('save') : t('create')] })] })] }) }));
158
+ }, disabled: isEdit, "aria-invalid": !!slugError }), slugError && _jsx("p", { className: "text-destructive mt-1 text-xs", children: slugError })] })] }), _jsxs("div", { children: [_jsx(Label, { children: t('imageSource') }), _jsxs("div", { className: "mt-2 grid grid-cols-2 gap-2", children: [_jsxs("button", { type: "button", onClick: () => setMode('image'), className: cn('border-border/60 flex flex-col items-start gap-1 rounded-md border p-3 text-left text-sm transition-colors', mode === 'image' && 'border-foreground/30 bg-muted/40'), children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Package, { className: "size-4" }), _jsx("span", { className: "font-medium", children: t('publicImage') })] }), _jsxs("span", { className: "text-muted-foreground text-xs", children: [t('example'), " ", _jsx("code", { className: "font-mono", children: "python:3.12-slim" })] })] }), _jsxs("button", { type: "button", onClick: () => setMode('dockerfile'), className: cn('border-border/60 flex flex-col items-start gap-1 rounded-md border p-3 text-left text-sm transition-colors', mode === 'dockerfile' && 'border-foreground/30 bg-muted/40'), children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(FileCode, { className: "size-4" }), _jsx("span", { className: "font-medium", children: tI18nComplete.raw('textdd2c0eb6ea5c') })] }), _jsx("span", { className: "text-muted-foreground text-xs", children: t('pathInsideRepo') })] })] }), _jsxs("div", { className: "mt-3", children: [mode === 'image' ? (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: "tpl-image", children: t('image') }), _jsx(Input, { id: "tpl-image", placeholder: tI18nComplete.raw('text1d0447543fac'), value: image, onChange: (e) => setImage(e.target.value) }), _jsxs("p", { className: "text-muted-foreground mt-1 text-xs", children: [t('specificTag'), " (", _jsx("code", { className: "font-mono", children: "latest" }), ")."] })] })) : (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: "tpl-df", children: t('dockerfilePath') }), _jsx(Input, { id: "tpl-df", placeholder: tI18nComplete.raw('text1bc7a47dada1'), value: dockerfilePath, onChange: (e) => setDockerfilePath(e.target.value) }), _jsx("p", { className: "text-muted-foreground mt-1 text-xs", children: t('relativeToRoot') })] })), sourceError && _jsx("p", { className: "text-destructive mt-1 text-xs", children: sourceError })] })] }), _jsxs("div", { children: [_jsx(Label, { children: t('resources') }), _jsxs("div", { className: "mt-2 grid grid-cols-3 gap-3", children: [_jsx(NumericField, { id: "cpu", label: tI18nComplete.raw('text082591a16602'), value: cpu, onChange: setCpu, min: 1, max: 32 }), _jsx(NumericField, { id: "mem", label: t('memory'), value: memoryGb, onChange: setMemoryGb, min: 1, max: 128 }), _jsx(NumericField, { id: "disk", label: t('disk'), value: diskGb, onChange: setDiskGb, min: 1, max: 500 })] })] }), _jsxs("div", { children: [_jsxs(Label, { htmlFor: "tpl-entry", children: [t('entrypoint'), " ", _jsxs("span", { className: "text-muted-foreground", children: ["(", t('optional'), ")"] })] }), _jsx(Textarea, { id: "tpl-entry", placeholder: t('entrypointPlaceholder'), value: entrypoint, onChange: (e) => setEntrypoint(e.target.value), className: "font-mono text-xs", rows: 2 })] }), _jsxs("div", { className: "flex items-start gap-2", children: [_jsx(Checkbox, { id: "tpl-residential-ip", className: "mt-0.5", checked: residentialProxy, onCheckedChange: (checked) => setResidentialProxy(checked === true) }), _jsxs("div", { children: [_jsx(Label, { htmlFor: "tpl-residential-ip", children: t('residentialIp') }), _jsx("p", { className: "text-muted-foreground mt-1 text-xs", children: t('residentialIpHint') })] })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: submitting, children: t('cancel') }), _jsxs(Button, { onClick: () => (isEdit ? editMut.mutate() : createMut.mutate()), disabled: !canSubmit || submitting, children: [submitting && _jsx(Loading, { className: "mr-2 size-4" }), isEdit ? t('save') : t('create')] })] })] }) }));
152
159
  }
153
160
  function NumericField({ id, label, value, onChange, min, max, }) {
154
161
  return (_jsxs("div", { children: [_jsx(Label, { htmlFor: id, className: "text-xs", children: label }), _jsx(Input, { id: id, type: "number", min: min, max: max, value: value, onChange: (e) => onChange(e.target.value) })] }));
@@ -1,8 +1,8 @@
1
1
  import type { ReactNode } from 'react';
2
2
  /**
3
- * The "nothing here" line every catalog uses, so five hand-typed copies of one
4
- * class string cannot drift into five different-looking messages. `CatalogGrid`
5
- * owns loading/error/empty; this is what callers put IN the empty slot when the
3
+ * The "nothing here" state every catalog uses: the shared `EmptyState`, so a
4
+ * catalog's empty slot looks like every other empty view. `CatalogGrid` owns
5
+ * loading/error/empty; this is what callers put IN the empty slot when the
6
6
  * cause is the search box rather than an empty project.
7
7
  */
8
8
  export declare function CatalogEmptyNote({ children }: {
@@ -1,13 +1,15 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { MagnifyingGlassIcon } from '@phosphor-icons/react';
2
3
  import { useTranslations } from '../../../i18n/index.js';
4
+ import { EmptyState } from '../../../primitives/empty-state.js';
3
5
  /**
4
- * The "nothing here" line every catalog uses, so five hand-typed copies of one
5
- * class string cannot drift into five different-looking messages. `CatalogGrid`
6
- * owns loading/error/empty; this is what callers put IN the empty slot when the
6
+ * The "nothing here" state every catalog uses: the shared `EmptyState`, so a
7
+ * catalog's empty slot looks like every other empty view. `CatalogGrid` owns
8
+ * loading/error/empty; this is what callers put IN the empty slot when the
7
9
  * cause is the search box rather than an empty project.
8
10
  */
9
11
  export function CatalogEmptyNote({ children }) {
10
- return _jsx("p", { className: "text-muted-foreground px-3 py-6 text-center text-xs", children: children });
12
+ return _jsx(EmptyState, { size: "sm", icon: MagnifyingGlassIcon, title: children });
11
13
  }
12
14
  /**
13
15
  * "No matches for `<query>`."
@@ -27,6 +27,8 @@ export interface ImageFrameProps {
27
27
  naturalRatio?: string | null;
28
28
  /** Reports the image's natural size once it decoded. */
29
29
  onNaturalSize?: (width: number, height: number) => void;
30
+ /** Drawn over the frame's own box (an overlay on the image's corner), which narrows with `maxHeight`. */
31
+ children?: ReactNode;
30
32
  }
31
33
  /**
32
34
  * The box an image takes before, while and after it exists: a flat `bg-muted`
@@ -35,7 +37,7 @@ export interface ImageFrameProps {
35
37
  * The frame itself never moves: the busy signal is the label (one waiting
36
38
  * channel, kortix-brand `layout.md` States).
37
39
  */
38
- export declare function ImageFrame({ aspectRatio, src, alt, pending, label, onOpen, className, fill, maxHeight, openLabel, naturalRatio, onNaturalSize, }: ImageFrameProps): import("react").JSX.Element;
40
+ export declare function ImageFrame({ aspectRatio, src, alt, pending, label, onOpen, className, fill, maxHeight, openLabel, naturalRatio, onNaturalSize, children, }: ImageFrameProps): import("react").JSX.Element;
39
41
  /**
40
42
  * An image whose file could not be read: "Image unavailable" and Try again,
41
43
  * centered in the frame. Never the read's own error text (R38): that is
@@ -16,7 +16,7 @@ function ratioValue(ratio) {
16
16
  * The frame itself never moves: the busy signal is the label (one waiting
17
17
  * channel, kortix-brand `layout.md` States).
18
18
  */
19
- export function ImageFrame({ aspectRatio, src, alt, pending, label, onOpen, className, fill = false, maxHeight, openLabel, naturalRatio = null, onNaturalSize, }) {
19
+ export function ImageFrame({ aspectRatio, src, alt, pending, label, onOpen, className, fill = false, maxHeight, openLabel, naturalRatio = null, onNaturalSize, children, }) {
20
20
  const img = useRef(null);
21
21
  const [loaded, setLoaded] = useState(false);
22
22
  const [natural, setNatural] = useState(naturalRatio);
@@ -46,7 +46,7 @@ export function ImageFrame({ aspectRatio, src, alt, pending, label, onOpen, clas
46
46
  : {
47
47
  aspectRatio: natural ?? aspectRatio,
48
48
  ...(maxHeight && { width: `min(100%, calc(${maxHeight} * ${ratioValue(natural ?? aspectRatio)}))` }),
49
- }, children: [image && onOpen ? (_jsx("button", { type: "button", onClick: onOpen, "aria-label": openLabel ?? alt, className: "focus-visible:ring-kortix-base block size-full cursor-zoom-in rounded-md focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-inset", children: image })) : (image), label ? (_jsx("div", { className: "absolute inset-0 flex items-center justify-center p-4 text-center", children: label })) : null] }));
49
+ }, children: [image && onOpen ? (_jsx("button", { type: "button", onClick: onOpen, "aria-label": openLabel ?? alt, className: "focus-visible:ring-kortix-base block size-full cursor-zoom-in rounded-md focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-inset", children: image })) : (image), label ? (_jsx("div", { className: "absolute inset-0 flex items-center justify-center p-4 text-center", children: label })) : null, children] }));
50
50
  }
51
51
  /**
52
52
  * An image whose file could not be read: "Image unavailable" and Try again,
@@ -30,7 +30,7 @@ export interface ShowContentProps {
30
30
  type: string;
31
31
  title?: string;
32
32
  description?: string;
33
- path?: string;
33
+ path?: string | null;
34
34
  url?: string;
35
35
  content?: string;
36
36
  language?: string;
@@ -80,7 +80,7 @@ export interface ShowContentProps {
80
80
  */
81
81
  toolbarActions?: React.ReactNode;
82
82
  }
83
- export declare function ShowContentRenderer({ type, title, description, path, url, content, language, aspectRatio, attachment, sha256, LocalhostPreview, fill, onStatusChange, toolbarActions, }: ShowContentProps): string | number | bigint | boolean | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | React.JSX.Element | null | undefined;
83
+ export declare function ShowContentRenderer({ type, title, description, path: inputPath, url, content, language, aspectRatio, attachment, sha256, LocalhostPreview, fill, onStatusChange, toolbarActions, }: ShowContentProps): string | number | bigint | boolean | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | React.JSX.Element | null | undefined;
84
84
  export interface ShowCarouselItem {
85
85
  type: string;
86
86
  title?: string;
@@ -172,7 +172,8 @@ function FileCard({ title, fileName, path }) {
172
172
  return (_jsxs("div", { className: "flex items-center gap-4 px-5 py-5", children: [_jsx("div", { className: "bg-muted/20 flex size-12 items-center justify-center rounded-xl", children: _jsx(FileText, { className: "text-muted-foreground/40 size-6" }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "text-foreground truncate text-sm font-medium", children: title || fileName }), _jsx("div", { className: "text-muted-foreground/50 mt-0.5 truncate font-mono text-xs", children: path })] })] }));
173
173
  }
174
174
  // ── Component ──────────────────────────────────────────────────────────────
175
- export function ShowContentRenderer({ type, title = '', description = '', path = '', url = '', content = '', language = '', aspectRatio = '', attachment, sha256, LocalhostPreview, fill = false, onStatusChange, toolbarActions, }) {
175
+ export function ShowContentRenderer({ type, title = '', description = '', path: inputPath, url = '', content = '', language = '', aspectRatio = '', attachment, sha256, LocalhostPreview, fill = false, onStatusChange, toolbarActions, }) {
176
+ const path = inputPath ?? '';
176
177
  const tI18nComplete = useTranslations('hardcodedUi.i18nComplete');
177
178
  const tImageGen = useTranslations('imageGen');
178
179
  const arCSS = showAspectRatioToCSS(aspectRatio);