@kortix/ui 0.14.4 → 0.14.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/README.md +8 -4
  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-grant-pages.js +5 -6
  12. package/dist/customize/agents/agent-page.js +2 -1
  13. package/dist/customize/agents/agents-page.js +18 -1
  14. package/dist/customize/connectors/add-mcp/add-mcp-server.js +4 -2
  15. package/dist/customize/connectors/connector-routes.d.ts +3 -1
  16. package/dist/customize/connectors/connector-routes.js +18 -0
  17. package/dist/customize/connectors/connectors-page.js +23 -4
  18. package/dist/customize/connectors/detail/connector-advanced-tools.js +4 -6
  19. package/dist/customize/providers/provider-accordion.js +2 -1
  20. package/dist/customize/providers/provider-connect.js +2 -1
  21. package/dist/customize/sections/component/telegram-channel-row.js +1 -1
  22. package/dist/customize/sections/connector-connection-modal.js +11 -9
  23. package/dist/customize/sections/connectors-view.js +11 -13
  24. package/dist/customize/sections/llm-provider/constants.d.ts +0 -1
  25. package/dist/customize/sections/llm-provider/constants.js +0 -2
  26. package/dist/customize/sections/llm-provider/models-tab-group.d.ts +1 -1
  27. package/dist/customize/sections/llm-provider/models-tab-group.js +7 -4
  28. package/dist/customize/sections/llm-provider/models-tab-row.d.ts +2 -18
  29. package/dist/customize/sections/llm-provider/models-tab-row.js +6 -30
  30. package/dist/customize/sections/llm-provider/models-tab.d.ts +0 -1
  31. package/dist/customize/sections/llm-provider/models-tab.js +1 -10
  32. package/dist/customize/sections/llm-provider/use-connected-providers.js +2 -2
  33. package/dist/customize/sections/llm-provider/utils.d.ts +1 -1
  34. package/dist/customize/sections/llm-provider/utils.js +7 -14
  35. package/dist/customize/sections/view/gateway/_shared.js +1 -2
  36. package/dist/customize/sections/view/gateway/gateway-routing.js +18 -20
  37. package/dist/customize/sections/view/sandbox-provider-coverage.d.ts +3 -18
  38. package/dist/customize/sections/view/sandbox-provider-coverage.js +7 -27
  39. package/dist/customize/settings/rail-copy-context.js +5 -3
  40. package/dist/customize/settings/tabs/sandbox-tab.d.ts +2 -27
  41. package/dist/customize/settings/tabs/sandbox-tab.js +13 -112
  42. package/dist/customize/settings/tabs/sandbox-template-form.js +9 -12
  43. package/dist/customize/shared/capability-tabs.js +2 -1
  44. package/dist/customize/shared/entity/entity-modal.d.ts +5 -1
  45. package/dist/customize/shared/entity/entity-modal.js +7 -4
  46. package/dist/customize/skills/remove-skill-button.d.ts +16 -0
  47. package/dist/customize/skills/remove-skill-button.js +42 -0
  48. package/dist/customize/skills/skills-page.js +4 -6
  49. package/dist/customize/triggers/schedule/event-trigger-copy.js +3 -2
  50. package/dist/customize/triggers/schedule/schedule-builder.js +7 -4
  51. package/dist/customize/triggers/schedule/trigger-composer.js +5 -2
  52. package/dist/customize/triggers/schedule/trigger-list.d.ts +0 -2
  53. package/dist/customize/triggers/schedule/trigger-list.js +1 -18
  54. package/dist/customize/triggers/schedule/trigger-tile.d.ts +1 -32
  55. package/dist/customize/triggers/schedule/trigger-tile.js +1 -16
  56. package/dist/customize/triggers/schedule/when-event.js +1 -1
  57. package/dist/diff/diff-view.js +9 -10
  58. package/dist/files/editor/code-editor.js +2 -3
  59. package/dist/files/project/components/change-request-detail-dialog.d.ts +3 -1
  60. package/dist/files/project/components/change-request-detail-dialog.js +3 -5
  61. package/dist/files/project/components/change-request-status-badge.d.ts +12 -0
  62. package/dist/files/project/components/change-request-status-badge.js +27 -0
  63. package/dist/files/project/components/change-requests-panel.js +1 -1
  64. package/dist/files/project/components/drive-explorer.js +2 -1
  65. package/dist/files/project/components/drive-grid-view.js +11 -7
  66. package/dist/files/project/components/drive-list-view.js +4 -4
  67. package/dist/files/project/components/file-breadcrumbs.js +1 -1
  68. package/dist/files/project/components/file-explorer-page.js +1 -1
  69. package/dist/files/project/components/file-history-popover.js +1 -6
  70. package/dist/files/project/components/file-search.js +6 -1
  71. package/dist/files/project/components/open-change-request-dialog.js +2 -1
  72. package/dist/files/project/components/version-selector-logic.d.ts +11 -0
  73. package/dist/files/project/components/version-selector-logic.js +26 -0
  74. package/dist/files/project/components/version-selector.js +11 -20
  75. package/dist/files/project/hooks/use-change-requests.d.ts +14 -1
  76. package/dist/files/project/hooks/use-change-requests.js +44 -25
  77. package/dist/files/project/hooks/use-server-health.js +17 -15
  78. package/dist/files/renderers/docx/docx-viewer.js +4 -6
  79. package/dist/files/renderers/image-renderer.js +6 -24
  80. package/dist/files/renderers/pdf/pdf-renderer.d.ts +3 -1
  81. package/dist/files/renderers/pdf/pdf-renderer.js +2 -2
  82. package/dist/files/renderers/pdf/pdf-viewer.d.ts +3 -0
  83. package/dist/files/renderers/pdf/pdf-viewer.js +16 -7
  84. package/dist/files/renderers/presentation/FullScreenPresentationViewer.js +2 -11
  85. package/dist/files/renderers/show-content-renderer.js +1 -1
  86. package/dist/files/renderers/sqlite-renderer.js +8 -3
  87. package/dist/files/renderers/xlsx/xlsx-viewer.js +15 -16
  88. package/dist/files/tree/components/file-tree-item.js +1 -1
  89. package/dist/files/viewer/file-content-renderer.d.ts +4 -1
  90. package/dist/files/viewer/file-content-renderer.js +2 -2
  91. package/dist/files/viewer/file-preview-modal.js +1 -1
  92. package/dist/files/viewer/use-file-content-state.js +1 -1
  93. package/dist/i18n/en.d.ts +36 -35
  94. package/dist/i18n/en.js +45 -44
  95. package/dist/index.d.ts +1 -1
  96. package/dist/internal/desktop/desktop-back-button.js +5 -3
  97. package/dist/internal/hooks/use-auto-scroll.js +5 -3
  98. package/dist/internal/hooks/use-notification-center.js +21 -15
  99. package/dist/internal/hooks/use-review-session-summary.d.ts +6 -0
  100. package/dist/internal/hooks/use-review-session-summary.js +11 -0
  101. package/dist/internal/lib/browser-error-noise.d.ts +1 -1
  102. package/dist/internal/lib/browser-error-noise.js +1 -1
  103. package/dist/internal/lib/browser-noise/rules/rejections.d.ts +10 -0
  104. package/dist/internal/lib/browser-noise/rules/rejections.js +27 -1
  105. package/dist/internal/lib/provider-branding.js +14 -3
  106. package/dist/internal/lib/trigger-clock.d.ts +42 -0
  107. package/dist/internal/lib/trigger-clock.js +34 -0
  108. package/dist/internal/lib/web-notifications.d.ts +6 -0
  109. package/dist/internal/lib/web-notifications.js +57 -7
  110. package/dist/internal/notifications/notification-bell.d.ts +13 -0
  111. package/dist/internal/notifications/notification-bell.js +1 -1
  112. package/dist/internal/notifications/notification-rows.js +2 -1
  113. package/dist/internal/outcomes/outcome-types.d.ts +5 -0
  114. package/dist/internal/reminders/reminder-filter.d.ts +12 -0
  115. package/dist/internal/reminders/reminder-filter.js +34 -0
  116. package/dist/internal/reminders/reminder-format.d.ts +6 -0
  117. package/dist/internal/reminders/reminder-format.js +20 -2
  118. package/dist/internal/stores/kortix-computer-store.d.ts +1 -1
  119. package/dist/internal/stores/session-hover-store.d.ts +20 -5
  120. package/dist/internal/stores/session-hover-store.js +31 -13
  121. package/dist/internal/stores/session-panel-memory-store.js +2 -0
  122. package/dist/markdown/code/inline-code.js +8 -0
  123. package/dist/markdown/code/output-paths-context.d.ts +18 -0
  124. package/dist/markdown/code/output-paths-context.js +35 -0
  125. package/dist/markdown/genui/components/charts.js +6 -5
  126. package/dist/primitives/count-badge.d.ts +3 -1
  127. package/dist/primitives/count-badge.js +3 -3
  128. package/dist/primitives/glyph-paths-source.js +1 -1
  129. package/dist/primitives/glyph-paths.generated.js +1 -1
  130. package/dist/primitives/map.js +2 -1
  131. package/dist/primitives/preview-image.js +4 -2
  132. package/dist/primitives/textarea.js +1 -1
  133. package/dist/primitives/typed-title.js +12 -14
  134. package/dist/primitives/typing-animation.js +4 -2
  135. package/dist/providers/ui-provider.d.ts +57 -0
  136. package/dist/providers/ui-provider.js +4 -0
  137. package/dist/session/composer/ComposerCard.d.ts +2 -0
  138. package/dist/session/composer/ComposerCard.js +5 -1
  139. package/dist/session/composer/agent-selector.js +17 -5
  140. package/dist/session/composer/composer-chat-input.js +5 -3
  141. package/dist/session/composer/composer-logic.js +8 -7
  142. package/dist/session/composer/composer-toolbar.js +4 -2
  143. package/dist/session/composer/composer-underbar.js +3 -1
  144. package/dist/session/composer/composer.js +34 -5
  145. package/dist/session/composer/draft/composer-draft.js +4 -4
  146. package/dist/session/composer/menus/slash-actions.d.ts +22 -2
  147. package/dist/session/composer/menus/slash-actions.js +20 -0
  148. package/dist/session/composer/menus/slash-items.js +19 -3
  149. package/dist/session/composer/pasted-text.js +3 -2
  150. package/dist/session/composer/stores/queued-draft-store.js +3 -2
  151. package/dist/session/header/reminder-parts.d.ts +294 -0
  152. package/dist/session/header/reminder-parts.js +120 -0
  153. package/dist/session/header/session-reminders-indicator.d.ts +13 -0
  154. package/dist/session/header/session-reminders-indicator.js +142 -35
  155. package/dist/session/media/sandbox-url-detector.js +0 -7
  156. package/dist/session/models/model-availability.d.ts +23 -0
  157. package/dist/session/models/model-availability.js +20 -0
  158. package/dist/session/models/model-grouping.js +1 -4
  159. package/dist/session/models/model-selector.js +23 -14
  160. package/dist/session/models/use-model-connection-gate.js +4 -1
  161. package/dist/session/outcomes/change-request-outcomes.js +24 -3
  162. package/dist/session/outcomes/session-outcomes-provider.d.ts +3 -1
  163. package/dist/session/outcomes/session-outcomes-provider.js +31 -3
  164. package/dist/session/overrides/provider-secret-pool-editor.js +2 -1
  165. package/dist/session/overrides/session-overrides-composer.d.ts +1 -1
  166. package/dist/session/overrides/session-overrides-composer.js +5 -1
  167. package/dist/session/panel/advanced/advanced-panel.js +3 -7
  168. package/dist/session/panel/easy/app-preview.js +12 -2
  169. package/dist/session/panel/easy/easy-panel-logic.d.ts +1 -1
  170. package/dist/session/panel/easy/easy-panel-logic.js +1 -1
  171. package/dist/session/panel/easy/outputs-card.js +2 -1
  172. package/dist/session/panel/easy/panel-card.js +5 -3
  173. package/dist/session/panel/easy/preview-load.d.ts +14 -0
  174. package/dist/session/panel/easy/preview-load.js +15 -0
  175. package/dist/session/panel/easy/step-detail-body.js +2 -7
  176. package/dist/session/panel/mobile-tool-drawer.js +4 -2
  177. package/dist/session/panel/open-session-quick-view.js +2 -1
  178. package/dist/session/panel/panel-fallback.js +8 -4
  179. package/dist/session/panel/session-panel-provider.d.ts +1 -0
  180. package/dist/session/panel/session-panel-provider.js +19 -3
  181. package/dist/session/panel/session-reminders-panel.d.ts +4 -0
  182. package/dist/session/panel/session-reminders-panel.js +109 -0
  183. package/dist/session/panel/shared/use-panel-derivations.js +6 -4
  184. package/dist/session/panel/shared/use-panel-handoffs.d.ts +2 -2
  185. package/dist/session/panel/shared/use-panel-handoffs.js +5 -1
  186. package/dist/session/panel/use-session-panel-layout.js +7 -5
  187. package/dist/session/print/session-print-header.d.ts +0 -10
  188. package/dist/session/print/session-print-header.js +25 -9
  189. package/dist/session/status/session-surface.js +1 -1
  190. package/dist/session/thread/optimistic-turn.d.ts +5 -1
  191. package/dist/session/thread/optimistic-turn.js +8 -6
  192. package/dist/session/thread/session-chat.d.ts +6 -1
  193. package/dist/session/thread/session-chat.js +34 -12
  194. package/dist/session/thread/session-history.js +3 -1
  195. package/dist/session/thread/session-thread.d.ts +10 -1
  196. package/dist/session/thread/session-thread.js +2 -2
  197. package/dist/session/thread/transcript.js +13 -7
  198. package/dist/session/tool/shared/file-list.js +19 -2
  199. package/dist/session/tool/shared/infrastructure-contexts.d.ts +7 -0
  200. package/dist/session/tool/shared/infrastructure-contexts.js +7 -0
  201. package/dist/session/tool/shared/infrastructure-output.js +9 -5
  202. package/dist/session/tool/shared/infrastructure-row-header.js +9 -1
  203. package/dist/session/tool/shared/infrastructure-rows.js +7 -5
  204. package/dist/session/tool/tools/bash-tool.js +10 -6
  205. package/dist/session/tool/tools/web-search-tool.js +25 -3
  206. package/dist/session/turn/burst-step-body.js +4 -0
  207. package/dist/session/turn/recovered-failures.d.ts +8 -0
  208. package/dist/session/turn/recovered-failures.js +32 -0
  209. package/dist/session/turn/streaming-cadence.js +5 -8
  210. package/dist/styles.css +1 -1
  211. package/dist/terminal/session-terminal-panel.js +3 -5
  212. package/dist/theme/theme.js +2 -1
  213. package/dist/workspace/projects/modal/github-import-pickers.js +7 -6
  214. package/dist/workspace/review/review-center.js +13 -10
  215. package/dist/workspace/session-workspace.js +5 -9
  216. package/dist/workspace/sessions/deleted-session-row.js +5 -2
  217. package/dist/workspace/sidebar/footer/project-computer-nav.js +6 -4
  218. package/dist/workspace/sidebar/footer/project-mcp-nav.js +6 -2
  219. package/dist/workspace/sidebar/modal/rename-session-modal.js +8 -3
  220. package/dist/workspace/sidebar/project-session-list.js +52 -55
  221. package/dist/workspace/sidebar/session-brief-hover-card.d.ts +35 -18
  222. package/dist/workspace/sidebar/session-brief-hover-card.js +114 -37
  223. package/dist/workspace/sidebar/session-status-mark.d.ts +15 -1
  224. package/dist/workspace/sidebar/session-status-mark.js +63 -17
  225. package/package.json +7 -3
  226. package/skills/kortix-ui/SKILL.md +1 -1
  227. package/dist/customize/sections/view/sandbox-provider-result.d.ts +0 -47
  228. package/dist/customize/sections/view/sandbox-provider-result.js +0 -79
