@kortix/ui 0.14.5 → 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.
Files changed (236) hide show
  1. package/dist/_vendor/kortix-shared.js +489 -381
  2. package/dist/access/access/access-dialog.js +8 -5
  3. package/dist/access/access/copy-row.js +2 -2
  4. package/dist/access/sharing-picker.js +2 -1
  5. package/dist/browser/app-in-browser.js +5 -3
  6. package/dist/browser/browser-panel.js +6 -6
  7. package/dist/browser/sandbox-browser-chrome.d.ts +4 -1
  8. package/dist/browser/sandbox-browser-chrome.js +4 -3
  9. package/dist/customize/agents/agent-access.d.ts +20 -0
  10. package/dist/customize/agents/agent-access.js +17 -0
  11. package/dist/customize/agents/agent-authority-card.js +1 -2
  12. package/dist/customize/agents/agent-detail-aside.js +15 -14
  13. package/dist/customize/agents/agent-drives-section.js +5 -6
  14. package/dist/customize/agents/agent-grant-pages.js +73 -76
  15. package/dist/customize/agents/agent-nav.prototype.d.ts +35 -0
  16. package/dist/customize/agents/agent-nav.prototype.js +95 -0
  17. package/dist/customize/agents/agent-page.js +59 -23
  18. package/dist/customize/agents/agent-people-section.js +18 -17
  19. package/dist/customize/agents/agent-triggers-section.js +6 -5
  20. package/dist/customize/agents/agents-page.js +18 -1
  21. package/dist/customize/connectors/add-mcp/add-mcp-server.js +4 -2
  22. package/dist/customize/connectors/catalog/catalog-entry.d.ts +14 -0
  23. package/dist/customize/connectors/catalog/catalog-entry.js +18 -1
  24. package/dist/customize/connectors/catalog/connector-browse.d.ts +28 -0
  25. package/dist/customize/connectors/catalog/connector-browse.js +18 -5
  26. package/dist/customize/connectors/connector-routes.d.ts +3 -1
  27. package/dist/customize/connectors/connector-routes.js +18 -0
  28. package/dist/customize/connectors/connectors-page.js +23 -4
  29. package/dist/customize/connectors/detail/app-page.js +19 -5
  30. package/dist/customize/connectors/detail/connector-advanced-tools.js +4 -6
  31. package/dist/customize/connectors/detail/connector-modal.d.ts +10 -13
  32. package/dist/customize/connectors/detail/connector-modal.js +18 -101
  33. package/dist/customize/connectors/detail/connector-notice.js +6 -2
  34. package/dist/customize/connectors/detail/connector-page.d.ts +30 -0
  35. package/dist/customize/connectors/detail/connector-page.js +26 -13
  36. package/dist/customize/providers/provider-accordion.js +2 -1
  37. package/dist/customize/providers/provider-connect.js +2 -1
  38. package/dist/customize/sections/component/telegram-channel-row.js +1 -1
  39. package/dist/customize/sections/connector-connection-modal.js +11 -9
  40. package/dist/customize/sections/connectors-view.js +11 -13
  41. package/dist/customize/sections/llm-provider/custom-provider-form.js +24 -7
  42. package/dist/customize/sections/llm-provider/custom-provider-panel.js +44 -8
  43. package/dist/customize/sections/llm-provider/types.d.ts +3 -0
  44. package/dist/customize/sections/llm-provider/utils.js +2 -2
  45. package/dist/customize/sections/view/agent-editor-access-fields.d.ts +9 -16
  46. package/dist/customize/sections/view/agent-editor-access-fields.js +30 -30
  47. package/dist/customize/sections/view/agent-editor-basics-fields.js +3 -10
  48. package/dist/customize/sections/view/agent-editor-primitives.d.ts +28 -10
  49. package/dist/customize/sections/view/agent-editor-primitives.js +32 -19
  50. package/dist/customize/sections/view/agent-editor.js +3 -3
  51. package/dist/customize/sections/view/gateway/gateway-routing.js +18 -21
  52. package/dist/customize/sections/view/grant-mode-field.d.ts +2 -5
  53. package/dist/customize/sections/view/grant-mode-field.js +10 -18
  54. package/dist/customize/sections/view/permission-editor.js +11 -8
  55. package/dist/customize/sections/view/secrets-view.js +2 -1
  56. package/dist/customize/settings/rail-copy-context.js +5 -3
  57. package/dist/customize/settings/tabs/general-tab.d.ts +9 -2
  58. package/dist/customize/settings/tabs/general-tab.js +33 -11
  59. package/dist/customize/settings/tabs/sandbox-template-form.js +9 -12
  60. package/dist/customize/settings/tabs/transfer-workspace-dialog.d.ts +4 -0
  61. package/dist/customize/settings/tabs/transfer-workspace-dialog.js +82 -0
  62. package/dist/customize/shared/capability-tabs.js +2 -1
  63. package/dist/customize/skills/skills-page.js +3 -5
  64. package/dist/customize/triggers/schedule/event-trigger-copy.js +3 -2
  65. package/dist/customize/triggers/schedule/schedule-builder.js +7 -4
  66. package/dist/customize/triggers/schedule/trigger-composer.js +5 -2
  67. package/dist/customize/triggers/schedule/when-event.js +1 -1
  68. package/dist/diff/diff-view.js +9 -10
  69. package/dist/files/editor/code-editor.js +2 -3
  70. package/dist/files/project/api/runtime-files.js +3 -2
  71. package/dist/files/project/components/change-request-detail-dialog.d.ts +3 -1
  72. package/dist/files/project/components/change-request-detail-dialog.js +3 -5
  73. package/dist/files/project/components/change-request-status-badge.d.ts +12 -0
  74. package/dist/files/project/components/change-request-status-badge.js +27 -0
  75. package/dist/files/project/components/change-requests-panel.js +1 -1
  76. package/dist/files/project/components/drive-explorer.js +2 -1
  77. package/dist/files/project/components/drive-grid-view.js +11 -7
  78. package/dist/files/project/components/drive-list-view.js +4 -4
  79. package/dist/files/project/components/file-breadcrumbs.js +1 -1
  80. package/dist/files/project/components/file-explorer-page.js +1 -1
  81. package/dist/files/project/components/file-history-popover.js +1 -6
  82. package/dist/files/project/components/file-search.js +6 -1
  83. package/dist/files/project/components/open-change-request-dialog.js +2 -1
  84. package/dist/files/project/components/version-selector-logic.d.ts +11 -0
  85. package/dist/files/project/components/version-selector-logic.js +26 -0
  86. package/dist/files/project/components/version-selector.js +11 -20
  87. package/dist/files/project/file-source.js +18 -1
  88. package/dist/files/project/hooks/use-file-content.js +2 -0
  89. package/dist/files/project/hooks/use-server-health.js +17 -15
  90. package/dist/files/renderers/docx/docx-viewer.js +0 -5
  91. package/dist/files/renderers/image-renderer.js +6 -24
  92. package/dist/files/renderers/pdf/pdf-viewer.js +1 -1
  93. package/dist/files/renderers/presentation/FullScreenPresentationViewer.js +2 -11
  94. package/dist/files/renderers/show-content-renderer.js +1 -1
  95. package/dist/files/renderers/sqlite-renderer.js +8 -3
  96. package/dist/files/renderers/xlsx/xlsx-viewer.js +15 -16
  97. package/dist/files/tree/components/file-tree-item.js +1 -1
  98. package/dist/files/viewer/file-header.js +1 -1
  99. package/dist/files/viewer/file-preview-modal.d.ts +1 -1
  100. package/dist/files/viewer/file-preview-modal.js +13 -3
  101. package/dist/files/viewer/file-source.d.ts +3 -1
  102. package/dist/files/viewer/use-file-content-state.js +27 -8
  103. package/dist/files/viewer/viewer-actions.js +2 -2
  104. package/dist/i18n/en.d.ts +355 -12
  105. package/dist/i18n/en.js +360 -17
  106. package/dist/internal/desktop/desktop-back-button.js +5 -3
  107. package/dist/internal/files/types.d.ts +2 -0
  108. package/dist/internal/goals/goal-meta.d.ts +30 -0
  109. package/dist/internal/goals/goal-meta.js +55 -0
  110. package/dist/internal/hooks/use-auto-scroll.js +5 -3
  111. package/dist/internal/lib/query-client-singleton.d.ts +9 -3
  112. package/dist/internal/lib/query-client-singleton.js +18 -12
  113. package/dist/internal/notifications/notification-rows.js +2 -1
  114. package/dist/internal/outcomes/outcome-types.d.ts +5 -0
  115. package/dist/internal/share/session-shares-modal.js +3 -7
  116. package/dist/internal/stores/kortix-computer-store.js +14 -2
  117. package/dist/internal/stores/session-hover-store.d.ts +32 -0
  118. package/dist/internal/stores/session-hover-store.js +38 -0
  119. package/dist/internal/stores/sidebar-nav-store.d.ts +1 -1
  120. package/dist/internal/stores/sidebar-nav-store.js +3 -1
  121. package/dist/lib/routes.d.ts +2 -0
  122. package/dist/lib/routes.js +1 -0
  123. package/dist/markdown/code/inline-code.js +8 -0
  124. package/dist/markdown/code/output-paths-context.d.ts +18 -0
  125. package/dist/markdown/code/output-paths-context.js +35 -0
  126. package/dist/markdown/genui/components/charts.js +6 -5
  127. package/dist/primitives/map.js +2 -1
  128. package/dist/primitives/preview-image.js +4 -2
  129. package/dist/primitives/settings-section-header.d.ts +1 -1
  130. package/dist/primitives/textarea.js +1 -1
  131. package/dist/primitives/typed-title.js +12 -14
  132. package/dist/primitives/typing-animation.js +4 -2
  133. package/dist/session/composer/agent-selector.js +6 -1
  134. package/dist/session/composer/attachment-tile.d.ts +0 -2
  135. package/dist/session/composer/attachment-tile.js +0 -8
  136. package/dist/session/composer/clipboard-files.d.ts +2 -1
  137. package/dist/session/composer/composer-chat-input.js +5 -3
  138. package/dist/session/composer/composer-draft-recovery.d.ts +1 -2
  139. package/dist/session/composer/composer-draft-recovery.js +0 -13
  140. package/dist/session/composer/composer-logic.js +8 -7
  141. package/dist/session/composer/context-ring.d.ts +1 -1
  142. package/dist/session/composer/context-ring.js +2 -2
  143. package/dist/session/composer/context-usage.d.ts +3 -6
  144. package/dist/session/composer/context-usage.js +3 -6
  145. package/dist/session/composer/draft/composer-draft.js +4 -4
  146. package/dist/session/composer/inbox-row-claims.d.ts +2 -2
  147. package/dist/session/composer/inbox-row-claims.js +1 -1
  148. package/dist/session/composer/pasted-text.js +3 -2
  149. package/dist/session/composer/queue-projection.d.ts +3 -2
  150. package/dist/session/composer/queued-prompt-edit.d.ts +1 -1
  151. package/dist/session/composer/reasoning-effort-selector.d.ts +2 -1
  152. package/dist/session/composer/session-composer-readiness.d.ts +7 -22
  153. package/dist/session/composer/session-composer-readiness.js +12 -40
  154. package/dist/session/composer/stores/queued-draft-store.js +3 -2
  155. package/dist/session/composer/token-progress.d.ts +1 -1
  156. package/dist/session/composer/token-progress.js +2 -2
  157. package/dist/session/header/reminder-parts.d.ts +11 -2
  158. package/dist/session/header/reminder-parts.js +9 -4
  159. package/dist/session/header/session-goal-indicator.d.ts +1 -0
  160. package/dist/session/header/session-goal-indicator.js +41 -0
  161. package/dist/session/header/session-site-header.js +2 -1
  162. package/dist/session/media/sandbox-url-detector.js +0 -7
  163. package/dist/session/outcomes/change-request-outcomes.js +24 -3
  164. package/dist/session/overrides/provider-secret-pool-editor.js +2 -1
  165. package/dist/session/panel/advanced/advanced-panel.js +3 -7
  166. package/dist/session/panel/easy/app-preview.js +12 -2
  167. package/dist/session/panel/easy/easy-panel-logic.d.ts +1 -1
  168. package/dist/session/panel/easy/easy-panel-logic.js +1 -1
  169. package/dist/session/panel/easy/outputs-card.js +2 -1
  170. package/dist/session/panel/easy/panel-card.js +5 -3
  171. package/dist/session/panel/easy/preview-load.d.ts +14 -0
  172. package/dist/session/panel/easy/preview-load.js +15 -0
  173. package/dist/session/panel/easy/step-detail-body.js +2 -7
  174. package/dist/session/panel/session-action-panel-column.js +11 -1
  175. package/dist/session/panel/session-audit-shared.d.ts +0 -4
  176. package/dist/session/panel/session-audit-shared.js +1 -1
  177. package/dist/session/panel/session-changes-shared.d.ts +2 -1
  178. package/dist/session/panel/session-panel-provider.js +6 -2
  179. package/dist/session/panel/shared/use-panel-derivations.js +6 -4
  180. package/dist/session/panel/use-session-panel-layout.js +7 -5
  181. package/dist/session/print/session-print-header.js +9 -8
  182. package/dist/session/prompts/session-permission-prompt.js +6 -3
  183. package/dist/session/status/provisioning-failure.d.ts +2 -1
  184. package/dist/session/status/session-error-banner.js +8 -6
  185. package/dist/session/status/session-starting-loader.d.ts +24 -31
  186. package/dist/session/status/session-starting-loader.js +32 -81
  187. package/dist/session/thread/chat-minimap-items.d.ts +4 -4
  188. package/dist/session/thread/first-prompt-handover.d.ts +3 -2
  189. package/dist/session/thread/message-parsing.d.ts +3 -3
  190. package/dist/session/thread/session-chat.js +15 -19
  191. package/dist/session/thread/session-history-scroll.d.ts +2 -1
  192. package/dist/session/thread/session-history.js +3 -1
  193. package/dist/session/thread/session-thread.js +1 -1
  194. package/dist/session/thread/transcript.js +13 -7
  195. package/dist/session/tool/shared/file-list.js +19 -2
  196. package/dist/session/tool/shared/infrastructure-contexts.d.ts +7 -0
  197. package/dist/session/tool/shared/infrastructure-contexts.js +7 -0
  198. package/dist/session/tool/shared/infrastructure-output.js +9 -5
  199. package/dist/session/tool/shared/infrastructure-row-header.js +9 -1
  200. package/dist/session/tool/shared/infrastructure-rows.js +7 -5
  201. package/dist/session/tool/tools/bash-tool.js +10 -6
  202. package/dist/session/tool/tools/web-search-tool.js +25 -3
  203. package/dist/session/turn/recovered-failures.d.ts +8 -0
  204. package/dist/session/turn/recovered-failures.js +32 -0
  205. package/dist/session/turn/streaming-cadence.js +5 -8
  206. package/dist/styles.css +1 -1
  207. package/dist/terminal/pty-connection.d.ts +5 -4
  208. package/dist/terminal/session-terminal-panel.js +3 -5
  209. package/dist/theme/theme.js +2 -1
  210. package/dist/workspace/index.d.ts +1 -0
  211. package/dist/workspace/index.js +1 -0
  212. package/dist/workspace/projects/modal/github-import-pickers.js +7 -6
  213. package/dist/workspace/review/review-center.js +13 -10
  214. package/dist/workspace/saved-session-skeleton-shape.d.ts +4 -3
  215. package/dist/workspace/saved-session-skeleton.d.ts +5 -1
  216. package/dist/workspace/saved-session-skeleton.js +5 -4
  217. package/dist/workspace/session-workspace.js +9 -14
  218. package/dist/workspace/sessions/deleted-session-row.js +5 -2
  219. package/dist/workspace/sidebar/footer/project-computer-nav.js +6 -4
  220. package/dist/workspace/sidebar/footer/project-goals-nav.d.ts +6 -0
  221. package/dist/workspace/sidebar/footer/project-goals-nav.js +26 -0
  222. package/dist/workspace/sidebar/footer/project-mcp-nav.js +6 -2
  223. package/dist/workspace/sidebar/modal/rename-session-modal.js +8 -3
  224. package/dist/workspace/sidebar/modal/session-embed-section.d.ts +25 -0
  225. package/dist/workspace/sidebar/modal/session-embed-section.js +71 -0
  226. package/dist/workspace/sidebar/modal/share-session-modal.js +2 -2
  227. package/dist/workspace/sidebar/nav-row-meta.js +14 -2
  228. package/dist/workspace/sidebar/project-nav-items.js +3 -0
  229. package/dist/workspace/sidebar/project-session-list.js +12 -3
  230. package/dist/workspace/sidebar/session-brief-hover-card.js +5 -8
  231. package/package.json +4 -4
  232. package/skills/kortix-ui/references/modules.md +2 -0
  233. package/dist/session/status/use-readiness-settling.d.ts +0 -18
  234. package/dist/session/status/use-readiness-settling.js +0 -43
  235. package/dist/workspace/sidebar/modal/session-public-link-section.d.ts +0 -13
  236. package/dist/workspace/sidebar/modal/session-public-link-section.js +0 -66
