@kortix/ui 0.14.4 → 0.14.5

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 (118) hide show
  1. package/README.md +8 -4
  2. package/dist/customize/sections/llm-provider/constants.d.ts +0 -1
  3. package/dist/customize/sections/llm-provider/constants.js +0 -2
  4. package/dist/customize/sections/llm-provider/models-tab-group.d.ts +1 -1
  5. package/dist/customize/sections/llm-provider/models-tab-group.js +7 -4
  6. package/dist/customize/sections/llm-provider/models-tab-row.d.ts +2 -18
  7. package/dist/customize/sections/llm-provider/models-tab-row.js +6 -30
  8. package/dist/customize/sections/llm-provider/models-tab.d.ts +0 -1
  9. package/dist/customize/sections/llm-provider/models-tab.js +1 -10
  10. package/dist/customize/sections/llm-provider/use-connected-providers.js +2 -2
  11. package/dist/customize/sections/llm-provider/utils.d.ts +1 -1
  12. package/dist/customize/sections/llm-provider/utils.js +7 -14
  13. package/dist/customize/sections/view/gateway/_shared.js +1 -2
  14. package/dist/customize/sections/view/gateway/gateway-routing.js +3 -2
  15. package/dist/customize/sections/view/sandbox-provider-coverage.d.ts +3 -18
  16. package/dist/customize/sections/view/sandbox-provider-coverage.js +7 -27
  17. package/dist/customize/settings/tabs/sandbox-tab.d.ts +2 -27
  18. package/dist/customize/settings/tabs/sandbox-tab.js +13 -112
  19. package/dist/customize/shared/entity/entity-modal.d.ts +5 -1
  20. package/dist/customize/shared/entity/entity-modal.js +7 -4
  21. package/dist/customize/skills/remove-skill-button.d.ts +16 -0
  22. package/dist/customize/skills/remove-skill-button.js +42 -0
  23. package/dist/customize/skills/skills-page.js +1 -1
  24. package/dist/customize/triggers/schedule/trigger-list.d.ts +0 -2
  25. package/dist/customize/triggers/schedule/trigger-list.js +1 -18
  26. package/dist/customize/triggers/schedule/trigger-tile.d.ts +1 -32
  27. package/dist/customize/triggers/schedule/trigger-tile.js +1 -16
  28. package/dist/files/project/hooks/use-change-requests.d.ts +14 -1
  29. package/dist/files/project/hooks/use-change-requests.js +44 -25
  30. package/dist/files/renderers/docx/docx-viewer.js +4 -1
  31. package/dist/files/renderers/pdf/pdf-renderer.d.ts +3 -1
  32. package/dist/files/renderers/pdf/pdf-renderer.js +2 -2
  33. package/dist/files/renderers/pdf/pdf-viewer.d.ts +3 -0
  34. package/dist/files/renderers/pdf/pdf-viewer.js +15 -6
  35. package/dist/files/viewer/file-content-renderer.d.ts +4 -1
  36. package/dist/files/viewer/file-content-renderer.js +2 -2
  37. package/dist/i18n/en.d.ts +26 -35
  38. package/dist/i18n/en.js +35 -44
  39. package/dist/index.d.ts +1 -1
  40. package/dist/internal/hooks/use-notification-center.js +21 -15
  41. package/dist/internal/hooks/use-review-session-summary.d.ts +6 -0
  42. package/dist/internal/hooks/use-review-session-summary.js +11 -0
  43. package/dist/internal/lib/browser-error-noise.d.ts +1 -1
  44. package/dist/internal/lib/browser-error-noise.js +1 -1
  45. package/dist/internal/lib/browser-noise/rules/rejections.d.ts +10 -0
  46. package/dist/internal/lib/browser-noise/rules/rejections.js +27 -1
  47. package/dist/internal/lib/provider-branding.js +14 -3
  48. package/dist/internal/lib/trigger-clock.d.ts +42 -0
  49. package/dist/internal/lib/trigger-clock.js +34 -0
  50. package/dist/internal/lib/web-notifications.d.ts +6 -0
  51. package/dist/internal/lib/web-notifications.js +57 -7
  52. package/dist/internal/notifications/notification-bell.d.ts +13 -0
  53. package/dist/internal/notifications/notification-bell.js +1 -1
  54. package/dist/internal/reminders/reminder-filter.d.ts +12 -0
  55. package/dist/internal/reminders/reminder-filter.js +34 -0
  56. package/dist/internal/reminders/reminder-format.d.ts +6 -0
  57. package/dist/internal/reminders/reminder-format.js +20 -2
  58. package/dist/internal/stores/kortix-computer-store.d.ts +1 -1
  59. package/dist/internal/stores/session-hover-store.d.ts +20 -5
  60. package/dist/internal/stores/session-hover-store.js +31 -13
  61. package/dist/internal/stores/session-panel-memory-store.js +2 -0
  62. package/dist/primitives/count-badge.d.ts +3 -1
  63. package/dist/primitives/count-badge.js +3 -3
  64. package/dist/primitives/glyph-paths-source.js +1 -1
  65. package/dist/primitives/glyph-paths.generated.js +1 -1
  66. package/dist/providers/ui-provider.d.ts +57 -0
  67. package/dist/providers/ui-provider.js +4 -0
  68. package/dist/session/composer/ComposerCard.d.ts +2 -0
  69. package/dist/session/composer/ComposerCard.js +5 -1
  70. package/dist/session/composer/agent-selector.js +12 -5
  71. package/dist/session/composer/composer-toolbar.js +4 -2
  72. package/dist/session/composer/composer-underbar.js +3 -1
  73. package/dist/session/composer/composer.js +34 -5
  74. package/dist/session/composer/menus/slash-actions.d.ts +22 -2
  75. package/dist/session/composer/menus/slash-actions.js +20 -0
  76. package/dist/session/composer/menus/slash-items.js +19 -3
  77. package/dist/session/header/reminder-parts.d.ts +285 -0
  78. package/dist/session/header/reminder-parts.js +115 -0
  79. package/dist/session/header/session-reminders-indicator.d.ts +13 -0
  80. package/dist/session/header/session-reminders-indicator.js +142 -35
  81. package/dist/session/models/model-availability.d.ts +23 -0
  82. package/dist/session/models/model-availability.js +20 -0
  83. package/dist/session/models/model-grouping.js +1 -4
  84. package/dist/session/models/model-selector.js +23 -14
  85. package/dist/session/models/use-model-connection-gate.js +4 -1
  86. package/dist/session/outcomes/session-outcomes-provider.d.ts +3 -1
  87. package/dist/session/outcomes/session-outcomes-provider.js +31 -3
  88. package/dist/session/overrides/session-overrides-composer.d.ts +1 -1
  89. package/dist/session/overrides/session-overrides-composer.js +5 -1
  90. package/dist/session/panel/mobile-tool-drawer.js +4 -2
  91. package/dist/session/panel/open-session-quick-view.js +2 -1
  92. package/dist/session/panel/panel-fallback.js +8 -4
  93. package/dist/session/panel/session-panel-provider.d.ts +1 -0
  94. package/dist/session/panel/session-panel-provider.js +13 -1
  95. package/dist/session/panel/session-reminders-panel.d.ts +4 -0
  96. package/dist/session/panel/session-reminders-panel.js +109 -0
  97. package/dist/session/panel/shared/use-panel-handoffs.d.ts +2 -2
  98. package/dist/session/panel/shared/use-panel-handoffs.js +5 -1
  99. package/dist/session/print/session-print-header.d.ts +0 -10
  100. package/dist/session/print/session-print-header.js +16 -1
  101. package/dist/session/status/session-surface.js +1 -1
  102. package/dist/session/thread/optimistic-turn.d.ts +5 -1
  103. package/dist/session/thread/optimistic-turn.js +8 -6
  104. package/dist/session/thread/session-chat.d.ts +6 -1
  105. package/dist/session/thread/session-chat.js +22 -3
  106. package/dist/session/thread/session-thread.d.ts +10 -1
  107. package/dist/session/thread/session-thread.js +2 -2
  108. package/dist/session/turn/burst-step-body.js +4 -0
  109. package/dist/styles.css +1 -1
  110. package/dist/workspace/sidebar/project-session-list.js +52 -55
  111. package/dist/workspace/sidebar/session-brief-hover-card.d.ts +35 -18
  112. package/dist/workspace/sidebar/session-brief-hover-card.js +114 -37
  113. package/dist/workspace/sidebar/session-status-mark.d.ts +15 -1
  114. package/dist/workspace/sidebar/session-status-mark.js +63 -17
  115. package/package.json +7 -3
  116. package/skills/kortix-ui/SKILL.md +1 -1
  117. package/dist/customize/sections/view/sandbox-provider-result.d.ts +0 -47
  118. package/dist/customize/sections/view/sandbox-provider-result.js +0 -79