@@ -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
  });
@@ -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
@@ -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;
@@ -129,15 +129,11 @@ function useSessionPanelBody({ sessionId, projectId, projectSessionId, isEasy, e
129
129
  const showTerminal = !isEasy && effectiveView === 'terminal';
130
130
  const showAudit = !isEasy && effectiveView === 'audit';
131
131
  const [terminalActivated, setTerminalActivated] = useState(false);
132
- useEffect(() => {
133
- if (showTerminal)
134
- setTerminalActivated(true);
135
- }, [showTerminal]);
132
+ if (showTerminal && !terminalActivated)
133
+ setTerminalActivated(true);
136
134
  const [browserActivated, setBrowserActivated] = useState(false);
137
- useEffect(() => {
138
- if (showBrowser)
139
- setBrowserActivated(true);
140
- }, [showBrowser]);
135
+ if (showBrowser && !browserActivated)
136
+ setBrowserActivated(true);
141
137
  // Easy mode's body is the detail shell and nothing else — the cards moved to
142
138
  // the floating overlay over the chat. The Advanced branches below are kept
143
139
  // 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 {
@@ -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?.();
@@ -17,7 +17,7 @@ import { useSidebar } from '../../primitives/sidebar.js';
17
17
  import { Skeleton } from '../../primitives/skeleton.js';
18
18
  import { errorToast, successToast } from '../../primitives/toast.js';
19
19
  import { changeRequestKeys } from '../../files/project/hooks/use-change-requests.js';
20
- import { useSessionNeedsYouCounts } from '../../internal/hooks/use-review-session-summary.js';
20
+ import { useSessionNeedsYouCounts, useSessionsAwaitingAnswer, } from '../../internal/hooks/use-review-session-summary.js';
21
21
  import { childSessionHref, readRuntimeSessionParam, } from '../../session/tool/tools/session-spawn-urls.js';
22
22
  import { SessionStarterMark, useSessionStarter, } from '../sessions/session-starter-mark.js';
23
23
  import { SessionsEmptyState } from '../sessions/sessions-empty-state.js';
@@ -26,11 +26,11 @@ import { SessionDeleteModal } from '../../session/actions/session-delete-modal.j
26
26
  import { SessionLabelsModal } from './modal/session-labels-modal.js';
27
27
  import { ShareSessionModal } from './modal/share-session-modal.js';
28
28
  import { getSessionDisplayTitle, groupChangeRequestsBySession, projectSessionsRefetchInterval, resolveSessionListViewState, shortRelative, starterSectionOf, } from '../../session/actions/project-session-list-helpers.js';
29
- import { MobileSessionCreatedTime, SessionBriefDescription, SessionBriefHoverCard, } from './session-brief-hover-card.js';
29
+ import { MobileSessionCreatedTime, SessionBriefDescription, SessionBriefCardHost, SessionBriefHoverCard, SubagentHoverCard, } from './session-brief-hover-card.js';
30
30
  import { SessionFilterMenu } from './session-filter-menu.js';
31
31
  import { groupSessions, } from './session-grouping.js';
32
32
  import { useSessionOpenIntent } from './session-open-intent.js';
33
- import { SessionStatusMark } from './session-status-mark.js';
33
+ import { isReviewOverdue, SessionStatusMark } from './session-status-mark.js';
34
34
  import { SessionTitle } from './session-title.js';
35
35
  import { useMediaQuery } from '../../internal/hooks/index.js';
36
36
  import { cn } from '../../internal/lib/utils.js';
@@ -40,7 +40,7 @@ import { selectCollapsedSections, selectGroupMode, selectHiddenSections, selectL
40
40
  import { shouldBeginSessionSwitch, useSessionSwitchStore } from '../../internal/stores/session-switch-store.js';
41
41
  import { directSubsessions, createProjectSession, listChangeRequests, restartProjectSession, sessionParentId, stopProjectSession, } from '@kortix/sdk';
42
42
  import { qk, useProjectSession, useProjectSessions, useRuntimeSupports, useSessionChildren, } from '@kortix/sdk/react';
43
- import { ArchiveIcon, CaretRightIcon, DotsThreeIcon, PushPinIcon, PushPinSlashIcon, GitForkIcon, PencilSimpleIcon, ArrowCounterClockwiseIcon as RotateCcw, ShareIcon as Share, SparkleIcon, SquareIcon as Square, TagIcon, TrashIcon, } from '@phosphor-icons/react';
43
+ import { ArchiveIcon, ArrowsSplitIcon, CaretRightIcon, DotsThreeIcon, PushPinIcon, PushPinSlashIcon, RobotIcon, GitForkIcon, PencilSimpleIcon, ArrowCounterClockwiseIcon as RotateCcw, ShareIcon as Share, SparkleIcon, SquareIcon as Square, TagIcon, TrashIcon, TreeViewIcon, } from '@phosphor-icons/react';
44
44
  import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
45
45
  import { formatDistanceToNowStrict } from 'date-fns';
46
46
  import { useUiPathname, useUiRouter, useUiSearchParams, useUiViewerId } from '../../providers/ui-provider.js';
@@ -227,6 +227,7 @@ export function ProjectSessionList({ projectId }) {
227
227
  // summary the footer "Review" pill reads) plus open agent questions and
228
228
  // connector approvals (KRTX-1739). A positive count makes a row `needs-you`.
229
229
  const needsYouBySession = useSessionNeedsYouCounts(projectId);
230
+ const awaitingAnswer = useSessionsAwaitingAnswer(projectId);
230
231
  // Grouping, ordering, and the two multi-select facets all live in the
231
232
  // persisted session-filter store (keyed by project) — see SessionFilterMenu,
232
233
  // which writes to the same store from the nested `⋯` menu.
@@ -302,7 +303,11 @@ export function ProjectSessionList({ projectId }) {
302
303
  const isSwitchTarget = switchingToSessionId === session.session_id;
303
304
  const children = directSubsessions(session);
304
305
  const spawnedCount = nested ? 0 : (session.child_count ?? 0);
305
- const spawnedOpen = spawnedCount > 0 && expandedIdSet.has(session.session_id);
306
+ // Sub-agents and spawned sessions open together, under one toggle. A row
307
+ // whose sub-agent is the open page stays open, so that row stays visible.
308
+ const childRunsOpen = children.length + spawnedCount > 0 &&
309
+ (expandedIdSet.has(session.session_id) ||
310
+ (!!isActive && children.some((child) => child.id === activeRuntimeSessionId)));
306
311
  return (_jsxs("div", { className: "space-y-px", children: [_jsx(ProjectSessionRow, { session: session, href: href, isActive: !!isActive && !activeRuntimeSessionId, isSwitching: isSwitchTarget, onNavigate: (event) => {
307
312
  if (switchingToSessionId && session.session_id === activeSessionId) {
308
313
  event.preventDefault();
@@ -313,7 +318,7 @@ export function ProjectSessionList({ projectId }) {
313
318
  if (shouldBeginSessionSwitch(event, session.session_id, activeSessionId)) {
314
319
  beginSessionSwitch(session.session_id);
315
320
  }
316
- }, displayTitle: getSessionDisplayTitle(session), inSharedGroup: inSharedGroup, childCount: children.length, spawnedCount: spawnedCount, spawnedOpen: spawnedOpen, onToggleSpawned: () => toggleExpanded(projectId, session.session_id), reviewCount: needsYouBySession[session.session_id] ?? 0, changeRequests: changeRequestsBySession.get(session.session_id) ?? [], canShowHoverCard: canShowSessionHoverCard, onDelete: (id, label) => setSessionToDelete({ id, label }), onPin: (id, pinned) => void listActions.setPinned(id, pinned), onArchive: (id, archived) => void listActions.setArchived(id, archived), onShare: (s) => setSessionToShare(s), onRename: (id, name) => setSessionToRename({ id, name }), onEditLabels: setSessionToLabel, onRestart: (id, label) => restartMutation.mutate({ sessionId: id, label }), isRestarting: restartMutation.isPending && restartMutation.variables?.sessionId === session.session_id, onStop: (id, label) => stopMutation.mutate({ sessionId: id, label }), isStopping: stopMutation.isPending && stopMutation.variables?.sessionId === session.session_id, canFork: runtimeForks &&
321
+ }, displayTitle: getSessionDisplayTitle(session), inSharedGroup: inSharedGroup, spawned: nested, subagentCount: children.length, spawnedCount: spawnedCount, childRunsOpen: childRunsOpen, onToggleChildRuns: () => toggleExpanded(projectId, session.session_id), reviewCount: needsYouBySession[session.session_id] ?? 0, awaitingAnswer: awaitingAnswer.has(session.session_id), changeRequests: changeRequestsBySession.get(session.session_id) ?? [], canShowHoverCard: canShowSessionHoverCard, onDelete: (id, label) => setSessionToDelete({ id, label }), onPin: (id, pinned) => void listActions.setPinned(id, pinned), onArchive: (id, archived) => void listActions.setArchived(id, archived), onShare: (s) => setSessionToShare(s), onRename: (id, name) => setSessionToRename({ id, name }), onEditLabels: setSessionToLabel, onRestart: (id, label) => restartMutation.mutate({ sessionId: id, label }), isRestarting: restartMutation.isPending && restartMutation.variables?.sessionId === session.session_id, onStop: (id, label) => stopMutation.mutate({ sessionId: id, label }), isStopping: stopMutation.isPending && stopMutation.variables?.sessionId === session.session_id, canFork: runtimeForks &&
317
322
  session.session_id === activeSessionId &&
318
323
  !!(session.runtime_session_id ?? session.opencode_session_id), isForking: forkSession.isPending &&
319
324
  forkSession.variables?.sessionId ===
@@ -325,28 +330,7 @@ export function ProjectSessionList({ projectId }) {
325
330
  onError: (err) => {
326
331
  errorToast(err instanceof Error ? err.message : tI18nComplete.raw('text32ad3abe4479'));
327
332
  },
328
- }) }), children.length > 0 && isActive && (
329
- // `ml-4` puts the trunk's local x=0 exactly under the parent's
330
- // icon center (px-2 + half of size-4, both 2 tokens = 4 tokens
331
- // total). No padding on this wrapper: padding here would shift an
332
- // absolutely-positioned child's own `left:0` inward with it (it
333
- // resolves against the padding box), throwing off the trunk.
334
- //
335
- // The trunk is ONE span for the whole block, not one per row: N
336
- // separate `top-4 bottom-0` segments stacked edge-to-edge should
337
- // in theory touch with zero gap, but sub-pixel rounding at each
338
- // row boundary showed up as visible hairline breaks.
339
- //
340
- // The trunk stops at the TOP of the last row (each row is a fixed
341
- // `h-8`, so that is `(N - 1) * 8` tokens). The last row's own
342
- // elbow draws the rest and curves away. Running the trunk to the
343
- // last row's center instead leaves a straight tail below the
344
- // point where the elbow starts to curve.
345
- _jsxs("div", { className: "relative ml-4", children: [children.length > 1 && (_jsx("span", { "aria-hidden": true, className: "border-border pointer-events-none absolute top-0 left-0 border-l-2", style: { height: `calc(var(--spacing) * ${(children.length - 1) * 8})` } })), children.map((child) => {
346
- const childHref = childSessionHref(href, child.id);
347
- const activeChild = !!isActive && activeRuntimeSessionId === child.id;
348
- return (_jsxs("div", { className: "relative h-8", children: [_jsx(SubAgentConnector, {}), _jsx("div", { style: { marginLeft: SUB_AGENT_CONNECTOR_RUN }, children: _jsx(ProjectSubsessionRow, { title: child.title || 'Sub-session', href: childHref, isActive: activeChild, updatedAt: child.updated_at }) })] }, child.id));
349
- })] })), spawnedOpen && (_jsx(SpawnedSessions, { projectId: projectId, parentId: session.session_id, renderNode: (child) => renderSessionNode(child, true) }))] }, session.session_id));
333
+ }) }), childRunsOpen && (_jsx(ChildRuns, { projectId: projectId, parentId: session.session_id, subagents: children, spawnedCount: spawnedCount, renderSubagent: (child) => (_jsx(ProjectSubsessionRow, { id: child.id, title: child.title || 'Sub-session', href: childSessionHref(href, child.id), isActive: !!isActive && activeRuntimeSessionId === child.id, createdAt: child.created_at, updatedAt: child.updated_at })), renderSpawned: (child) => renderSessionNode(child, true) }))] }, session.session_id));
350
334
  };
