@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.
- package/README.md +8 -4
- package/dist/customize/sections/llm-provider/constants.d.ts +0 -1
- package/dist/customize/sections/llm-provider/constants.js +0 -2
- package/dist/customize/sections/llm-provider/models-tab-group.d.ts +1 -1
- package/dist/customize/sections/llm-provider/models-tab-group.js +7 -4
- package/dist/customize/sections/llm-provider/models-tab-row.d.ts +2 -18
- package/dist/customize/sections/llm-provider/models-tab-row.js +6 -30
- package/dist/customize/sections/llm-provider/models-tab.d.ts +0 -1
- package/dist/customize/sections/llm-provider/models-tab.js +1 -10
- package/dist/customize/sections/llm-provider/use-connected-providers.js +2 -2
- package/dist/customize/sections/llm-provider/utils.d.ts +1 -1
- package/dist/customize/sections/llm-provider/utils.js +7 -14
- package/dist/customize/sections/view/gateway/_shared.js +1 -2
- package/dist/customize/sections/view/gateway/gateway-routing.js +3 -2
- package/dist/customize/sections/view/sandbox-provider-coverage.d.ts +3 -18
- package/dist/customize/sections/view/sandbox-provider-coverage.js +7 -27
- package/dist/customize/settings/tabs/sandbox-tab.d.ts +2 -27
- package/dist/customize/settings/tabs/sandbox-tab.js +13 -112
- package/dist/customize/shared/entity/entity-modal.d.ts +5 -1
- package/dist/customize/shared/entity/entity-modal.js +7 -4
- package/dist/customize/skills/remove-skill-button.d.ts +16 -0
- package/dist/customize/skills/remove-skill-button.js +42 -0
- package/dist/customize/skills/skills-page.js +1 -1
- package/dist/customize/triggers/schedule/trigger-list.d.ts +0 -2
- package/dist/customize/triggers/schedule/trigger-list.js +1 -18
- package/dist/customize/triggers/schedule/trigger-tile.d.ts +1 -32
- package/dist/customize/triggers/schedule/trigger-tile.js +1 -16
- package/dist/files/project/hooks/use-change-requests.d.ts +14 -1
- package/dist/files/project/hooks/use-change-requests.js +44 -25
- package/dist/files/renderers/docx/docx-viewer.js +4 -1
- package/dist/files/renderers/pdf/pdf-renderer.d.ts +3 -1
- package/dist/files/renderers/pdf/pdf-renderer.js +2 -2
- package/dist/files/renderers/pdf/pdf-viewer.d.ts +3 -0
- package/dist/files/renderers/pdf/pdf-viewer.js +15 -6
- package/dist/files/viewer/file-content-renderer.d.ts +4 -1
- package/dist/files/viewer/file-content-renderer.js +2 -2
- package/dist/i18n/en.d.ts +26 -35
- package/dist/i18n/en.js +35 -44
- package/dist/index.d.ts +1 -1
- package/dist/internal/hooks/use-notification-center.js +21 -15
- package/dist/internal/hooks/use-review-session-summary.d.ts +6 -0
- package/dist/internal/hooks/use-review-session-summary.js +11 -0
- package/dist/internal/lib/browser-error-noise.d.ts +1 -1
- package/dist/internal/lib/browser-error-noise.js +1 -1
- package/dist/internal/lib/browser-noise/rules/rejections.d.ts +10 -0
- package/dist/internal/lib/browser-noise/rules/rejections.js +27 -1
- package/dist/internal/lib/provider-branding.js +14 -3
- package/dist/internal/lib/trigger-clock.d.ts +42 -0
- package/dist/internal/lib/trigger-clock.js +34 -0
- package/dist/internal/lib/web-notifications.d.ts +6 -0
- package/dist/internal/lib/web-notifications.js +57 -7
- package/dist/internal/notifications/notification-bell.d.ts +13 -0
- package/dist/internal/notifications/notification-bell.js +1 -1
- package/dist/internal/reminders/reminder-filter.d.ts +12 -0
- package/dist/internal/reminders/reminder-filter.js +34 -0
- package/dist/internal/reminders/reminder-format.d.ts +6 -0
- package/dist/internal/reminders/reminder-format.js +20 -2
- package/dist/internal/stores/kortix-computer-store.d.ts +1 -1
- package/dist/internal/stores/session-hover-store.d.ts +20 -5
- package/dist/internal/stores/session-hover-store.js +31 -13
- package/dist/internal/stores/session-panel-memory-store.js +2 -0
- package/dist/primitives/count-badge.d.ts +3 -1
- package/dist/primitives/count-badge.js +3 -3
- package/dist/primitives/glyph-paths-source.js +1 -1
- package/dist/primitives/glyph-paths.generated.js +1 -1
- package/dist/providers/ui-provider.d.ts +57 -0
- package/dist/providers/ui-provider.js +4 -0
- package/dist/session/composer/ComposerCard.d.ts +2 -0
- package/dist/session/composer/ComposerCard.js +5 -1
- package/dist/session/composer/agent-selector.js +12 -5
- package/dist/session/composer/composer-toolbar.js +4 -2
- package/dist/session/composer/composer-underbar.js +3 -1
- package/dist/session/composer/composer.js +34 -5
- package/dist/session/composer/menus/slash-actions.d.ts +22 -2
- package/dist/session/composer/menus/slash-actions.js +20 -0
- package/dist/session/composer/menus/slash-items.js +19 -3
- package/dist/session/header/reminder-parts.d.ts +285 -0
- package/dist/session/header/reminder-parts.js +115 -0
- package/dist/session/header/session-reminders-indicator.d.ts +13 -0
- package/dist/session/header/session-reminders-indicator.js +142 -35
- package/dist/session/models/model-availability.d.ts +23 -0
- package/dist/session/models/model-availability.js +20 -0
- package/dist/session/models/model-grouping.js +1 -4
- package/dist/session/models/model-selector.js +23 -14
- package/dist/session/models/use-model-connection-gate.js +4 -1
- package/dist/session/outcomes/session-outcomes-provider.d.ts +3 -1
- package/dist/session/outcomes/session-outcomes-provider.js +31 -3
- package/dist/session/overrides/session-overrides-composer.d.ts +1 -1
- package/dist/session/overrides/session-overrides-composer.js +5 -1
- package/dist/session/panel/mobile-tool-drawer.js +4 -2
- package/dist/session/panel/open-session-quick-view.js +2 -1
- package/dist/session/panel/panel-fallback.js +8 -4
- package/dist/session/panel/session-panel-provider.d.ts +1 -0
- package/dist/session/panel/session-panel-provider.js +13 -1
- package/dist/session/panel/session-reminders-panel.d.ts +4 -0
- package/dist/session/panel/session-reminders-panel.js +109 -0
- package/dist/session/panel/shared/use-panel-handoffs.d.ts +2 -2
- package/dist/session/panel/shared/use-panel-handoffs.js +5 -1
- package/dist/session/print/session-print-header.d.ts +0 -10
- package/dist/session/print/session-print-header.js +16 -1
- package/dist/session/status/session-surface.js +1 -1
- package/dist/session/thread/optimistic-turn.d.ts +5 -1
- package/dist/session/thread/optimistic-turn.js +8 -6
- package/dist/session/thread/session-chat.d.ts +6 -1
- package/dist/session/thread/session-chat.js +22 -3
- package/dist/session/thread/session-thread.d.ts +10 -1
- package/dist/session/thread/session-thread.js +2 -2
- package/dist/session/turn/burst-step-body.js +4 -0
- package/dist/styles.css +1 -1
- package/dist/workspace/sidebar/project-session-list.js +52 -55
- package/dist/workspace/sidebar/session-brief-hover-card.d.ts +35 -18
- package/dist/workspace/sidebar/session-brief-hover-card.js +114 -37
- package/dist/workspace/sidebar/session-status-mark.d.ts +15 -1
- package/dist/workspace/sidebar/session-status-mark.js +63 -17
- package/package.json +7 -3
- package/skills/kortix-ui/SKILL.md +1 -1
- package/dist/customize/sections/view/sandbox-provider-result.d.ts +0 -47
- 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
|
-
|
|
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,
|
|
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.
|
|
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
|
-
/**
|
|
466
|
-
*
|
|
467
|
-
*
|
|
468
|
-
* so hovering the row never
|
|
469
|
-
|
|
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 (
|
|
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('
|
|
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:
|
|
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:
|
|
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
|
-
*
|
|
502
|
-
*
|
|
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: "
|
|
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
|
|
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(
|
|
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,
|
|
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
|
|
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
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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
|
-
*
|
|
34
|
-
* the
|
|
35
|
-
*
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
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
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
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
|
-
*
|
|
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
|
|
116
|
-
const
|
|
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 ===
|
|
190
|
+
if (store.activeSessionId === id)
|
|
126
191
|
store.dismiss();
|
|
127
|
-
}, [
|
|
128
|
-
return (_jsxs(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
+
}
|