@@ -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
  }
@@ -9,31 +9,36 @@ interface SessionBrief {
9
9
  changeRequests: readonly ChangeRequest[];
10
10
  /** The session's free-form labels; the sidebar row has no room for them. */
11
11
  labels?: readonly string[];
12
+ /** `needs-you` only: what the session waits on, see `SessionStatusMark`. */
13
+ waitingOn?: 'answer' | 'review';
14
+ /** The session was spawned by another one and is drawn under it. */
15
+ spawned?: boolean;
16
+ /** What this session started: its sub-agents and the sessions it spawned. */
17
+ subagentCount?: number;
18
+ spawnedCount?: number;
12
19
  }
13
20
  interface SessionBriefInteractionProps {
14
21
  projectId: string;
15
22
  }
16
23
  /**
17
- * Radix HoverCard strips every tab stop inside its content — `getTabbableNodes`
18
- * runs on each render and sets `tabindex="-1"` — because a hover card is a
19
- * preview surface, not a menu. Pointer clicks and `cmd`-clicks on the change
20
- * rows still work; the keyboard route to the same changes is the footer's
21
- * Review pill, which is a Popover and therefore fully tabbable. Do not try to
22
- * restore focusability here: the effect has no dependency array and will
23
- * overwrite whatever you set on the next commit.
24
+ * The one card for the whole session list. Rows do not own a card: the active
25
+ * row renders its brief into this host's slot (`SessionBriefCard`), and the
26
+ * host lines up with that row. Moving to the next row therefore moves the same
27
+ * card, with a 150ms glide, instead of unmounting one card and mounting another
28
+ * at a new place. Radix HoverCard cannot do this: it exports no `Anchor`, so a
29
+ * single instance cannot follow the pointer between triggers.
24
30
  *
25
- * Timing and exclusivity belong to `useSessionHoverStore`, not to this instance,
26
- * because both are properties of the LIST: only one card may be open at a time,
27
- * and the second row the pointer visits must not pay the opening delay again.
28
- * Radix's own delays are zeroed and its `onOpenChange` left unwired — with
29
- * `closeDelay={0}` it would report a close the instant the pointer left the row,
30
- * cancelling the window in which the pointer travels into the card. The store is
31
- * the only thing that decides whether this card is showing.
31
+ * The first open fades in over 100ms and does not move: the card opens to the
32
+ * right, into the path the pointer is already taking. The glide is only for a
33
+ * move between rows, never for keyboard focus or reduced motion.
32
34
  *
33
- * `animated={false}` for the reason submenus opt out of their enter animation:
34
- * the card opens to the right, into the path the pointer is already taking, so
35
- * an enter animation is time spent moving content away from the hand.
35
+ * The left padding is the gap between row and card. It is part of the card's
36
+ * box, so the pointer never leaves on its way in and the card needs no close
37
+ * grace for the crossing beyond the store's own.
38
+ *
39
+ * Mount it once, client-side only, where hover cards can show at all.
36
40
  */