@@ -9,7 +9,7 @@ export type PtyCloseAction = 'ended' | 'reconnect' | 'replace';
9
9
  * terminal asks `GET /kortix/pty`, which passes the same control-plane gate and
10
10
  * returns a readable body. That GET never wakes a box.
11
11
  */
12
- export type PtyAttachProbe = 'reachable' | 'not-ready' | 'unreachable';
12
+ type PtyAttachProbe = 'reachable' | 'not-ready' | 'unreachable';
13
13
  /** `null` = the PTY list answered. Anything else is the error it threw. */
14
14
  export declare function classifyPtyAttachProbe(error: unknown): PtyAttachProbe;
15
15
  /**
@@ -36,10 +36,10 @@ export declare const PTY_WAKE_RETRY_MS = 2000;
36
36
  export declare const PTY_WAKE_DEADLINE_MS: number;
37
37
  /** Consecutive non-wake failures before the terminal stops and offers Retry. */
38
38
  export declare const PTY_MAX_ATTACH_FAILURES = 5;
39
- export type PtyAttachPause = 'asleep' | 'failed';
39
+ type PtyAttachPause = 'asleep' | 'failed';
40
40
  /** What the attach overlay shows. `null` means the shell is live. */
41
41
  export type AttachPhase = 'connecting' | 'waking' | 'reconnecting' | PtyAttachPause | null;