351
335
  // Everything below the header — skeleton, error, empty, or the grouped list.
352
336
  // Kept as one function so the header stays mounted across all four states
@@ -383,12 +367,12 @@ export function ProjectSessionList({ projectId }) {
383
367
  if (viewState !== 'more' && grouped.sections.length === 0 && visibleOtherCount === 0) {
384
368
  return (_jsx("div", { className: "text-muted-foreground px-2 pt-1 pb-2 text-xs", children: t('allSectionsHidden') }));
385
369
  }
386
- return (_jsxs(FadedScrollArea, { fadeColor: "from-background", className: "h-full min-h-0 space-y-px", children: [_jsx(StarterSection, { title: tPinArchive('section'), query: pinnedQuery, sessions: visiblePinned, open: !collapsedSectionIds.has(PINNED_SECTION_ID), onToggle: () => toggleSectionCollapsed(projectId, PINNED_SECTION_ID), renderNode: (session) => renderSessionNode(session, false) }), visiblePinned.length > 0 && (_jsx("div", { className: "px-2 py-1", children: _jsx(Separator, {}) })), grouped.sections.map((section) => (_jsx(SessionListSection, { section: section, projectId: projectId, sessions: sessions, reviewCountBySession: needsYouBySession, showHeader: grouped.showHeaders, open: !collapsedSectionIds.has(section.id), onOpenChange: () => toggleSectionCollapsed(projectId, section.id), children: section.sessions.map((session) => renderSessionNode(session, false)) }, section.id))), hasNextPage && (_jsx(ShowMoreButton, { loading: isFetchingNextPage, failed: isFetchNextPageError, label: t('loadMore'), onClick: () => fetchNextPage() })), _jsx(StarterSection, { title: t('startedBy.others'), query: sharedQuery, sessions: visibleShared, open: expandedIdSet.has('section:shared'), onToggle: () => toggleExpanded(projectId, 'section:shared'), renderNode: (session) => renderSessionNode(session, false, true) }), _jsx(StarterSection, { title: t('startedBy.automated'), query: automatedQuery, sessions: visibleAutomated, open: expandedIdSet.has('section:automated'), onToggle: () => toggleExpanded(projectId, 'section:automated'), renderNode: (session) => renderSessionNode(session, false) }), firstChatPending &&
370
+ return (_jsxs(FadedScrollArea, { fadeColor: "from-background", className: "h-full min-h-0 space-y-px", children: [_jsx(StarterSection, { title: tPinArchive('section'), query: pinnedQuery, sessions: visiblePinned, open: !collapsedSectionIds.has(PINNED_SECTION_ID), onToggle: () => toggleSectionCollapsed(projectId, PINNED_SECTION_ID), renderNode: (session) => renderSessionNode(session, false), caretOnHover: true }), visiblePinned.length > 0 && !grouped.showHeaders && (_jsx("div", { className: "px-2 py-1", children: _jsx(Separator, {}) })), grouped.sections.map((section) => (_jsx(SessionListSection, { section: section, projectId: projectId, sessions: sessions, reviewCountBySession: needsYouBySession, showHeader: grouped.showHeaders, open: !collapsedSectionIds.has(section.id), onOpenChange: () => toggleSectionCollapsed(projectId, section.id), children: section.sessions.map((session) => renderSessionNode(session, false)) }, section.id))), hasNextPage && (_jsx(ShowMoreButton, { loading: isFetchingNextPage, failed: isFetchNextPageError, label: t('loadMore'), onClick: () => fetchNextPage() })), _jsx(StarterSection, { title: t('startedBy.others'), query: sharedQuery, sessions: visibleShared, open: expandedIdSet.has('section:shared'), onToggle: () => toggleExpanded(projectId, 'section:shared'), renderNode: (session) => renderSessionNode(session, false, true) }), _jsx(StarterSection, { title: t('startedBy.automated'), query: automatedQuery, sessions: visibleAutomated, open: expandedIdSet.has('section:automated'), onToggle: () => toggleExpanded(projectId, 'section:automated'), renderNode: (session) => renderSessionNode(session, false), caretOnHover: true }), firstChatPending &&
387
371
  !hasNextPage &&
388
372
  !sharedQuery.hasNextPage &&
389
373
  !automatedQuery.hasNextPage && (_jsx(FirstChatRow, { projectId: projectId, isActive: pathname === uiRoutes().project(projectId) && isFirstChatRequested(searchParams) }))] }));
390
374
  }
391
- return (_jsxs("div", { className: "flex h-full min-h-0 flex-col space-y-px", children: [_jsx(SessionListHeader, { projectId: projectId, sessions: sessions, reviewCountBySession: needsYouBySession, onMenuOpenChange: holdPeek }), _jsx("div", { className: "flex min-h-0 flex-1 flex-col overflow-hidden", children: renderBody() }), _jsx(ShareSessionModal, { projectId: projectId, session: sessionToShare, open: !!sessionToShare, onOpenChange: (open) => !open && setSessionToShare(null), onSaved: () => queryClient.invalidateQueries({ queryKey: qk.project.sessionsScope(projectId) }) }), _jsx(SessionLabelsModal, { projectId: projectId, session: sessionToLabel, open: !!sessionToLabel, onOpenChange: (open) => !open && setSessionToLabel(null) }), _jsx(RenameSessionModal, { projectId: projectId, sessionId: sessionToRename?.id ?? null, currentName: sessionToRename?.name, open: !!sessionToRename, onOpenChange: (open) => !open && setSessionToRename(null) }), _jsx(SessionDeleteModal, { projectId: projectId, sessionId: sessionToDelete?.id ?? null, sessionLabel: sessionToDelete?.label, open: !!sessionToDelete, onOpenChange: (open) => !open && setSessionToDelete(null) })] }));
375
+ return (_jsxs("div", { className: "flex h-full min-h-0 flex-col space-y-px", children: [_jsx(SessionListHeader, { projectId: projectId, sessions: sessions, reviewCountBySession: needsYouBySession, onMenuOpenChange: holdPeek }), _jsx("div", { className: "flex min-h-0 flex-1 flex-col overflow-hidden", children: renderBody() }), _jsx(ShareSessionModal, { projectId: projectId, session: sessionToShare, open: !!sessionToShare, onOpenChange: (open) => !open && setSessionToShare(null), onSaved: () => queryClient.invalidateQueries({ queryKey: qk.project.sessionsScope(projectId) }) }), _jsx(SessionLabelsModal, { projectId: projectId, session: sessionToLabel, open: !!sessionToLabel, onOpenChange: (open) => !open && setSessionToLabel(null) }), _jsx(RenameSessionModal, { projectId: projectId, sessionId: sessionToRename?.id ?? null, currentName: sessionToRename?.name, open: !!sessionToRename, onOpenChange: (open) => !open && setSessionToRename(null) }), _jsx(SessionDeleteModal, { projectId: projectId, sessionId: sessionToDelete?.id ?? null, sessionLabel: sessionToDelete?.label, open: !!sessionToDelete, onOpenChange: (open) => !open && setSessionToDelete(null) }), canShowSessionHoverCard && _jsx(SessionBriefCardHost, {})] }));
392
376
  }
393
377
  /** The `Sessions` row above the list. Lives here — not in `project-sidebar.tsx`
394
378
  * — because everything it needs (the session list, the review summary, the
@@ -462,58 +446,73 @@ function SessionSectionMenu({ projectId, sessions, reviewCountBySession, }) {
462
446
  }
463
447
  }, children: _jsx(DotsThreeIcon, { className: "size-4" }) }) }), _jsx(SessionFilterMenu, { projectId: projectId, sessions: sessions, reviewCountBySession: reviewCountBySession, align: "start" })] }));
464
448
  }
465
- /** Chevron + count of the sessions a row spawned. A `span`, not a button: it
466
- * lives inside the row's link, so it stops the click before it navigates.
467
- * Without an indicator strip beside it, it keeps the `⋯` slot free at rest,
468
- * so hovering the row never moves it. */
469
- function SpawnedToggle({ count, open, onToggle, clearOfMenu, }) {
449
+ /** Tree icon + count of everything this session started: its sub-agents and
450
+ * the sessions it spawned. A `span`, not a button: it lives inside the row's
451
+ * link, so it stops the click before it navigates. Without an indicator strip
452
+ * beside it, it keeps the `⋯` slot free at rest, so hovering the row never
453
+ * moves it. */
454
+ function ChildRunsToggle({ count, open, onToggle, clearOfMenu, }) {
470
455
  const t = useTranslations('sidebar.sessionList');
471
456
  const toggle = (event) => {
472
457
  event.preventDefault();
473
458
  event.stopPropagation();
474
459
  onToggle();
475
460
  };
476
- return (_jsxs("span", { role: "button", tabIndex: 0, "aria-expanded": open, "aria-label": open ? t('collapseChildren') : t('expandChildren', { count }), "data-session-children-toggle": "true", onClick: toggle, onKeyDown: (event) => {
461
+ return (_jsx("span", { role: "button", tabIndex: 0, "aria-expanded": open, "aria-label": open ? t('collapseChildren') : t('expandChildren', { count }), "data-session-children-toggle": "true", onClick: toggle, onKeyDown: (event) => {
477
462
  if (event.key === 'Enter' || event.key === ' ')
478
463
  toggle(event);
479
- }, className: cn('text-muted-foreground hover:text-sidebar-foreground flex shrink-0 items-center gap-0.5 rounded-sm text-xs tabular-nums', 'focus-visible:ring-kortix-base focus-visible:ring-[0.6px] focus-visible:outline-none',
464
+ }, className: cn('flex shrink-0 items-center rounded-sm', open ? 'text-sidebar-foreground' : 'text-muted-foreground hover:text-sidebar-foreground', 'focus-visible:ring-kortix-base focus-visible:ring-[0.6px] focus-visible:outline-none',
480
465
  // Reserve the `⋯` slot at rest. A toggle that slid out of the way on
481
466
  // hover moved away from the pointer and put the menu under the click.
482
- clearOfMenu && 'mr-6'), children: [_jsx(CaretRightIcon, { "aria-hidden": true, className: cn('size-3 transition-transform', open && 'rotate-90') }), count] }));
467
+ clearOfMenu && 'mr-6'), children: _jsx(CountBadge, { count: count, icon: _jsx(TreeViewIcon, { "aria-hidden": true, className: "size-3.5" }), className: cn(open && 'bg-foreground/12') }) }));
483
468
  }
484
469
  /**
485
470
  * The Shared and Automated sections: top-level sessions another member or an
486
471
  * automation started. Closed until opened, and absent while the viewer has none.
487
472
  */
488
- function StarterSection({ title, query, sessions, open, onToggle, renderNode, }) {
473
+ function StarterSection({ title, query, sessions, open, onToggle, renderNode, caretOnHover = false, }) {
489
474
  const t = useTranslations('sidebar');
490
475
  // A section with no row yet but a next page stays, with its Load more: the
491
476
  // rows the viewer can see may sit past the first pages (KRTX-1727).
492
477
  if (query.sessions.length === 0 && !query.hasNextPage)
493
478
  return null;
494
- return (_jsxs(Disclosure, { open: open, onOpenChange: onToggle, className: "group/section space-y-1", transition: { duration: 0.15, ease: 'easeOut' }, children: [_jsx(DisclosureTrigger, { children: _jsxs("div", { className: cn('group/section-header text-muted-foreground flex items-center gap-1 px-2 text-sm font-medium', SESSION_ROW_HEIGHT_CLASS), children: [_jsx("span", { className: "truncate", children: title }), _jsx(CaretRightIcon, { "aria-hidden": true, className: "duration-normal size-3 shrink-0 transition-transform ease-out group-data-[state=open]/section:rotate-90" })] }) }), _jsxs(DisclosureContent, { contentClassName: "space-y-px", children: [sessions.map(renderNode), query.hasNextPage && (_jsx(ShowMoreButton, { loading: query.isFetchingNextPage, failed: query.isFetchNextPageError, label: t('loadMore'), onClick: () => query.fetchNextPage() }))] })] }));
479
+ return (_jsxs(Disclosure, { open: open, onOpenChange: onToggle, className: "group/section space-y-1", transition: { duration: 0.15, ease: 'easeOut' }, children: [_jsx(DisclosureTrigger, { children: _jsxs("div", { className: cn('group/section-header text-muted-foreground flex items-center gap-1 px-2 text-sm font-medium', SESSION_ROW_HEIGHT_CLASS), children: [_jsx("span", { className: "truncate", children: title }), _jsx(CaretRightIcon, { "aria-hidden": true, className: cn('duration-normal size-3 shrink-0 transition-[transform,opacity] ease-out group-data-[state=open]/section:rotate-90', caretOnHover &&
480
+ 'opacity-0 group-hover/section-header:opacity-100 group-focus-visible/section-header:opacity-100') })] }) }), _jsxs(DisclosureContent, { contentClassName: "space-y-px", children: [sessions.map(renderNode), query.hasNextPage && (_jsx(ShowMoreButton, { loading: query.isFetchingNextPage, failed: query.isFetchNextPageError, label: t('loadMore'), onClick: () => query.fetchNextPage() }))] })] }));
495
481
  }
496
482
  function ShowMoreButton({ loading, failed, label, onClick, }) {
497
483
  const t = useTranslations('sidebar');
498
484
  return (_jsx(Button, { variant: "ghost", size: "sm", className: "text-muted-foreground hover:text-foreground h-8 w-full justify-start px-2 text-xs", disabled: loading, onClick: onClick, children: loading ? t('loadingMore') : failed ? t('retry') : label }));
499
485
  }
500
486
  /**
501
- * The sessions one parent spawned, fetched when the parent opens, 20 at a time.
502
- * Drawn on the same trunk-and-elbow connector as the sub-agent rows.
487
+ * Everything one session started, on ONE trunk: its sub-agents first, then the
488
+ * sessions it spawned (fetched when the row opens, 20 at a time). `ml-4` puts
489
+ * the trunk's x=0 under the parent's icon center (px-2 + half of size-4).
503
490
  */
491
+ function ChildRuns({ projectId, parentId, subagents, spawnedCount, renderSubagent, renderSpawned, }) {
492
+ return (_jsxs("div", { className: "ml-4", "data-session-children": parentId, children: [subagents.map((child, index) => (_jsx(ChildRunBranch, { continues: index < subagents.length - 1 || spawnedCount > 0, children: renderSubagent(child) }, child.id))), spawnedCount > 0 && (_jsx(SpawnedSessions, { projectId: projectId, parentId: parentId, renderNode: renderSpawned }))] }));
493
+ }
494
+ /**
495
+ * One child on the trunk. A child with siblings below it carries the trunk
496
+ * through its whole height; the last one ends at its own elbow. Each segment
497
+ * overlaps the next by 1px: edge-to-edge segments showed hairline breaks from
498
+ * sub-pixel rounding at row boundaries.
499
+ */
500
+ function ChildRunBranch({ continues, children }) {
501
+ return (_jsxs("div", { className: "relative", children: [continues && (_jsx("span", { "aria-hidden": true, className: "border-border pointer-events-none absolute top-0 -bottom-px left-0 border-l-2" })), _jsx(SubAgentConnector, {}), _jsx("div", { style: { marginLeft: SUB_AGENT_CONNECTOR_RUN }, children: children })] }));
502
+ }
504
503
  function SpawnedSessions({ projectId, parentId, renderNode, }) {
505
504
  const t = useTranslations('sidebar');
506
505
  const children = useSessionChildren(projectId, parentId, { limit: SIDEBAR_PAGE_SIZE });
507
506
  if (children.data === undefined) {
508
- return (_jsx("div", { className: "ml-4 px-2", children: _jsx(Skeleton, { className: "h-3 w-24 py-0", "aria-hidden": true }) }));
507
+ return (_jsx("div", { className: "px-2", children: _jsx(Skeleton, { className: "h-3 w-24 py-0", "aria-hidden": true }) }));
509
508
  }
510
509
  if (children.isError && children.sessions.length === 0) {
511
- return _jsx("p", { className: "text-destructive/80 ml-4 px-2 text-xs", children: t('sessionList.loadError') });
510
+ return _jsx("p", { className: "text-destructive/80 px-2 text-xs", children: t('sessionList.loadError') });
512
511
  }
513
512
  const rows = children.sessions;
514
- return (_jsxs("div", { className: "ml-4", "data-session-children": parentId, children: [rows.map((child, index) => (_jsxs("div", { className: "relative", children: [(index < rows.length - 1 || children.hasNextPage) && (_jsx("span", { "aria-hidden": true, className: "border-border pointer-events-none absolute top-0 bottom-0 left-0 border-l-2" })), _jsx(SubAgentConnector, {}), _jsx("div", { style: { marginLeft: SUB_AGENT_CONNECTOR_RUN }, children: renderNode(child) })] }, child.session_id))), children.hasNextPage && (_jsxs("div", { className: "relative", children: [_jsx(SubAgentConnector, {}), _jsx("div", { style: { marginLeft: SUB_AGENT_CONNECTOR_RUN }, children: _jsx(ShowMoreButton, { loading: children.isFetchingNextPage, failed: children.isFetchNextPageError, label: t('sessionList.showMore'), onClick: () => children.fetchNextPage() }) })] }))] }));
513
+ return (_jsxs(_Fragment, { children: [rows.map((child, index) => (_jsx(ChildRunBranch, { continues: index < rows.length - 1 || children.hasNextPage, children: renderNode(child) }, child.session_id))), children.hasNextPage && (_jsx(ChildRunBranch, { continues: false, children: _jsx(ShowMoreButton, { loading: children.isFetchingNextPage, failed: children.isFetchNextPageError, label: t('sessionList.showMore'), onClick: () => children.fetchNextPage() }) }))] }));
515
514
  }
516
- function ProjectSessionRow({ session, href, isActive, isSwitching, onNavigate, displayTitle, onDelete, onPin, onArchive, onShare, onRename, onEditLabels, onRestart, isRestarting, onStop, isStopping, canFork = false, isForking = false, onFork, childCount = 0, spawnedCount = 0, spawnedOpen = false, onToggleSpawned, reviewCount = 0, changeRequests, canShowHoverCard, inSharedGroup = false, }) {
515
+ function ProjectSessionRow({ session, href, isActive, isSwitching, onNavigate, displayTitle, onDelete, onPin, onArchive, onShare, onRename, onEditLabels, onRestart, isRestarting, onStop, isStopping, canFork = false, isForking = false, onFork, spawned = false, subagentCount = 0, spawnedCount = 0, childRunsOpen = false, onToggleChildRuns, reviewCount = 0, awaitingAnswer = false, changeRequests, canShowHoverCard, inSharedGroup = false, }) {
517
516
  const tI18nComplete = useTranslations('hardcodedUi.i18nComplete');
518
517
  const t = useTranslations('sidebar');
519
518
  const tPinArchive = useTranslations('sidebar.pinArchive');
@@ -549,7 +548,8 @@ function ProjectSessionRow({ session, href, isActive, isSwitching, onNavigate, d
549
548
  // listing the very change requests that made it `needs-you`. Both now read the
550
549
  // same `reviewCount` prop, so they agree by construction rather than by luck.
551
550
  const displayStatus = sessionDisplayStatus(session, reviewCount);
552
- const sessionLink = (_jsxs(HoverPrefetchLink, { href: href, prefetch: true, onClick: onNavigate, ...openIntent, "aria-busy": isSwitching || undefined, "aria-current": isActive ? 'page' : undefined, "aria-describedby": descriptionId, className: "focus-visible:ring-kortix-base flex min-w-0 flex-1 items-center gap-2 self-stretch rounded-md py-1 focus-visible:ring-[0.6px] focus-visible:outline-none", children: [_jsx("div", { className: "size-4 shrink-0", children: _jsx(SessionStatusDot, { session: session, reviewCount: reviewCount }) }), isMeta && (_jsx(Hint, { side: "top", label: t('metaCoordinator'), children: _jsx("span", { className: "text-muted-foreground flex size-4 shrink-0 items-center justify-center", children: _jsx(SparkleIcon, { className: "size-3.5", weight: "fill" }) }) })), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx(SessionTitle, { title: displayTitle, className: cn('min-w-0', isActive && 'font-medium') }), _jsx("span", { className: "hidden max-md:block pointer-coarse:block [@media(hover:none)]:block", "aria-hidden": true, "data-session-mobile-created-at": "true", children: _jsx(MobileSessionCreatedTime, { createdAt: session.created_at }) })] }), childCount > 0 && _jsx(CountBadge, { count: childCount }), spawnedCount > 0 && onToggleSpawned && (_jsx(SpawnedToggle, { count: spawnedCount, open: spawnedOpen, onToggle: onToggleSpawned, clearOfMenu: !hasIndicators })), hasIndicators && (_jsxs("div", { className: cn('flex shrink-0 items-center gap-0 transition-none', 'max-md:hidden pointer-coarse:hidden [@media(hover:none)]:hidden', 'group-hover/session-list:pointer-events-none group-hover/session-list:opacity-0', 'group-has-data-[state=open]/session-list:pointer-events-none group-has-data-[state=open]/session-list:opacity-0'), "data-session-indicators": "true", children: [showStarter && (_jsx("span", { className: "flex size-5 shrink-0 items-center justify-center", "data-session-source": "true", "data-session-starter": starter.type, children: _jsx(Hint, { side: "top", label: t('startedByLabel', { name: starter.label }), children: _jsx("span", { className: "text-muted-foreground flex size-5 items-center justify-center", children: _jsx(SessionStarterMark, { session: session, starter: starter, iconClassName: "size-3", avatarClassName: "size-5" }) }) }) })), !inSharedGroup && _jsx(SessionSharedIcon, { session: session })] }))] }));
551
+ const waitingOn = awaitingAnswer ? 'answer' : 'review';
552
+ const sessionLink = (_jsxs(HoverPrefetchLink, { href: href, prefetch: true, onClick: onNavigate, ...openIntent, "aria-busy": isSwitching || undefined, "aria-current": isActive ? 'page' : undefined, "aria-describedby": descriptionId, className: "focus-visible:ring-kortix-base flex min-w-0 flex-1 items-center gap-2 self-stretch rounded-md py-1 focus-visible:ring-[0.6px] focus-visible:outline-none", children: [_jsx("div", { className: "size-4 shrink-0", children: spawned ? (_jsx(ArrowsSplitIcon, { "aria-hidden": true, className: "text-muted-foreground size-4" })) : (_jsx(SessionStatusDot, { session: session, reviewCount: reviewCount, waitingOn: waitingOn, changeRequests: changeRequests })) }), isMeta && (_jsx(Hint, { side: "top", label: t('metaCoordinator'), children: _jsx("span", { className: "text-muted-foreground flex size-4 shrink-0 items-center justify-center", children: _jsx(SparkleIcon, { className: "size-3.5", weight: "fill" }) }) })), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx(SessionTitle, { title: displayTitle, className: cn('min-w-0', isActive && 'font-medium') }), _jsx("span", { className: "hidden max-md:block pointer-coarse:block [@media(hover:none)]:block", "aria-hidden": true, "data-session-mobile-created-at": "true", children: _jsx(MobileSessionCreatedTime, { createdAt: session.created_at }) })] }), subagentCount + spawnedCount > 0 && onToggleChildRuns && (_jsx(ChildRunsToggle, { count: subagentCount + spawnedCount, open: childRunsOpen, onToggle: onToggleChildRuns, clearOfMenu: !hasIndicators })), hasIndicators && (_jsxs("div", { className: cn('flex shrink-0 items-center gap-0 transition-none', 'max-md:hidden pointer-coarse:hidden [@media(hover:none)]:hidden', 'group-hover/session-list:pointer-events-none group-hover/session-list:opacity-0', 'group-has-data-[state=open]/session-list:pointer-events-none group-has-data-[state=open]/session-list:opacity-0'), "data-session-indicators": "true", children: [showStarter && (_jsx("span", { className: "flex size-5 shrink-0 items-center justify-center", "data-session-source": "true", "data-session-starter": starter.type, children: _jsx(Hint, { side: "top", label: t('startedByLabel', { name: starter.label }), children: _jsx("span", { className: "text-muted-foreground flex size-5 items-center justify-center", children: _jsx(SessionStarterMark, { session: session, starter: starter, iconClassName: "size-3", avatarClassName: "size-5" }) }) }) })), !inSharedGroup && _jsx(SessionSharedIcon, { session: session })] }))] }));
553
553
  return (_jsx("div", { className: "group/session-list block", children: _jsxs("div", { className: cn(
554
554
  // --session-row-surface paints the row AND the title fade in the same
555
555
  // style pass. Do not transition background — transition-colors made the
@@ -558,7 +558,7 @@ function ProjectSessionRow({ session, href, isActive, isSwitching, onNavigate, d
558
558
  // the end fade can never disagree with the row fill.
559
559
  'relative flex h-8 cursor-pointer items-center gap-2 rounded-md px-2 transition-none', 'max-md:h-auto max-md:min-h-12 max-md:gap-1', '[@media(hover:none)]:h-auto [@media(hover:none)]:min-h-12 [@media(hover:none)]:gap-1', 'pointer-coarse:h-auto pointer-coarse:min-h-12 pointer-coarse:gap-1', isActive
560
560
  ? 'text-sidebar-foreground bg-(--session-row-surface) font-medium [--session-row-surface:var(--sidebar-row)]'
561
- : 'text-muted-foreground hover:text-sidebar-foreground bg-(--session-row-surface) [--session-row-surface:var(--background)] hover:[--session-row-surface:var(--sidebar-row)]'), children: [canShowHoverCard ? (_jsx(SessionBriefHoverCard, { sessionId: session.session_id, title: displayTitle, status: displayStatus, createdAt: session.created_at, source: source, changeRequests: changeRequests, labels: session.labels, projectId: session.project_id, children: sessionLink })) : (sessionLink), _jsx(SessionBriefDescription, { id: descriptionId, status: displayStatus, createdAt: session.created_at, source: source, changeRequests: changeRequests }), _jsxs(DropdownMenu, { open: menuOpen, onOpenChange: setMenuOpen, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", type: "button", "aria-label": t('sessionList.actionsFor', { title: displayTitle }), className: cn(SESSION_MENU_TRIGGER_CLASS, 'absolute top-1/2 right-1 z-10 -translate-y-1/2', 'bg-(--session-row-surface)', 'pointer-events-none opacity-0', 'group-hover/session-list:pointer-events-auto group-hover/session-list:opacity-100', 'focus-visible:pointer-events-auto focus-visible:opacity-100', 'data-[state=open]:pointer-events-auto data-[state=open]:opacity-100', SESSION_MENU_TRIGGER_TOUCH_CLASS), onClick: (e) => {
561
+ : 'text-muted-foreground hover:text-sidebar-foreground bg-(--session-row-surface) [--session-row-surface:var(--background)] hover:[--session-row-surface:var(--sidebar-row)]'), children: [canShowHoverCard ? (_jsx(SessionBriefHoverCard, { sessionId: session.session_id, title: displayTitle, status: displayStatus, createdAt: session.created_at, source: source, changeRequests: changeRequests, waitingOn: waitingOn, spawned: spawned, subagentCount: subagentCount, spawnedCount: spawnedCount, labels: session.labels, projectId: session.project_id, children: sessionLink })) : (sessionLink), _jsx(SessionBriefDescription, { id: descriptionId, status: displayStatus, createdAt: session.created_at, source: source, changeRequests: changeRequests, waitingOn: waitingOn }), _jsxs(DropdownMenu, { open: menuOpen, onOpenChange: setMenuOpen, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", type: "button", "aria-label": t('sessionList.actionsFor', { title: displayTitle }), className: cn(SESSION_MENU_TRIGGER_CLASS, 'absolute top-1/2 right-1 z-10 -translate-y-1/2', 'bg-(--session-row-surface)', 'pointer-events-none opacity-0', 'group-hover/session-list:pointer-events-auto group-hover/session-list:opacity-100', 'focus-visible:pointer-events-auto focus-visible:opacity-100', 'data-[state=open]:pointer-events-auto data-[state=open]:opacity-100', SESSION_MENU_TRIGGER_TOUCH_CLASS), onClick: (e) => {
562
562
  e.preventDefault();
563
563
  e.stopPropagation();
564
564
  }, children: _jsx(DotsThreeIcon, { className: "size-4" }) }) }), _jsxs(DropdownMenuContent, { align: "start", side: "right", className: "w-44", children: [_jsxs(DropdownMenuItem, { className: "cursor-pointer", onSelect: () => deferAfterClose(() => onRename(session.session_id, displayTitle)), children: [_jsx(PencilSimpleIcon, {}), tI18nComplete.raw('text3064d79a295c')] }), _jsxs(DropdownMenuItem, { className: "cursor-pointer", onSelect: () => deferAfterClose(() => onEditLabels(session)), children: [_jsx(TagIcon, {}), t('labels.menu')] }), _jsxs(DropdownMenuItem, { className: "cursor-pointer", onSelect: () => deferAfterClose(() => onShare(session)), children: [_jsx(Share, {}), session.can_manage_sharing !== false
@@ -596,16 +596,13 @@ const SUB_AGENT_CONNECTOR_RUN = 'calc(var(--spacing) * 3.5)';
596
596
  function SubAgentConnector() {
597
597
  return (_jsx("span", { "aria-hidden": true, className: "border-border pointer-events-none absolute top-0 left-0 h-4 rounded-bl-md border-b-2 border-l-2", style: { width: SUB_AGENT_CONNECTOR_RUN } }));
598
598
  }
599
- function ProjectSubsessionRow({ title, href, isActive, updatedAt, }) {
599
+ function ProjectSubsessionRow({ id, title, href, isActive, createdAt, updatedAt, }) {
600
600
  const relative = updatedAt
601
601
  ? shortRelative(formatDistanceToNowStrict(new Date(updatedAt), { addSuffix: false }))
602
602
  : '';
603
- return (
604
- // Same reason as ProjectSessionRow: sub-session rows are session routes too,
605
- // so prefetching them on mount multiplies the per-open RSC storm.
606
- _jsx(HoverPrefetchLink, { href: href, className: "block", children: _jsxs("div", { className: cn('flex h-8 cursor-pointer items-center gap-2 rounded-lg px-2 text-sm', isActive ? 'bg-sidebar-accent text-sidebar-foreground font-medium' : ''), children: [_jsx("span", { title: title, className: cn('flex-1 truncate', isActive && 'font-medium'), children: title }), relative && _jsx("span", { className: SESSION_RELATIVE_TIME_CLASS, children: relative })] }) }));
603
+ return (_jsx(SubagentHoverCard, { id: id, title: title, createdAt: createdAt, updatedAt: updatedAt, children: _jsx(HoverPrefetchLink, { href: href, className: "block", children: _jsxs("div", { className: cn('flex h-8 cursor-pointer items-center gap-2 rounded-lg px-2 text-sm', isActive ? 'bg-sidebar-accent text-sidebar-foreground font-medium' : ''), children: [_jsx(RobotIcon, { "aria-hidden": true, className: "text-muted-foreground size-4 shrink-0" }), _jsx("span", { className: cn('flex-1 truncate', isActive && 'font-medium'), children: title }), relative && _jsx("span", { className: SESSION_RELATIVE_TIME_CLASS, children: relative })] }) }) }));
607
604
  }
608
- function SessionStatusDot({ session, reviewCount = 0, }) {
605
+ function SessionStatusDot({ session, reviewCount = 0, waitingOn, changeRequests, }) {
609
606
  const display = sessionDisplayStatus(session, reviewCount);
610
- return (_jsx("div", { className: "flex size-4 shrink-0 items-center justify-center", children: _jsx(SessionStatusMark, { status: display }) }));
607
+ return (_jsx("div", { className: "flex size-4 shrink-0 items-center justify-center", children: _jsx(SessionStatusMark, { status: display, waitingOn: waitingOn, overdue: isReviewOverdue(changeRequests) }) }));
611
608
  }