41
+ export declare function SessionBriefCardHost(): import("react").ReactPortal;
37
42
  export declare function SessionBriefHoverCard({ sessionId, children, ...brief }: SessionBrief & SessionBriefInteractionProps & {
38
43
  sessionId: string;
39
44
  children: ReactElement;
@@ -42,10 +47,22 @@ export declare function SessionBriefHoverCard({ sessionId, children, ...brief }:
42
47
  * The same brief remains available to keyboard and screen-reader users when
43
48
  * the visual HoverCard is absent or intentionally ignored by assistive tech.
44
49
  */
45
- export declare function SessionBriefDescription({ id, status, createdAt, source, changeRequests, }: Omit<SessionBrief, 'title'> & {
50
+ export declare function SessionBriefDescription({ id, status, createdAt, source, changeRequests, waitingOn, }: Omit<SessionBrief, 'title'> & {
46
51
  id: string;
47
52
  }): import("react").JSX.Element;
48
53
  export declare function MobileSessionCreatedTime({ createdAt }: {
49
54
  createdAt: string;
50
55
  }): import("react").JSX.Element;
56
+ /**
57
+ * The hover card of a sub-agent row: what it is and when it ran. Same store,
58
+ * same shell as `SessionBriefHoverCard`, so one card is open at a time across
59
+ * sessions, sub-agents and spawned children, and moving between them is free.
60
+ */
61
+ export declare function SubagentHoverCard({ id, title, createdAt, updatedAt, children, }: {
62
+ id: string;
63
+ title: string;
64
+ createdAt: number | null;
65
+ updatedAt: number | null;
66
+ children: ReactElement;
67
+ }): import("react").JSX.Element;
51
68
  export {};
@@ -2,20 +2,25 @@
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { HoverPrefetchLink } from '../../internal/components/hover-prefetch-link.js';
4
4
  import { SESSION_STATUS_TRANSLATION_KEY, } from '../../session/actions/session-label.js';
5
- import { HoverCard, HoverCardContent, HoverCardTrigger } from '../../primitives/hover-card.js';
5
+ import { FLOATING_PANEL_SURFACE } from '../../primitives/menu-recipe.js';
6
+ import { usePortalContainer } from '../../lib/portal-container.js';
7
+ import { cn } from '../../lib/utils.js';
8
+ import { floatingZ, useDialogDepth } from '../../lib/z-stack.js';
6
9
  import { LocalTime } from '../../primitives/local-time.js';
7
10
  import { menuRow } from '../../primitives/menu-recipe.js';
8
11
  import { CR_ID_PREFIX } from '../review/review-actions.js';
9
12
  import { reviewHref } from '../../internal/lib/capability-tab-routes.js';
10
13
  import { useTranslations } from '../../i18n/index.js';
11
14
  import { useSessionHoverStore } from '../../internal/stores/session-hover-store.js';
12
- import { GitDiffIcon } from '@phosphor-icons/react';
15
+ import { ArrowsSplitIcon, GitDiffIcon, RobotIcon } from '@phosphor-icons/react';
13
16
  import { formatDistanceToNowStrict } from 'date-fns';
14
- import { useEffect } from 'react';
17
+ import { Slot } from 'radix-ui';
18
+ import { useEffect, useLayoutEffect, useRef } from 'react';
19
+ import { createPortal } from 'react-dom';
15
20
  import { shortRelative } from '../../session/actions/project-session-list-helpers.js';
16
21
  import { SessionLabelBadges } from '../sessions/session-label-badges.js';
17
22
  import { SOURCE_ICONS } from './session-source-icons.js';
18
- import { SessionStatusMark } from './session-status-mark.js';
23
+ import { isReviewOverdue, SessionStatusMark } from './session-status-mark.js';
19
24
  const DATE_TIME_OPTIONS = {
20
25
  year: 'numeric',
21
26
  month: 'short',
@@ -40,8 +45,10 @@ const CHANGE_REQUEST_STATUS_KEY = {
40
45
  merged: 'changeRequestStatus.merged',
41
46
  closed: 'changeRequestStatus.closed',
42
47
  };
43
- function useStatusLabel(status) {
48
+ function useStatusLabel(status, waitingOn) {
44
49
  const t = useTranslations('sidebar.sessionList.status');
50
+ if (status === 'needs-you' && waitingOn === 'answer')
51
+ return t('needsInput');
45
52
  return t(SESSION_STATUS_TRANSLATION_KEY[status]);
46
53
  }
47
54
  function SessionCreatedTime({ createdAt, className }) {
@@ -87,59 +94,118 @@ function ChangeRequestRow({ changeRequest, projectId, onDismiss, }) {
87
94
  * `px-2.5`, so 3.5 spacing steps is exactly where the row labels start. The
88
95
  * title and the change titles share one left edge.
89
96
  */
90
- function SessionBriefContent({ title, status, createdAt, source, changeRequests, labels, projectId, onDismiss, }) {
97
+ function SessionBriefContent({ title, status, createdAt, source, changeRequests, labels, waitingOn, spawned = false, subagentCount = 0, spawnedCount = 0, projectId, onDismiss, }) {
91
98
  const SourceIcon = source.kind === 'chat' ? null : SOURCE_ICONS[source.kind];
92
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "space-y-1.5 px-3.5 py-2.5", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-2", children: [_jsx("p", { className: "text-foreground truncate text-sm font-medium", children: title }), _jsx("span", { className: "shrink-0", children: _jsx(SessionStatusMark, { status: status }) })] }), _jsx(RelativeCreatedTime, { createdAt: createdAt })] }), SourceIcon ? (_jsxs("div", { className: "text-muted-foreground flex min-w-0 items-center gap-2 text-xs", children: [_jsx(SourceIcon, { className: "size-3.5 shrink-0" }), _jsxs("span", { className: "text-foreground min-w-0 truncate", children: [source.label, source.triggerSlug ? (_jsxs("span", { className: "text-muted-foreground", children: [" \u00B7 ", source.triggerSlug] })) : null] })] })) : null, labels?.length ? _jsx(SessionLabelBadges, { session: { labels: [...labels] }, max: 6, className: "flex-wrap" }) : null] }), changeRequests.length > 0 ? (_jsx("ul", { className: "border-border max-h-64 overflow-y-auto overscroll-contain border-t p-1", children: changeRequests.map((changeRequest) => (_jsx("li", { children: _jsx(ChangeRequestRow, { changeRequest: changeRequest, projectId: projectId, onDismiss: onDismiss }) }, changeRequest.cr_id))) })) : null] }));
99
+ const statusLabel = useStatusLabel(status, waitingOn);
100
+ const t = useTranslations('sidebar.sessionList.brief');
101
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "space-y-1.5 px-3.5 py-2.5", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("p", { className: "text-foreground min-w-0 flex-1 truncate text-sm font-medium", children: title }), _jsx(RelativeCreatedTime, { createdAt: createdAt })] }), _jsxs("div", { className: "text-muted-foreground flex items-center gap-2 text-xs", children: [_jsx(SessionStatusMark, { status: status, waitingOn: waitingOn, overdue: isReviewOverdue(changeRequests) }), _jsx("span", { children: statusLabel })] }), spawned && (_jsxs("div", { className: "text-muted-foreground flex items-center gap-2 text-xs", children: [_jsx(ArrowsSplitIcon, { "aria-hidden": true, className: "size-4 shrink-0" }), _jsx("span", { children: t('spawned') })] })), subagentCount > 0 && (_jsxs("div", { className: "text-muted-foreground flex items-center gap-2 text-xs", children: [_jsx(RobotIcon, { "aria-hidden": true, className: "size-4 shrink-0" }), _jsx("span", { children: t('subAgentCount', { count: subagentCount }) })] })), spawnedCount > 0 && (_jsxs("div", { className: "text-muted-foreground flex items-center gap-2 text-xs", children: [_jsx(ArrowsSplitIcon, { "aria-hidden": true, className: "size-4 shrink-0" }), _jsx("span", { children: t('spawnedCount', { count: spawnedCount }) })] })), SourceIcon ? (_jsxs("div", { className: "text-muted-foreground flex min-w-0 items-center gap-2 text-xs", children: [_jsx(SourceIcon, { className: "size-3.5 shrink-0" }), _jsxs("span", { className: "text-foreground min-w-0 truncate", children: [source.label, source.triggerSlug ? (_jsxs("span", { className: "text-muted-foreground", children: [" \u00B7 ", source.triggerSlug] })) : null] })] })) : null, labels?.length ? _jsx(SessionLabelBadges, { session: { labels: [...labels] }, max: 6, className: "flex-wrap" }) : null] }), changeRequests.length > 0 ? (_jsx("ul", { className: "border-border max-h-64 overflow-y-auto overscroll-contain border-t p-1", children: changeRequests.map((changeRequest) => (_jsx("li", { children: _jsx(ChangeRequestRow, { changeRequest: changeRequest, projectId: projectId, onDismiss: onDismiss }) }, changeRequest.cr_id))) })) : null] }));
93
102
  }