42
- export type PtyAttachStep = {
42
+ type PtyAttachStep = {
43
43
  kind: 'retry';
44
44
  delayMs: number;
45
45
  phase: 'waking' | 'reconnecting';
@@ -56,7 +56,7 @@ export declare function nextPtyAttachStep(input: {
56
56
  /** How long the box has reported not-ready while a wake was armed. */
57
57
  wakingForMs: number;
58
58
  }): PtyAttachStep;
59
- export type TerminalPanelState = 'connecting' | 'error' | 'empty' | 'terminal';
59
+ type TerminalPanelState = 'connecting' | 'error' | 'empty' | 'terminal';
60
60
  export declare function deriveTerminalPanelState(input: {
61
61
  hasServerUrl: boolean;
62
62
  serverWaitExpired: boolean;
@@ -81,3 +81,4 @@ export declare function classifyPtyClose(input: {
81
81
  }): PtyCloseAction;
82
82
  /** Prevent a bad runtime from creating terminals forever in a replacement loop. */
83
83
  export declare function shouldAutoReplaceTerminal(replacementAttempt: number): boolean;
84
+ export {};
@@ -94,11 +94,9 @@ export function SessionTerminalPanel({ sessionId, projectId, projectSessionId, h
94
94
  setOptimisticPty(null);
95
95
  setTerminalPty(sessionId, null);
96
96
  }, [sessionId, setTerminalPty]);
97
- useEffect(() => {
98
- if (listedPty && optimisticPty?.id === listedPty.id) {
99
- setOptimisticPty(null);
100
- }
101
- }, [listedPty, optimisticPty?.id]);
97
+ if (listedPty && optimisticPty?.id === listedPty.id) {
98
+ setOptimisticPty(null);
99
+ }
102
100
  useEffect(() => {
103
101
  if (!terminalPtyId || isLoading || !ptys || pty || optimisticPty?.id === terminalPtyId)
104
102
  return;
@@ -123,8 +123,9 @@ function checkName(name) {
123
123
  return name;
124
124
  }
125
125
  function steps(field, allowed, values) {
126
+ const allowedSet = new Set(allowed);
126
127
  return Object.entries(values ?? {}).flatMap(([k, v]) => {
127
- if (!allowed.includes(k))
128
+ if (!allowedSet.has(k))
128
129
  throw new Error(`@kortix/ui theme: unknown ${field} step "${k}"`);
129
130
  return v === undefined ? [] : [[k, v]];
130
131
  });
@@ -23,3 +23,4 @@ export { ProjectSessionsView } from './sessions/project-sessions-view.js';
23
23
  export { ProjectSessionList } from './sidebar/project-session-list.js';
24
24
  export { ProjectSidebar } from './sidebar/project-sidebar.js';
25
25
  export { ShareSessionModal } from './sidebar/modal/share-session-modal.js';
26
+ export { formatGoalTime, GoalHealthDot, goalHref, goalNeedsYou, goalOpenAsks, goalProgress, GoalStatusBadge, useGoalClock, useGoalIssueText, } from '../internal/goals/goal-meta.js';
@@ -23,3 +23,4 @@ export { ProjectSessionsView } from './sessions/project-sessions-view.js';
23
23
  export { ProjectSessionList } from './sidebar/project-session-list.js';
24
24
  export { ProjectSidebar } from './sidebar/project-sidebar.js';
25
25
  export { ShareSessionModal } from './sidebar/modal/share-session-modal.js';
26
+ export { formatGoalTime, GoalHealthDot, goalHref, goalNeedsYou, goalOpenAsks, goalProgress, GoalStatusBadge, useGoalClock, useGoalIssueText, } from '../internal/goals/goal-meta.js';
@@ -10,7 +10,7 @@ import { Github } from '../../../internal/icons/github.js';
10
10
  import { cn } from '../../../internal/lib/utils.js';
11
11
  import { CaretDownIcon as CaretDown, GitBranchIcon as GitBranch, MagnifyingGlassIcon as Search, } from '@phosphor-icons/react';
12
12
  import { useTranslations } from '../../../i18n/index.js';
13
- import { useEffect, useMemo, useState } from 'react';
13
+ import { useMemo, useState } from 'react';
14
14
  /** Varied so the placeholder reads as a list of names, not a barcode. */
15
15
  const SKELETON_WIDTHS = ['w-3/5', 'w-2/5', 'w-1/2', 'w-3/4', 'w-1/3'];
16
16
  /** Above this many options the list is fixed-height (see `SearchPicker`). */
@@ -28,13 +28,14 @@ function SearchPicker({ value, options, loading, disabled, loadingLabel, placeho
28
28
  const filtered = normalizedSearch
29
29
  ? options.filter((option) => option.keywords.includes(normalizedSearch))
30
30
  : options;
31
- useEffect(() => {
32
- if (!open) {
31
+ const handleOpenChange = (next) => {
32
+ setOpen(next);
33
+ if (!next) {
33
34
  setSearch('');
34
35
  onSearchChange?.('');
35
36
  }
36
- }, [onSearchChange, open]);
37
- return (_jsxs(Popover, { open: open, onOpenChange: setOpen, modal: false, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs("button", { type: "button", disabled: disabled, className: cn(triggerVariants({ size: 'md' }), 'w-full'), children: [_jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [_jsx("span", { className: "text-muted-foreground flex shrink-0 [&_svg]:size-4", children: icon }), _jsx("span", { className: cn('min-w-0 truncate', !selected && 'text-muted-foreground'), children: selected?.label ?? (firstLoad ? loadingLabel : placeholder) })] }), loading ? (_jsx(Loading, { variant: "spokes", className: "text-muted-foreground! size-4 shrink-0" })) : (_jsx(CaretDown, { className: cn(TRIGGER_CARET_CLASS, TRIGGER_ICON_SIZE.md) }))] }) }), _jsxs(PopoverContent, { side: "top", align: "start", avoidCollisions: false, className: "w-[var(--radix-popover-trigger-width)] overflow-hidden p-0", children: [_jsx("div", { className: "border-border border-b py-1", children: _jsxs(InputGroupSearch, { children: [_jsx(InputGroupSearchIcon, { children: _jsx(Search, {}) }), _jsx(InputGroupSearchInput, { size: "xs", className: "border-transparent focus:border-transparent focus:ring-0", value: search, onChange: (event) => {
37
+ };
38
+ return (_jsxs(Popover, { open: open, onOpenChange: handleOpenChange, modal: false, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs("button", { type: "button", disabled: disabled, className: cn(triggerVariants({ size: 'md' }), 'w-full'), children: [_jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [_jsx("span", { className: "text-muted-foreground flex shrink-0 [&_svg]:size-4", children: icon }), _jsx("span", { className: cn('min-w-0 truncate', !selected && 'text-muted-foreground'), children: selected?.label ?? (firstLoad ? loadingLabel : placeholder) })] }), loading ? (_jsx(Loading, { variant: "spokes", className: "text-muted-foreground! size-4 shrink-0" })) : (_jsx(CaretDown, { className: cn(TRIGGER_CARET_CLASS, TRIGGER_ICON_SIZE.md) }))] }) }), _jsxs(PopoverContent, { side: "top", align: "start", avoidCollisions: false, className: "w-[var(--radix-popover-trigger-width)] overflow-hidden p-0", children: [_jsx("div", { className: "border-border border-b py-1", children: _jsxs(InputGroupSearch, { children: [_jsx(InputGroupSearchIcon, { children: _jsx(Search, {}) }), _jsx(InputGroupSearchInput, { size: "xs", className: "border-transparent focus:border-transparent focus:ring-0", value: search, onChange: (event) => {
38
39
  setSearch(event.target.value);
39
40
  onSearchChange?.(event.target.value);
40
41
  }, placeholder: searchPlaceholder, autoCapitalize: "none", autoCorrect: "off", autoFocus: true })] }) }), firstLoad ? (
@@ -49,7 +50,7 @@ function SearchPicker({ value, options, loading, disabled, loadingLabel, placeho
49
50
  const active = option.value === value;
50
51
  return (_jsx("li", { children: _jsxs("button", { type: "button", "aria-pressed": active, className: cn(menuRow('md', 'default'), 'text-left', option.description && 'items-start', active && 'bg-primary/10 text-foreground'), onClick: () => {
51
52
  onValueChange(option.value);
52
- setOpen(false);
53
+ handleOpenChange(false);
53
54
  }, children: [_jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [_jsx("span", { className: "text-foreground truncate", children: option.label }), option.description ? (_jsx("span", { className: "text-muted-foreground truncate text-xs", children: option.description })) : null] }), option.badge ? _jsx("span", { className: "shrink-0", children: option.badge }) : null] }) }, option.value));
54
55
  }) }))] })] }));
55
56
  }
@@ -347,15 +347,15 @@ export function ReviewCenter({ initialItems, segment: segmentProp, onSegmentChan
347
347
  setSelectMode(false);
348
348
  }
349
349
  // If the active session filter no longer has items in this segment, reset it.
350
- useEffect(() => {
351
- if (sessionFilter !== 'all' && !sessionOpts.some((s) => s.sessionId === sessionFilter)) {
352
- setSessionFilter('all');
353
- }
354
- }, [sessionOpts, sessionFilter]);
350
+ if (sessionFilter !== 'all' && !sessionOpts.some((s) => s.sessionId === sessionFilter)) {
351
+ setSessionFilter('all');
352
+ }
355
353
  // Keep the focus cursor in range as the visible list changes.
356
- useEffect(() => {
354
+ const [visibleCountSeen, setVisibleCountSeen] = useState(null);
355
+ if (visible.length !== visibleCountSeen) {
356
+ setVisibleCountSeen(visible.length);
357
357
  setFocusedIdx((i) => Math.max(0, Math.min(i, visible.length - 1)));
358
- }, [visible.length]);
358
+ }
359
359
  // Select mode: DOM focus follows the cursor (j/k, a toggled row), so Space
360
360
  // and Enter act on the row the user sees, and a pointer entry that
361
361
  // unmounts the clicked checkbox keeps focus on its row.
@@ -377,12 +377,14 @@ export function ReviewCenter({ initialItems, segment: segmentProp, onSegmentChan
377
377
  // mutation (there's no per-call settle callback threaded through `apply`),
378
378
  // so clear the pending marker here rather than leave a button spinning
379
379
  // forever if the item already left the list (e.g. a stale poll raced it).
380
- useEffect(() => {
380
+ const [initialItemsSeen, setInitialItemsSeen] = useState(initialItems);
381
+ if (initialItems !== initialItemsSeen) {
382
+ setInitialItemsSeen(initialItems);
381
383
  if (initialItems)
382
384
  setItems(initialItems);
383
385
  setPendingId(null);
384
386
  setPendingDecision(null);
385
- }, [initialItems]);
387
+ }
386
388
  // Detect items that arrived since the user last looked → flag them "fresh".
387
389
  // The first load of each segment seeds its known set silently (nothing is
388
390
  // "new" on open, or on a first visit to a segment).
@@ -711,9 +713,10 @@ export function ReviewCenter({ initialItems, segment: segmentProp, onSegmentChan
711
713
  },
712
714
  ];
713
715
  const confirmCount = bulkIds[confirm.verb].length;
716
+ const confirmIds = new Set(bulkIds[confirm.verb]);
714
717
  // Every item the verb will decide: which app, what it does, to what, how risky.
715
718
  const confirmList = (_jsx("ul", { className: "border-border divide-border max-h-72 w-full min-w-0 divide-y overflow-y-auto rounded-lg border", children: selectedItems
716
- .filter((i) => bulkIds[confirm.verb].includes(i.id))
719
+ .filter((i) => confirmIds.has(i.id))
717
720
  .map((i) => {
718
721
  const risk = RISK_BADGE[i.risk];
719
722
  // The arguments, as the approval modal reads them: the content
@@ -23,12 +23,12 @@ export declare const LINE_WIDTHS: readonly ["w-full", "w-11/12", "w-5/6"];
23
23
  export declare const LAST_LINE_WIDTHS: readonly ["w-1/3", "w-2/5", "w-1/2", "w-3/5", "w-2/3", "w-3/4"];
24
24
  /** Tailwind's `animate-pulse` period, which the `Skeleton` primitive runs. */
25
25
  export declare const PULSE_PERIOD_MS = 2000;
26
- export interface SkeletonLine {
26
+ interface SkeletonLine {
27
27
  width: string;
28
28
  /** The row's place in the pulse wave; see `pulseDelayMs`. */
29
29
  phase: number;
30
30
  }
31
- export type SkeletonParagraph = {
31
+ type SkeletonParagraph = {
32
32
  kind: 'text';
33
33
  lines: SkeletonLine[];
34
34
  }
@@ -37,7 +37,7 @@ export type SkeletonParagraph = {
37
37
  kind: 'block';
38
38
  phase: number;
39
39
  };
40
- export interface SkeletonTurn {
40
+ interface SkeletonTurn {
41
41
  prompt: SkeletonLine & {
42
42
  tall: boolean;
43
43
  };
@@ -63,3 +63,4 @@ export declare function savedSessionSkeletonShape(sessionId: string): SkeletonSh
63
63
  * its cycle, so every row is on screen and moving from the first frame.
64
64
  */
65
65
  export declare function pulseDelayMs(phase: number, phases: number): number;
66
+ export {};
@@ -1,7 +1,11 @@
1
1
  import type { SessionStartStage } from '@kortix/sdk';
2
- export declare function SavedSessionSkeleton({ projectId, sessionId, stage, }: {
2
+ import { type ReactNode } from 'react';
3
+ export declare function SavedSessionSkeleton({ projectId, sessionId, stage, status, }: {
3
4
  projectId: string;
4
5
  /** The route's session id. */
5
6
  sessionId: string;
6
7
  stage: SessionStartStage;
8
+ /** Drawn in place of the skeleton rows: there is no saved conversation to
9
+ * stand in for, only the wake's status. */
10
+ status?: ReactNode;
7
11
  }): import("react").JSX.Element;
@@ -15,8 +15,9 @@ import { useMemo } from 'react';
15
15
  * The control plane keeps a saved copy of every session's conversation and
16
16
  * answers in one round trip; the computer takes 5-240 s to wake. So a session
17
17
  * that has a saved copy opens on this — its header, skeleton turns, and the
18
- * composer's outline — and the conversation replaces it in one step. The boot
19
- * screen is only for a session with nothing to read (`resolveResumeOverlay`).
18
+ * composer's outline — and the conversation replaces it in one step. A session
19
+ * with nothing to read (`resolveResumeOverlay`) gets the same frame with a
20
+ * `status` in place of the rows.
20
21
  *
21
22
  * Built from the geometry the real chat uses (`SessionLayout`, `SessionBodyRow`,
22
23
  * `SESSION_TRANSCRIPT_CLASS`, `COMPOSER_SHELL_CLASS`), so the crossfade into
@@ -36,8 +37,8 @@ import { useMemo } from 'react';
36
37
  * open waits 0.3-2.3 s for the server's copy, so its rows come 200 ms later.
37
38
  */
38
39
  const APPEAR_AFTER_A_MOMENT = 'animate-in fade-in fill-mode-both duration-normal delay-[var(--duration-moderate)]';
39
- export function SavedSessionSkeleton({ projectId, sessionId, stage, }) {
40
+ export function SavedSessionSkeleton({ projectId, sessionId, stage, status, }) {
40
41
  const t = useTranslations('sessionPage');
41
42
  const shape = useMemo(() => savedSessionSkeletonShape(sessionId), [sessionId]);
42
- return (_jsx(SessionLayout, { sessionId: sessionId, projectId: projectId, projectSessionId: sessionId, transient: true, bootStage: stage === 'ready' ? null : stage, children: _jsxs("div", { className: "bg-background relative flex h-full flex-col", children: [_jsx(SessionSiteHeader, { sessionId: sessionId, sessionTitle: "" }), _jsxs(SessionBodyRow, { transient: true, children: [_jsx("div", { className: "relative z-10 min-h-0 flex-1 overflow-hidden", children: _jsx("div", { role: "status", "aria-busy": "true", "aria-label": t('loadingConversation'), "data-testid": "saved-session-skeleton", className: cn(SESSION_TRANSCRIPT_CLASS, APPEAR_AFTER_A_MOMENT), children: _jsx(SavedSessionSkeletonRows, { shape: shape }) }) }), _jsx("div", { "aria-hidden": true, className: cn(COMPOSER_SHELL_CLASS, 'pb-8', APPEAR_AFTER_A_MOMENT), children: _jsx(SkeletonBar, { phase: shape.composerPhase, phases: shape.phases, className: "h-28 w-full rounded-xl" }) })] })] }) }));
43
+ return (_jsx(SessionLayout, { sessionId: sessionId, projectId: projectId, projectSessionId: sessionId, transient: true, bootStage: stage === 'ready' ? null : stage, children: _jsxs("div", { className: "bg-background relative flex h-full flex-col", children: [_jsx(SessionSiteHeader, { sessionId: sessionId, sessionTitle: "" }), _jsxs(SessionBodyRow, { transient: true, children: [_jsx("div", { className: "relative z-10 min-h-0 flex-1 overflow-hidden", children: status ?? (_jsx("div", { role: "status", "aria-busy": "true", "aria-label": t('loadingConversation'), "data-testid": "saved-session-skeleton", className: cn(SESSION_TRANSCRIPT_CLASS, APPEAR_AFTER_A_MOMENT), children: _jsx(SavedSessionSkeletonRows, { shape: shape }) })) }), _jsx("div", { "aria-hidden": true, className: cn(COMPOSER_SHELL_CLASS, 'pb-8', APPEAR_AFTER_A_MOMENT), children: _jsx(SkeletonBar, { phase: shape.composerPhase, phases: shape.phases, className: "h-28 w-full rounded-xl" }) })] })] }) }));
43
44
  }
@@ -21,7 +21,7 @@ import { useKortixComputerStore } from '../internal/stores/kortix-computer-store
21
21
  import { sessionPreviewTabId, useSessionBrowserStore, } from '../internal/stores/session-browser-store.js';
22
22
  import { ProjectScope } from '../internal/project-scope.js';
23
23
  import { SessionThread } from '../session/thread/session-thread.js';
24
- import { memo, useCallback, useEffect, useState } from 'react';
24
+ import { memo, useCallback, useState } from 'react';
25
25
  export const SessionLayout = memo(function SessionLayout({ sessionId, projectId, projectSessionId, children, bootStage = null, transient = false, }) {
26
26
  const isMobile = useIsMobile();
27
27
  const booting = !!bootStage;
@@ -106,10 +106,9 @@ function useSessionPanelSurfaces({ sessionId, projectId, projectSessionId, bootS
106
106
  setIsSidePanelOpen(!isSidePanelOpen);
107
107
  }, [isSidePanelOpen, setIsSidePanelOpen]);
108
108
  const panelHeader = (_jsx(PanelHeaderSwitcher, { view: effectiveView, onChangeView: (v) => setPanelView(sessionId, v), isSidePanelOpen: isSidePanelOpen, onTogglePanel: handleTogglePanel, auditBadge: auditPendingCount, onToggleMode: togglePanelMode }));
109
- // While booting, the panel is JUST the dead-center "Kortix Computer is
110
- // starting" loader — no header bar (the loader has its own heading, so a panel
111
- // title would be redundant), filling the whole card so it's perfectly
112
- // centered. The runtime-coupled views (Actions/Files/Terminal/Browser) need a
109
+ // While booting, the panel is JUST the wake status — empty unless the wake
110
+ // needs attention, and then dead-center in the card — with no header bar.
111
+ // The runtime-coupled views (Actions/Files/Terminal/Browser) need a
113
112
  // live sandbox, so they only render once booted.
114
113
  //
115
114
  // Easy mode has no header either: it is the three cards and nothing else. No
@@ -120,7 +119,7 @@ function useSessionPanelSurfaces({ sessionId, projectId, projectSessionId, bootS
120
119
  // own close DISCARDS the detail (it is done with), while ⌘I only puts it
121
120
  // away — press it again and the same detail comes back.
122
121
  const effectivePanelHeader = booting || isEasy ? null : panelHeader;
123
- const effectivePanelBody = booting ? (_jsx(SessionStartingLoader, { stage: bootStage ?? 'provisioning', delayMs: 0, projectId: projectId, sessionId: projectSessionId, variant: "stepper" })) : (panelBody);
122
+ const effectivePanelBody = booting ? (_jsx(SessionStartingLoader, { projectId: projectId, sessionId: projectSessionId })) : (panelBody);
124
123
  return { effectivePanelHeader, effectivePanelBody };
125
124
  }
126
125
  function useSessionPanelBody({ sessionId, projectId, projectSessionId, isEasy, effectiveView, }) {
@@ -129,15 +128,11 @@ function useSessionPanelBody({ sessionId, projectId, projectSessionId, isEasy, e
129
128
  const showTerminal = !isEasy && effectiveView === 'terminal';
130
129
  const showAudit = !isEasy && effectiveView === 'audit';
131
130
  const [terminalActivated, setTerminalActivated] = useState(false);
132
- useEffect(() => {
133
- if (showTerminal)
134
- setTerminalActivated(true);
135
- }, [showTerminal]);
131
+ if (showTerminal && !terminalActivated)
132
+ setTerminalActivated(true);
136
133
  const [browserActivated, setBrowserActivated] = useState(false);
137
- useEffect(() => {
138
- if (showBrowser)
139
- setBrowserActivated(true);
140
- }, [showBrowser]);
134
+ if (showBrowser && !browserActivated)
135
+ setBrowserActivated(true);
141
136
  // Easy mode's body is the detail shell and nothing else — the cards moved to
142
137
  // the floating overlay over the chat. The Advanced branches below are kept
143
138
  // intact (Advanced is disabled, not deleted) and are unreachable while
@@ -5,6 +5,7 @@ import Loading from '../../primitives/loading.js';
5
5
  import { getSessionDisplayTitle } from '../../session/actions/project-session-list-helpers.js';
6
6
  import { useLocale, useTranslations } from '../../i18n/index.js';
7
7
  import { relativeTime } from '../../internal/lib/relative-time.js';
8
+ import { useMemo } from 'react';
8
9
  /**
9
10
  * One row of Recently deleted (KRTX-2853): a session the viewer deleted (or,
10
11
  * as a manager, one they can open), still inside its restore window. Not a
@@ -16,10 +17,12 @@ export function DeletedSessionRow({ session, restoring, onRestore, }) {
16
17
  const title = getSessionDisplayTitle(session);
17
18
  const deletedAt = session.deleted_at ? new Date(session.deleted_at) : null;
18
19
  const restoreUntil = session.restore_until ? new Date(session.restore_until) : null;
19
- const exact = (date) => new Intl.DateTimeFormat(locale, { dateStyle: 'medium', timeStyle: 'short' }).format(date);
20
+ const exactFormat = useMemo(() => new Intl.DateTimeFormat(locale, { dateStyle: 'medium', timeStyle: 'short' }), [locale]);
21
+ const dayFormat = useMemo(() => new Intl.DateTimeFormat(locale, { month: 'short', day: 'numeric' }), [locale]);
22
+ const exact = (date) => exactFormat.format(date);
20
23
  return (_jsxs("div", { className: "bg-popover flex items-center gap-3 rounded-md border px-3 py-2", "data-deleted-session-id": session.session_id, children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("p", { className: "text-foreground truncate text-sm", children: title }), _jsxs("p", { className: "text-muted-foreground truncate text-xs", children: [deletedAt ? (_jsx("time", { dateTime: session.deleted_at, title: exact(deletedAt), children: t('deletedAgo', {
21
24
  when: relativeTime(session.deleted_at, locale, undefined, { style: 'long' }),
22
25
  }) })) : null, deletedAt && restoreUntil ? ' · ' : null, restoreUntil ? (_jsx("time", { dateTime: session.restore_until, title: exact(restoreUntil), children: t('restorableUntil', {
23
- date: new Intl.DateTimeFormat(locale, { month: 'short', day: 'numeric' }).format(restoreUntil),
26
+ date: dayFormat.format(restoreUntil),
24
27
  }) })) : null] })] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", disabled: restoring, onClick: () => onRestore(session), "aria-label": t('restoreNamed', { title }), children: [restoring ? _jsx(Loading, { className: "size-3.5 shrink-0" }) : null, t('restore')] })] }));
25
28
  }
@@ -2,13 +2,14 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { PromoNavRow } from './promo-nav-row.js';
4
4
  import { LaptopIcon } from '@phosphor-icons/react';
5
- import { useCallback, useState } from 'react';
5
+ import { useCallback, useState, useSyncExternalStore } from 'react';
6
6
  import Loading from '../../../primitives/loading.js';
7
7
  import { useSidebar } from '../../../primitives/sidebar.js';
8
8
  import { useConnectDesktopComputer, useOwnsPairedComputer, useThisComputerState, } from '../../../internal/tunnel/computer-connect.js';
9
9
  import { useIsMobile } from '../../../internal/hooks/index.js';
10
10
  import { useTranslations } from '../../../i18n/index.js';
11
11
  export const COMPUTER_PROMO_DISMISSED_KEY = 'kortix.computer-promo.dismissed';
12
+ const subscribeNoop = () => () => { };
12
13
  function readDismissed() {
13
14
  try {
14
15
  return window.localStorage.getItem(COMPUTER_PROMO_DISMISSED_KEY) === '1';
@@ -30,9 +31,10 @@ function readDismissed() {
30
31
  * and closing the sheet unmounts it together with anything it renders.
31
32
  */
32
33
  export function ProjectComputerNavItem({ projectId, onOpenConnect, }) {
33
- // Client-only: the row renders nothing until its queries resolve, so the
34
- // server render and the first client render agree.
35
- const [dismissed, setDismissed] = useState(() => typeof window === 'undefined' ? true : readDismissed());
34
+ // The server and the hydrating render read "dismissed"; the stored flag applies after.
35
+ const stored = useSyncExternalStore(subscribeNoop, readDismissed, () => true);
36
+ const [dismissedNow, setDismissed] = useState(false);
37
+ const dismissed = stored || dismissedNow;
36
38
  const dismiss = useCallback(() => {
37
39
  setDismissed(true);
38
40
  try {
@@ -0,0 +1,6 @@
1
+ /** The Goals row shows only with the `goals` flag on. */
2
+ export declare function useGoalsRowVisible(projectId: string | undefined): boolean;
3
+ /** The Goals row, with the count of goals that wait on a person or need attention. */
4
+ export declare function ProjectGoalsNavItem({ projectId }: {
5
+ projectId: string;
6
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,26 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { uiRoutes } from '../../../lib/routes.js';
4
+ import { ProjectNavRow } from '../../../internal/layout/project-nav-row.js';
5
+ import { CountBadge } from '../../../primitives/count-badge.js';
6
+ import { useTranslations } from '../../../i18n/index.js';
7
+ import { useFeatureFlag, useProjectGoals } from '@kortix/sdk/react';
8
+ import { TargetIcon } from '@phosphor-icons/react';
9
+ import { useUiPathname } from '../../../providers/ui-provider.js';
10
+ import { goalNeedsYou } from '../../../internal/goals/goal-meta.js';
11
+ /** The Goals row shows only with the `goals` flag on. */
12
+ export function useGoalsRowVisible(projectId) {
13
+ return useFeatureFlag(projectId, 'goals').enabled;
14
+ }
15
+ /** The Goals row, with the count of goals that wait on a person or need attention. */
16
+ export function ProjectGoalsNavItem({ projectId }) {
17
+ const t = useTranslations('sidebar');
18
+ const pathname = useUiPathname();
19
+ const visible = useGoalsRowVisible(projectId);
20
+ const goals = useProjectGoals(visible ? projectId : null);
21
+ const needsYou = (goals.data ?? []).filter(goalNeedsYou).length;
22
+ const href = projectId ? uiRoutes().goals(projectId) : null;
23
+ if (!href || !visible)
24
+ return null;
25
+ return (_jsxs(ProjectNavRow, { active: pathname?.startsWith(href) === true, label: t('goals'), href: href, prefetch: true, children: [_jsx("span", { className: "shrink-0", children: _jsx(TargetIcon, {}) }), t('goals'), needsYou > 0 ? (_jsx("span", { className: "ml-auto flex shrink-0 items-center gap-1", children: _jsx(CountBadge, { count: needsYou, tone: "waiting" }) })) : null] }));
26
+ }
@@ -1,12 +1,13 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { PromoNavRow } from './promo-nav-row.js';
4
- import { useCallback, useState } from 'react';
4
+ import { useCallback, useState, useSyncExternalStore } from 'react';
5
5
  import { useSidebar } from '../../../primitives/sidebar.js';
6
6
  import { Mcp } from '../../../internal/icons/mcp.js';
7
7
  import { useIsMobile } from '../../../internal/hooks/index.js';
8
8
  import { useTranslations } from '../../../i18n/index.js';
9
9
  export const MCP_PROMO_DISMISSED_KEY = 'kortix.mcp-promo.dismissed';
10
+ const subscribeNoop = () => () => { };
10
11
  function readDismissed() {
11
12
  try {
12
13
  return window.localStorage.getItem(MCP_PROMO_DISMISSED_KEY) === '1';
@@ -26,7 +27,10 @@ export function ProjectMcpNavItem({ onOpenConnect }) {
26
27
  const tComputers = useTranslations('computers');
27
28
  const isMobile = useIsMobile();
28
29
  const { setOpenMobile } = useSidebar();
29
- const [dismissed, setDismissed] = useState(() => typeof window === 'undefined' ? true : readDismissed());
30
+ // The server and the hydrating render read "dismissed"; the stored flag applies after.
31
+ const stored = useSyncExternalStore(subscribeNoop, readDismissed, () => true);
32
+ const [dismissedNow, setDismissed] = useState(false);
33
+ const dismissed = stored || dismissedNow;
30
34
  const dismiss = useCallback(() => {
31
35
  setDismissed(true);
32
36
  try {
@@ -5,15 +5,20 @@ import { Input } from '../../../primitives/input.js';
5
5
  import Loading from '../../../primitives/loading.js';
6
6
  import { Modal, ModalBody, ModalContent, ModalDescription, ModalFooter, ModalHeader, ModalTitle, } from '../../../primitives/modal.js';
7
7
  import { useTranslations } from '../../../i18n/index.js';
8
- import { useEffect, useState } from 'react';
8
+ import { useState } from 'react';
9
9
  import { MAX_SESSION_NAME_LENGTH, useRenameSession } from '../../../session/actions/use-rename-session.js';
10
10
  export function RenameSessionModal({ projectId, sessionId, currentName, open, onOpenChange, onSaved, }) {
11
11
  const tI18nHardcoded = useTranslations('hardcodedUi');
12
12
  const [value, setValue] = useState(currentName ?? '');
13
- useEffect(() => {
13
+ const [syncedFor, setSyncedFor] = useState([
14
+ open,
15
+ currentName,
16
+ ]);
17
+ if (syncedFor[0] !== open || syncedFor[1] !== currentName) {
18
+ setSyncedFor([open, currentName]);
14
19
  if (open)
15
20
  setValue(currentName ?? '');
16
- }, [open, currentName]);
21
+ }
17
22
  const renameMutation = useRenameSession(projectId, sessionId, {
18
23
  onSuccess: () => {
19
24
  onSaved?.();
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The iframe snippet a host page pastes: the ACTUAL session page
3
+ * (`/projects/:projectId/sessions/:sessionId`), not a separate widget or
4
+ * viewer. Pure, so the test can pin the exact HTML without a browser.
5
+ *
6
+ * The snippet is static HTML by construction: the URL is built from ids, and
7
+ * the `title` attribute is a fixed string. Never interpolate a session title
8
+ * into the attribute — an unescaped quote would break out of it on the HOST
9
+ * page, which does not share this app's CSP.
10
+ */
11
+ export declare function sessionEmbedSnippet(projectId: string, sessionId: string, origin: string): string | null;
12
+ /**
13
+ * "Embed" in the Share dialog: one copy button for the iframe snippet above.
14
+ * Framing the session page is allowed by the CSP `frame-ancestors` rule on
15
+ * that route (next.config.ts); the viewer inside the frame is the app's own
16
+ * auth. Same-site embeds require an existing signed-in session; cross-site
17
+ * embeds cannot sign in inside the frame (SameSite=Lax and /auth's CSP).
18
+ *
19
+ * Mounted inside `ModalContent`, so `window` is read at render time of an open
20
+ * dialog, never during SSR.
21
+ */
22
+ export declare function SessionEmbedSection({ projectId, sessionId, }: {
23
+ projectId: string;
24
+ sessionId: string;
25
+ }): import("react").JSX.Element;
@@ -0,0 +1,71 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Button } from '../../../primitives/button.js';
4
+ import { Label } from '../../../primitives/label.js';
5
+ import { errorToast, successToast } from '../../../primitives/toast.js';
6
+ import { useTranslations } from '../../../i18n/index.js';
7
+ import { CheckIcon, CodeIcon } from '@phosphor-icons/react';
8
+ import { useEffect, useRef, useState } from 'react';
9
+ /**
10
+ * The iframe snippet a host page pastes: the ACTUAL session page
11
+ * (`/projects/:projectId/sessions/:sessionId`), not a separate widget or
12
+ * viewer. Pure, so the test can pin the exact HTML without a browser.
13
+ *
14
+ * The snippet is static HTML by construction: the URL is built from ids, and
15
+ * the `title` attribute is a fixed string. Never interpolate a session title
16
+ * into the attribute — an unescaped quote would break out of it on the HOST
17
+ * page, which does not share this app's CSP.
18
+ */
19
+ export function sessionEmbedSnippet(projectId, sessionId, origin) {
20
+ if (!projectId || !sessionId || !origin)
21
+ return null;
22
+ const src = `${origin}/projects/${encodeURIComponent(projectId)}/sessions/${encodeURIComponent(sessionId)}`;
23
+ return `<iframe src="${src}" style="height:640px;width:100%;border:0;border-radius:12px" title="Kortix session" loading="lazy"></iframe>`;
24
+ }
25
+ /**
26
+ * "Embed" in the Share dialog: one copy button for the iframe snippet above.
27
+ * Framing the session page is allowed by the CSP `frame-ancestors` rule on
28
+ * that route (next.config.ts); the viewer inside the frame is the app's own
29
+ * auth. Same-site embeds require an existing signed-in session; cross-site
30
+ * embeds cannot sign in inside the frame (SameSite=Lax and /auth's CSP).
31
+ *
32
+ * Mounted inside `ModalContent`, so `window` is read at render time of an open
33
+ * dialog, never during SSR.
34
+ */
35
+ export function SessionEmbedSection({ projectId, sessionId, }) {
36
+ const t = useTranslations('hardcodedUi.sessionEmbed');
37
+ const tI18nComplete = useTranslations('hardcodedUi.i18nComplete');
38
+ const [copied, setCopied] = useState(false);
39
+ const copiedTimer = useRef(null);
40
+ const mounted = useRef(false);
41
+ useEffect(() => {
42
+ mounted.current = true;
43
+ return () => {
44
+ mounted.current = false;
45
+ if (copiedTimer.current)
46
+ clearTimeout(copiedTimer.current);
47
+ };
48
+ }, []);
49
+ const origin = typeof window === 'undefined' ? null : window.location.origin;
50
+ const snippet = origin ? sessionEmbedSnippet(projectId, sessionId, origin) : null;
51
+ const copy = async () => {
52
+ if (!snippet)
53
+ return;
54
+ try {
55
+ await navigator.clipboard.writeText(snippet);
56
+ if (!mounted.current)
57
+ return;
58
+ setCopied(true);
59
+ successToast(t('copied'));
60
+ if (copiedTimer.current)
61
+ clearTimeout(copiedTimer.current);
62
+ copiedTimer.current = setTimeout(() => setCopied(false), 2000);
63
+ }
64
+ catch {
65
+ if (!mounted.current)
66
+ return;
67
+ errorToast(tI18nComplete.raw('text167c96824acb'));
68
+ }
69
+ };
70
+ return (_jsxs("section", { className: "space-y-2", "data-testid": "session-embed", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { children: t('sectionTitle') }), _jsx("p", { className: "text-muted-foreground text-xs", children: t('sectionDescription') })] }), _jsxs(Button, { variant: "outline", size: "sm", disabled: !snippet, onClick: copy, children: [copied ? (_jsx(CheckIcon, { className: "text-kortix-green size-4 shrink-0" })) : (_jsx(CodeIcon, { className: "size-4 shrink-0" })), t('copy')] })] }));
71
+ }
@@ -13,7 +13,7 @@ import { sessionOversightQueryKey } from '../../../access/iam/session-oversight-
13
13
  import { ShieldCheckIcon } from '@phosphor-icons/react';
14
14
  import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
15
15
  import { useEffect, useState } from 'react';
16
- import { SessionPublicLinkSection } from './session-public-link-section.js';
16
+ import { SessionEmbedSection } from './session-embed-section.js';
17
17
  import { sessionAccessSummary, sessionAccessView } from './share-session-access.js';
18
18
  import { refreshAfterShare } from './share-session-cache.js';
19
19
  /**
@@ -105,5 +105,5 @@ export function ShareSessionModal({ projectId, session, open, onOpenChange, onSa
105
105
  // Read-only, not hidden: a person a session was shared with should
106
106
  // still be able to see who else is in it. Withholding that is what
107
107
  // sent people to the editable dialog in the first place.
108
- _jsx("p", { className: "text-foreground text-sm", "data-testid": "session-access-summary", children: session ? sessionAccessSummary(session) : null })), oversightOn ? (_jsx(InfoBanner, { tone: "neutral", icon: ShieldCheckIcon, "data-testid": "session-oversight-disclosure", children: tOversight.raw('shareDisclosure') })) : null, view.canEdit && session && publicSharesEnabled ? (_jsx(SessionPublicLinkSection, { projectId: projectId, sessionId: session.session_id })) : null] }), _jsxs(ModalFooter, { className: "sm:justify-between", children: [_jsx(Button, { variant: "outline-ghost", size: "sm", className: "w-full sm:w-auto", onClick: () => onOpenChange(false), disabled: save.isPending, children: view.canEdit ? 'Cancel' : 'Close' }), view.canEdit && (_jsxs(Button, { size: "sm", onClick: () => save.mutate(), disabled: save.isPending || !isSharingComplete(sharing) || blockedSelection, className: "w-full sm:w-auto", children: [save.isPending && _jsx(Loading, {}), tI18nHardcoded.raw('i18nComplete.text1509f561f241')] }))] })] }) }));
108
+ _jsx("p", { className: "text-foreground text-sm", "data-testid": "session-access-summary", children: session ? sessionAccessSummary(session) : null })), oversightOn ? (_jsx(InfoBanner, { tone: "neutral", icon: ShieldCheckIcon, "data-testid": "session-oversight-disclosure", children: tOversight.raw('shareDisclosure') })) : null, view.canEdit && session && publicSharesEnabled ? (_jsx(SessionEmbedSection, { projectId: projectId, sessionId: session.session_id })) : null] }), _jsxs(ModalFooter, { className: "sm:justify-between", children: [_jsx(Button, { variant: "outline-ghost", size: "sm", className: "w-full sm:w-auto", onClick: () => onOpenChange(false), disabled: save.isPending, children: view.canEdit ? 'Cancel' : 'Close' }), view.canEdit && (_jsxs(Button, { size: "sm", onClick: () => save.mutate(), disabled: save.isPending || !isSharingComplete(sharing) || blockedSelection, className: "w-full sm:w-auto", children: [save.isPending && _jsx(Loading, {}), tI18nHardcoded.raw('i18nComplete.text1509f561f241')] }))] })] }) }));
109
109
  }
@@ -3,11 +3,12 @@ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { uiRoutes } from '../../lib/routes.js';
4
4
  import { capabilityTabHref } from '../../internal/lib/capability-tab-routes.js';
5
5
  import { useAppsRowVisible } from './footer/project-apps-nav.js';
6
+ import { useGoalsRowVisible } from './footer/project-goals-nav.js';
6
7
  import { useRoomsRowVisible } from './footer/project-rooms-nav.js';
7
8
  import { filesRow } from '../../internal/layout/files-nav-label.js';
8
9
  import { CustomizeGlyph, useProjectNavRows, } from '../../internal/layout/project-settings-nav.js';
9
10
  import { useTranslations } from '../../i18n/index.js';
10
- import { AppWindowIcon, ChatsTeardropIcon, FoldersIcon, HardDrivesIcon } from '@phosphor-icons/react';
11
+ import { AppWindowIcon, ChatsTeardropIcon, FoldersIcon, HardDrivesIcon, TargetIcon } from '@phosphor-icons/react';
11
12
  import { useMemo } from 'react';
12
13
  /**
13
14
  * Label, glyph, destination and availability of every customizable nav row,
@@ -21,12 +22,14 @@ export function useNavRowMeta(projectId) {
21
22
  const { customize: customizeTab, files: filesTarget } = useProjectNavRows(projectId);
22
23
  const appsVisible = useAppsRowVisible(projectId);
23
24
  const roomsVisible = useRoomsRowVisible(projectId);
25
+ const goalsVisible = useGoalsRowVisible(projectId);
24
26
  // Memoized on the gates, not rebuilt per render: the Customize sidebar
25
27
  // modal memoizes its rows on this object, and a fresh one per render would
26
28
  // re-render every row on every drag frame.
27
29
  return useMemo(() => {
28
30
  const appsHref = uiRoutes().apps(projectId);
29
31
  const roomsHref = uiRoutes().rooms(projectId);
32
+ const goalsHref = uiRoutes().goals(projectId);
30
33
  const files = filesTarget ? filesRow(projectId, filesTarget) : null;
31
34
  return {
32
35
  ...(customizeTab
@@ -56,6 +59,15 @@ export function useNavRowMeta(projectId) {
56
59
  },
57
60
  }
58
61
  : {}),
62
+ ...(goalsVisible && goalsHref
63
+ ? {
64
+ goals: {
65
+ label: t('goals'),
66
+ icon: _jsx(TargetIcon, {}),
67
+ href: goalsHref,
68
+ },
69
+ }
70
+ : {}),
59
71
  ...(files
60
72
  ? {
61
73
  files: {
@@ -66,5 +78,5 @@ export function useNavRowMeta(projectId) {
66
78
  }
67
79
  : {}),
68
80
  };
69
- }, [t, tI18nComplete, projectId, customizeTab, appsVisible, roomsVisible, filesTarget]);
81
+ }, [t, tI18nComplete, projectId, customizeTab, appsVisible, roomsVisible, goalsVisible, filesTarget]);
70
82
  }