103
+ const CARD_EDGE_PX = 8;
94
104
  /**
95
- * Radix HoverCard strips every tab stop inside its content — `getTabbableNodes`
96
- * runs on each render and sets `tabindex="-1"` — because a hover card is a
97
- * preview surface, not a menu. Pointer clicks and `cmd`-clicks on the change
98
- * rows still work; the keyboard route to the same changes is the footer's
99
- * Review pill, which is a Popover and therefore fully tabbable. Do not try to
100
- * restore focusability here: the effect has no dependency array and will
101
- * overwrite whatever you set on the next commit.
105
+ * The one card for the whole session list. Rows do not own a card: the active
106
+ * row renders its brief into this host's slot (`SessionBriefCard`), and the
107
+ * host lines up with that row. Moving to the next row therefore moves the same
108
+ * card, with a 150ms glide, instead of unmounting one card and mounting another
109
+ * at a new place. Radix HoverCard cannot do this: it exports no `Anchor`, so a
110
+ * single instance cannot follow the pointer between triggers.
111
+ *
112
+ * The first open fades in over 100ms and does not move: the card opens to the
113
+ * right, into the path the pointer is already taking. The glide is only for a
114
+ * move between rows, never for keyboard focus or reduced motion.
102
115
  *
103
- * Timing and exclusivity belong to `useSessionHoverStore`, not to this instance,
104
- * because both are properties of the LIST: only one card may be open at a time,
105
- * and the second row the pointer visits must not pay the opening delay again.
106
- * Radix's own delays are zeroed and its `onOpenChange` left unwired — with
107
- * `closeDelay={0}` it would report a close the instant the pointer left the row,
108
- * cancelling the window in which the pointer travels into the card. The store is
109
- * the only thing that decides whether this card is showing.
116
+ * The left padding is the gap between row and card. It is part of the card's
117
+ * box, so the pointer never leaves on its way in and the card needs no close
118
+ * grace for the crossing beyond the store's own.
110
119
  *
111
- * `animated={false}` for the reason submenus opt out of their enter animation:
112
- * the card opens to the right, into the path the pointer is already taking, so
113
- * an enter animation is time spent moving content away from the hand.
120
+ * Mount it once, client-side only, where hover cards can show at all.
114
121
  */
115
- export function SessionBriefHoverCard({ sessionId, children, ...brief }) {
116
- const open = useSessionHoverStore((state) => state.activeSessionId === sessionId);
122
+ export function SessionBriefCardHost() {
123
+ const activeSessionId = useSessionHoverStore((state) => state.activeSessionId);
124
+ const anchor = useSessionHoverStore((state) => state.anchor);
125
+ const via = useSessionHoverStore((state) => state.via);
126
+ const setSlot = useSessionHoverStore((state) => state.setSlot);
117
127
  const openSession = useSessionHoverStore((state) => state.openSession);
118
128
  const closeSession = useSessionHoverStore((state) => state.closeSession);
119
129
  const dismiss = useSessionHoverStore((state) => state.dismiss);
130
+ const container = usePortalContainer();
131
+ const depth = useDialogDepth();
132
+ const wrapperRef = useRef(null);
133
+ const shown = useRef(false);
134
+ const visible = !!activeSessionId && !!anchor;
135
+ // Before paint: line up with the row, kept inside the viewport.
136
+ useLayoutEffect(() => {
137
+ const el = wrapperRef.current;
138
+ if (!el || !visible || !anchor) {
139
+ shown.current = false;
140
+ return;
141
+ }
142
+ const rect = anchor.getBoundingClientRect();
143
+ const top = Math.max(CARD_EDGE_PX, Math.min(rect.top, window.innerHeight - CARD_EDGE_PX - el.offsetHeight));
144
+ const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
145
+ // A move between rows glides; the first open fades in, as the reminders card does.
146
+ el.style.transition =
147
+ shown.current && via === 'pointer' && !reduce
148
+ ? 'transform var(--duration-normal) var(--ease-in-out)'
149
+ : 'opacity var(--duration-fast) var(--ease-out)';
150
+ el.style.transform = `translate(${rect.right}px, ${top}px)`;
151
+ shown.current = true;
152
+ }, [visible, activeSessionId, anchor, via]);
153
+ // WCAG 1.4.13: dismissible without moving the pointer. A scroll moves the
154
+ // row away from the card, so it closes too, unless it scrolls the card itself.
155
+ useEffect(() => {
156
+ if (!visible)
157
+ return;
158
+ const onKey = (event) => event.key === 'Escape' && dismiss();
159
+ const onScroll = (event) => {
160
+ if (!wrapperRef.current?.contains(event.target))
161
+ dismiss();
162
+ };
163
+ document.addEventListener('keydown', onKey);
164
+ window.addEventListener('scroll', onScroll, true);
165
+ window.addEventListener('resize', dismiss);
166
+ return () => {
167
+ document.removeEventListener('keydown', onKey);
168
+ window.removeEventListener('scroll', onScroll, true);
169
+ window.removeEventListener('resize', dismiss);
170
+ };
171
+ }, [visible, dismiss]);
172
+ return createPortal(_jsx("div", { ref: wrapperRef, "data-slot": "session-brief-card", className: cn('fixed top-0 left-0 pl-3.5 starting:opacity-0', !visible && 'hidden'), style: { zIndex: floatingZ(depth) }, onPointerEnter: () => activeSessionId && openSession(activeSessionId), onPointerLeave: () => activeSessionId && closeSession(activeSessionId), children: _jsx("div", { ref: setSlot, className: cn(FLOATING_PANEL_SURFACE, 'w-72 overflow-hidden rounded-md shadow-xs') }) }), container ?? document.body);
173
+ }
174
+ /**
175
+ * A row's trigger plus its brief, rendered into the shared card while the row
176
+ * is active. Timing and exclusivity belong to `useSessionHoverStore`: only one
177
+ * card may show at a time, and the second row the pointer visits must not pay
178
+ * the opening delay again.
179
+ */
180
+ function SessionBriefCard({ id, children, content, }) {
181
+ const open = useSessionHoverStore((state) => state.activeSessionId === id);
182
+ const slot = useSessionHoverStore((state) => state.slot);
183
+ const openSession = useSessionHoverStore((state) => state.openSession);
184
+ const closeSession = useSessionHoverStore((state) => state.closeSession);
120
185
  // A row can be filtered out, or the sidebar unmounted, while its card shows.
121
186
  // Without this the store keeps naming a row that no longer exists and the
122
187
  // group is still warm the next time the list mounts.
123
188
  useEffect(() => () => {
124
189
  const store = useSessionHoverStore.getState();
125
- if (store.activeSessionId === sessionId)
190
+ if (store.activeSessionId === id)
126
191
  store.dismiss();
127
- }, [sessionId]);
128
- return (_jsxs(HoverCard, { open: open, openDelay: 0, closeDelay: 0, children: [_jsx(HoverCardTrigger, { asChild: true, onPointerEnter: () => openSession(sessionId), onPointerLeave: () => closeSession(sessionId), onFocus: () => openSession(sessionId), onBlur: () => closeSession(sessionId), children: children }), _jsx(HoverCardContent, { side: "right", align: "start", sideOffset: 14, animated: false, className: "w-72 overflow-hidden rounded-md p-0 shadow-xs", onPointerEnter: () => openSession(sessionId), onPointerLeave: () => closeSession(sessionId),
129
- // WCAG 1.4.13: content shown on hover must be dismissible without moving
130
- // the pointer. Radix's own dismissal path runs through `setOpen`, which
131
- // is inert here because the store owns `open`, so Escape is wired
132
- // straight to the store. DismissableLayer listens on the document, so it
133
- // fires even though nothing inside the card can hold focus.
134
- onEscapeKeyDown: dismiss, children: _jsx(SessionBriefContent, { ...brief, onDismiss: dismiss }) })] }));
192
+ }, [id]);
193
+ return (_jsxs(_Fragment, { children: [_jsx(Slot.Root, { "data-state": open ? 'open' : 'closed', onPointerEnter: (event) => {
194
+ if (event.pointerType !== 'touch')
195
+ openSession(id, event.currentTarget, 'pointer');
196
+ }, onPointerLeave: () => closeSession(id), onFocus: (event) => openSession(id, event.currentTarget, 'focus'), onBlur: () => closeSession(id), children: children }), open && slot ? createPortal(content, slot) : null] }));
197
+ }
198
+ export function SessionBriefHoverCard({ sessionId, children, ...brief }) {
199
+ const dismiss = useSessionHoverStore((state) => state.dismiss);
200
+ return (_jsx(SessionBriefCard, { id: sessionId, content: _jsx(SessionBriefContent, { ...brief, onDismiss: dismiss }), children: children }));
135
201
  }
136
202
  /**
137
203
  * The same brief remains available to keyboard and screen-reader users when
138
204
  * the visual HoverCard is absent or intentionally ignored by assistive tech.
139
205
  */
140
- export function SessionBriefDescription({ id, status, createdAt, source, changeRequests, }) {
206
+ export function SessionBriefDescription({ id, status, createdAt, source, changeRequests, waitingOn, }) {
141
207
  const t = useTranslations('sidebar.sessionList.brief');
142
- const statusLabel = useStatusLabel(status);
208
+ const statusLabel = useStatusLabel(status, waitingOn);
143
209
  return (_jsxs("span", { id: id, className: "sr-only", children: [t('status'), ": ", statusLabel, ". ", t('created'), ": ", _jsx(SessionCreatedTime, { createdAt: createdAt }), ".", source.kind !== 'chat'
144
210
  ? ` ${t('source')}: ${source.label}${source.triggerSlug ? `, ${source.triggerSlug}` : ''}.`
145
211
  : null, ' ', changeRequests.length > 0
@@ -151,3 +217,14 @@ export function SessionBriefDescription({ id, status, createdAt, source, changeR
151
217
  export function MobileSessionCreatedTime({ createdAt }) {
152
218
  return (_jsx(SessionCreatedTime, { createdAt: createdAt, className: "text-muted-foreground block truncate text-xs leading-4 tabular-nums" }));
153
219
  }
220
+ /**
221
+ * The hover card of a sub-agent row: what it is and when it ran. Same store,
222
+ * same shell as `SessionBriefHoverCard`, so one card is open at a time across
223
+ * sessions, sub-agents and spawned children, and moving between them is free.
224
+ */
225
+ export function SubagentHoverCard({ id, title, createdAt, updatedAt, children, }) {
226
+ const t = useTranslations('sidebar.sessionList.brief');
227
+ const created = createdAt ?? updatedAt;
228
+ const createdIso = created ? new Date(created).toISOString() : null;
229
+ return (_jsx(SessionBriefCard, { id: id, content: _jsxs("div", { className: "space-y-1.5 px-3.5 py-2.5", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("p", { className: "text-foreground min-w-0 flex-1 truncate text-sm font-medium", children: title }), createdIso && _jsx(RelativeCreatedTime, { createdAt: createdIso })] }), _jsxs("div", { className: "text-muted-foreground flex items-center gap-2 text-xs", children: [_jsx(RobotIcon, { "aria-hidden": true, className: "size-4 shrink-0" }), _jsx("span", { children: t('subAgent') })] }), createdIso && (_jsxs("div", { className: "text-muted-foreground text-xs", children: [t('created'), ": ", _jsx(SessionCreatedTime, { createdAt: createdIso })] }))] }), children: children }));
230
+ }