From 9d419626ef72691e01746d39eadaaf6e7644b756 Mon Sep 17 00:00:00 2001 From: 777genius Date: Thu, 7 May 2026 18:43:37 +0300 Subject: [PATCH] fix(ci): apply frontend validation fixes --- .../renderer/adapters/TeamGraphAdapter.ts | 2 +- .../renderer/ui/GraphMemberLogPreviewHud.tsx | 69 ++++-- .../renderer/ui/GraphNodePopover.tsx | 2 +- .../memberLogPreviewExtractor.test.ts | 132 ++++++++++ .../policies/memberLogPreviewExtractor.ts | 231 +++++++++++++++--- .../MemberWorkSyncDiagnosticsReader.ts | 1 + .../MemberWorkSyncNudgeDispatcher.ts | 3 +- .../createMemberWorkSyncFeature.ts | 2 +- ...xSessionFileRecentProjectsSourceAdapter.ts | 89 +++++-- src/main/services/team/TeamConfigReader.ts | 5 +- src/main/services/team/TeamDataService.ts | 2 +- .../team/TeamMemberRuntimeAdvisoryService.ts | 6 +- .../services/team/TeamMessageFeedService.ts | 2 +- .../services/team/TeamProvisioningService.ts | 20 +- src/main/services/team/TeamTaskReader.ts | 5 +- .../bridge/OpenCodeBridgeHandshakeClient.ts | 9 +- .../OpenCodeRuntimeManifestEvidenceReader.ts | 6 +- .../runtime/OpenCodeTeamRuntimeAdapter.ts | 2 +- .../TeamTaskStallSnapshotSource.ts | 2 +- .../activity/BoardTaskActivityRecordSource.ts | 4 +- .../discovery/TeamTranscriptSourceLocator.ts | 4 +- .../stream/CodexNativeTraceProjector.ts | 2 +- src/main/services/team/teamTaskActiveState.ts | 2 +- .../components/dashboard/CliStatusBanner.tsx | 18 +- .../runtime/CodexLoginLinkCopyButton.tsx | 6 +- .../runtime/ProviderRuntimeSettingsDialog.tsx | 4 +- src/renderer/components/team/TaskTooltip.tsx | 2 +- .../components/team/activity/ActivityItem.tsx | 2 +- .../team/dialogs/CodexReconnectPrompt.tsx | 6 +- .../team/dialogs/CreateTaskDialog.tsx | 2 +- .../team/dialogs/CreateTeamDialog.tsx | 4 +- .../team/dialogs/LaunchTeamDialog.tsx | 4 +- .../team/dialogs/ProjectPathSelector.tsx | 6 +- .../team/dialogs/TaskDetailDialog.tsx | 10 +- .../team/dialogs/projectPathOptions.ts | 2 +- .../components/team/kanban/KanbanTaskCard.tsx | 2 +- .../team/members/MemberTasksTab.tsx | 2 +- .../components/team/tasks/TaskRow.tsx | 2 +- src/renderer/utils/memberHelpers.ts | 2 +- src/shared/types/team.ts | 4 +- ...ionFileRecentProjectsSourceAdapter.test.ts | 38 +++ .../GraphMemberLogPreviewHud.test.tsx | 73 +++++- 42 files changed, 621 insertions(+), 170 deletions(-) diff --git a/src/features/agent-graph/renderer/adapters/TeamGraphAdapter.ts b/src/features/agent-graph/renderer/adapters/TeamGraphAdapter.ts index 06bb7df7..cce9ae7b 100644 --- a/src/features/agent-graph/renderer/adapters/TeamGraphAdapter.ts +++ b/src/features/agent-graph/renderer/adapters/TeamGraphAdapter.ts @@ -25,11 +25,11 @@ import { } from '@shared/utils/idleNotificationSemantics'; import { isInboxNoiseMessage } from '@shared/utils/inboxNoise'; import { isLeadMember } from '@shared/utils/leadDetection'; +import { buildOrderedVisibleTeamGraphOwnerIds } from '@shared/utils/teamGraphDefaultLayout'; import { isTeamTaskActivelyWorked, isTeamTaskNeedsFixActionable, } from '@shared/utils/teamTaskState'; -import { buildOrderedVisibleTeamGraphOwnerIds } from '@shared/utils/teamGraphDefaultLayout'; import { buildInlineActivityEntries, diff --git a/src/features/agent-graph/renderer/ui/GraphMemberLogPreviewHud.tsx b/src/features/agent-graph/renderer/ui/GraphMemberLogPreviewHud.tsx index 8171a72f..5042bce2 100644 --- a/src/features/agent-graph/renderer/ui/GraphMemberLogPreviewHud.tsx +++ b/src/features/agent-graph/renderer/ui/GraphMemberLogPreviewHud.tsx @@ -28,6 +28,8 @@ import type { const LOG_PREVIEW_FALLBACK_WIDTH = 260; const LOG_PREVIEW_FALLBACK_HEIGHT = 292; const NEW_LOG_HIGHLIGHT_MS = 1_000; +const COMPACT_ROW_TEXT_LIMIT = 118; +const COMPACT_ROW_MIN_PREVIEW_LIMIT = 48; interface StableRectLike { left: number; @@ -155,6 +157,18 @@ function compactPreviewText(item: MemberLogPreviewItem, displayTitle: string): s return item.sourceLabel || 'Log event'; } +function truncateCompactRowPreview( + preview: string, + displayTitle: string, + relativeTime: string +): string { + const normalized = preview.replace(/\s+/g, ' ').trim(); + const metaLength = displayTitle.length + relativeTime.length + (relativeTime ? 2 : 1); + const previewLimit = Math.max(COMPACT_ROW_MIN_PREVIEW_LIMIT, COMPACT_ROW_TEXT_LIMIT - metaLength); + if (normalized.length <= previewLimit) return normalized; + return `${normalized.slice(0, Math.max(0, previewLimit - 3)).trimEnd()}...`; +} + function setShellHidden(shell: HTMLDivElement): void { shell.style.opacity = '0'; shell.style.pointerEvents = 'none'; @@ -405,42 +419,53 @@ export const GraphMemberLogPreviewHud = ({ (memberName: string, item: MemberLogPreviewItem) => { const relativeTime = formatRelativeTime(item.timestamp); const displayTitle = compactDisplayTitle(item); - const previewText = compactPreviewText(item, displayTitle); + const fullPreviewText = compactPreviewText(item, displayTitle); + const previewText = truncateCompactRowPreview(fullPreviewText, displayTitle, relativeTime); const titleText = relativeTime - ? `${displayTitle} ${relativeTime} ${previewText}` - : `${displayTitle} ${previewText}`; + ? `${displayTitle} ${relativeTime} ${fullPreviewText}` + : `${displayTitle} ${fullPreviewText}`; const isHighlighted = highlightedItemIds.has(item.id); + const isError = item.tone === 'error'; + const rowStateClassName = isHighlighted + ? isError + ? 'border-rose-300/75 bg-rose-950/35 shadow-[0_0_0_1px_rgba(253,164,175,0.30),0_0_18px_rgba(244,63,94,0.22)] hover:border-rose-300/80 hover:bg-rose-950/45' + : 'border-sky-300/70 bg-[rgba(14,34,62,0.74)] shadow-[0_0_0_1px_rgba(125,211,252,0.30),0_0_18px_rgba(56,189,248,0.22)] hover:border-sky-300/75 hover:bg-[rgba(14,34,62,0.82)]' + : isError + ? 'border-rose-400/35 bg-rose-950/20 hover:border-rose-300/50 hover:bg-rose-950/30' + : 'border-white/10 bg-[rgba(8,14,28,0.52)] hover:border-white/20 hover:bg-[rgba(12,20,40,0.78)]'; + const iconClassName = isError + ? 'float-left mr-2 inline-flex size-5 shrink-0 items-center justify-center rounded bg-rose-500/10' + : 'float-left mr-2 inline-flex size-5 shrink-0 items-center justify-center rounded bg-white/5'; + const headerClassName = 'inline-flex h-5 items-center align-top'; + const titleClassName = isError + ? 'text-[11px] font-medium leading-[18px] text-rose-100' + : 'text-[11px] font-medium leading-[18px] text-slate-200'; + const timeClassName = isError + ? 'ml-1 text-[9px] font-normal leading-[18px] text-rose-300/70' + : 'ml-1 text-[9px] font-normal leading-[18px] text-slate-500'; + const previewClassName = isError + ? 'ml-1 break-words align-top text-[10px] leading-[18px] text-rose-100/85' + : 'ml-1 break-words align-top text-[10px] leading-[18px] text-slate-300/85'; return ( ); }, @@ -494,7 +519,7 @@ export const GraphMemberLogPreviewHud = ({ ) : ( ); -} +}; diff --git a/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx b/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx index 93375cb3..cd91af77 100644 --- a/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx +++ b/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx @@ -20,8 +20,9 @@ import { resolveCodexRuntimeSelection, } from '@features/codex-runtime-profile/renderer'; import { RuntimeProviderManagementPanel } from '@features/runtime-provider-management/renderer'; -import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo'; import { api } from '@renderer/api'; +import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo'; +import { CodexLoginLinkCopyButton } from '@renderer/components/runtime/CodexLoginLinkCopyButton'; import { Button } from '@renderer/components/ui/button'; import { Dialog, @@ -40,7 +41,6 @@ import { SelectValue, } from '@renderer/components/ui/select'; import { Tabs, TabsList, TabsTrigger } from '@renderer/components/ui/tabs'; -import { CodexLoginLinkCopyButton } from '@renderer/components/runtime/CodexLoginLinkCopyButton'; import { useStore } from '@renderer/store'; import { AlertTriangle, Key, Link2, Loader2, Trash2 } from 'lucide-react'; diff --git a/src/renderer/components/team/TaskTooltip.tsx b/src/renderer/components/team/TaskTooltip.tsx index 5382ef16..fc4a4d9d 100644 --- a/src/renderer/components/team/TaskTooltip.tsx +++ b/src/renderer/components/team/TaskTooltip.tsx @@ -7,11 +7,11 @@ import { useStore } from '@renderer/store'; import { selectResolvedMembersForTeamName } from '@renderer/store/slices/teamSlice'; import { buildMemberColorMap, REVIEW_STATE_DISPLAY } from '@renderer/utils/memberHelpers'; import { linkifyTaskIdsInMarkdown } from '@renderer/utils/taskReferenceUtils'; +import { formatTaskDisplayLabel, taskMatchesRef } from '@shared/utils/taskIdentity'; import { getTeamTaskWorkflowColumn, isTeamTaskNeedsFixActionable, } from '@shared/utils/teamTaskState'; -import { formatTaskDisplayLabel, taskMatchesRef } from '@shared/utils/taskIdentity'; import { useShallow } from 'zustand/react/shallow'; import type { TeamTaskWithKanban } from '@shared/types'; diff --git a/src/renderer/components/team/activity/ActivityItem.tsx b/src/renderer/components/team/activity/ActivityItem.tsx index 592b399a..f92b998b 100644 --- a/src/renderer/components/team/activity/ActivityItem.tsx +++ b/src/renderer/components/team/activity/ActivityItem.tsx @@ -63,8 +63,8 @@ import { getKnownSlashCommand, parseStandaloneSlashCommand, } from '@shared/utils/slashCommands'; -import { isTaskStallRemediationMessage } from '@shared/utils/teamAutomationMessages'; import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity'; +import { isTaskStallRemediationMessage } from '@shared/utils/teamAutomationMessages'; import { AlertTriangle, Check, diff --git a/src/renderer/components/team/dialogs/CodexReconnectPrompt.tsx b/src/renderer/components/team/dialogs/CodexReconnectPrompt.tsx index 9efaccae..4a5b502f 100644 --- a/src/renderer/components/team/dialogs/CodexReconnectPrompt.tsx +++ b/src/renderer/components/team/dialogs/CodexReconnectPrompt.tsx @@ -59,7 +59,7 @@ export function shouldShowCodexReconnectPrompt({ ); } -export function CodexReconnectPrompt({ +export const CodexReconnectPrompt = ({ authUrl, reconnectBusy, onReconnect, @@ -67,7 +67,7 @@ export function CodexReconnectPrompt({ authUrl: string | null; reconnectBusy: boolean; onReconnect: () => void; -}): React.JSX.Element { +}): React.JSX.Element => { return (
); -} +}; diff --git a/src/renderer/components/team/dialogs/CreateTaskDialog.tsx b/src/renderer/components/team/dialogs/CreateTaskDialog.tsx index ea9479ad..35363c6f 100644 --- a/src/renderer/components/team/dialogs/CreateTaskDialog.tsx +++ b/src/renderer/components/team/dialogs/CreateTaskDialog.tsx @@ -28,8 +28,8 @@ import { extractTaskRefsFromText, stripEncodedTaskReferenceMetadata, } from '@renderer/utils/taskReferenceUtils'; -import { getTeamTaskWorkflowColumn } from '@shared/utils/teamTaskState'; import { deriveTaskDisplayId, formatTaskDisplayLabel } from '@shared/utils/taskIdentity'; +import { getTeamTaskWorkflowColumn } from '@shared/utils/teamTaskState'; import { AlertTriangle, ChevronDown, ChevronRight, Search } from 'lucide-react'; import type { InlineChip } from '@renderer/types/inlineChip'; diff --git a/src/renderer/components/team/dialogs/CreateTeamDialog.tsx b/src/renderer/components/team/dialogs/CreateTeamDialog.tsx index dd653a33..180d05a3 100644 --- a/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +++ b/src/renderer/components/team/dialogs/CreateTeamDialog.tsx @@ -88,15 +88,15 @@ import { AlertTriangle, CheckCircle2, Info, Loader2, X } from 'lucide-react'; import { AdvancedCliSection } from './AdvancedCliSection'; import { AnthropicFastModeSelector } from './AnthropicFastModeSelector'; -import { CodexReconnectPrompt, shouldShowCodexReconnectPrompt } from './CodexReconnectPrompt'; import { CodexFastModeSelector } from './CodexFastModeSelector'; +import { CodexReconnectPrompt, shouldShowCodexReconnectPrompt } from './CodexReconnectPrompt'; import { clearInheritedMemberModelsUnavailableForProvider, resolveProviderScopedMemberModel, } from './memberModelScope'; import { OptionalSettingsSection } from './OptionalSettingsSection'; -import { ProjectPathSelector } from './ProjectPathSelector'; import { loadProjectPathProjects, type ProjectPathProject } from './projectPathProjects'; +import { ProjectPathSelector } from './ProjectPathSelector'; import { buildProviderPrepareModelCacheKey } from './providerPrepareCacheKey'; import { buildReusableProviderPrepareModelResults, diff --git a/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx b/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx index 8fabcc54..3689d4f1 100644 --- a/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx +++ b/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx @@ -91,8 +91,8 @@ import { CronScheduleInput } from '../schedule/CronScheduleInput'; import { AdvancedCliSection } from './AdvancedCliSection'; import { AnthropicFastModeSelector } from './AnthropicFastModeSelector'; -import { CodexReconnectPrompt, shouldShowCodexReconnectPrompt } from './CodexReconnectPrompt'; import { CodexFastModeSelector } from './CodexFastModeSelector'; +import { CodexReconnectPrompt, shouldShowCodexReconnectPrompt } from './CodexReconnectPrompt'; import { EffortLevelSelector } from './EffortLevelSelector'; import { resolveLaunchDialogPrefill } from './launchDialogPrefill'; import { @@ -100,8 +100,8 @@ import { resolveProviderScopedMemberModel, } from './memberModelScope'; import { OptionalSettingsSection } from './OptionalSettingsSection'; -import { ProjectPathSelector } from './ProjectPathSelector'; import { loadProjectPathProjects, type ProjectPathProject } from './projectPathProjects'; +import { ProjectPathSelector } from './ProjectPathSelector'; import { buildProviderPrepareModelCacheKey } from './providerPrepareCacheKey'; import { buildReusableProviderPrepareModelResults, diff --git a/src/renderer/components/team/dialogs/ProjectPathSelector.tsx b/src/renderer/components/team/dialogs/ProjectPathSelector.tsx index da65e517..94faeeef 100644 --- a/src/renderer/components/team/dialogs/ProjectPathSelector.tsx +++ b/src/renderer/components/team/dialogs/ProjectPathSelector.tsx @@ -66,11 +66,11 @@ function getSourceLabel(source: DashboardRecentProjectSource): string { } } -function ProjectSourceBadge({ +const ProjectSourceBadge = ({ source, }: { source?: DashboardRecentProjectSource; -}): React.JSX.Element | null { +}): React.JSX.Element | null => { if (!source) { return null; } @@ -92,7 +92,7 @@ function ProjectSourceBadge({ ))} ); -} +}; export type CwdMode = 'project' | 'custom'; diff --git a/src/renderer/components/team/dialogs/TaskDetailDialog.tsx b/src/renderer/components/team/dialogs/TaskDetailDialog.tsx index 2e903d37..b88df1d5 100644 --- a/src/renderer/components/team/dialogs/TaskDetailDialog.tsx +++ b/src/renderer/components/team/dialogs/TaskDetailDialog.tsx @@ -54,16 +54,16 @@ import { } from '@renderer/utils/taskChangeRequest'; import { linkifyTaskIdsInMarkdown, parseTaskLinkHref } from '@renderer/utils/taskReferenceUtils'; import { isLeadMember } from '@shared/utils/leadDetection'; -import { - getTeamTaskWorkflowColumn, - isTeamTaskFinishedForDependency, - isTeamTaskNeedsFixActionable, -} from '@shared/utils/teamTaskState'; import { deriveTaskDisplayId, formatTaskDisplayLabel, taskMatchesRef, } from '@shared/utils/taskIdentity'; +import { + getTeamTaskWorkflowColumn, + isTeamTaskFinishedForDependency, + isTeamTaskNeedsFixActionable, +} from '@shared/utils/teamTaskState'; import { format, formatDistanceToNow } from 'date-fns'; import { AlignLeft, diff --git a/src/renderer/components/team/dialogs/projectPathOptions.ts b/src/renderer/components/team/dialogs/projectPathOptions.ts index b3dba417..8dd5af03 100644 --- a/src/renderer/components/team/dialogs/projectPathOptions.ts +++ b/src/renderer/components/team/dialogs/projectPathOptions.ts @@ -1,8 +1,8 @@ import { normalizePath } from '@renderer/utils/pathNormalize'; import { isEphemeralProjectPath } from '@shared/utils/ephemeralProjectPath'; -import type { ComboboxOption } from '@renderer/components/ui/combobox'; import type { DashboardRecentProjectSource } from '@features/recent-projects/contracts'; +import type { ComboboxOption } from '@renderer/components/ui/combobox'; import type { Project } from '@shared/types'; export interface ProjectPathProject extends Project { diff --git a/src/renderer/components/team/kanban/KanbanTaskCard.tsx b/src/renderer/components/team/kanban/KanbanTaskCard.tsx index 0377c1d0..1600058b 100644 --- a/src/renderer/components/team/kanban/KanbanTaskCard.tsx +++ b/src/renderer/components/team/kanban/KanbanTaskCard.tsx @@ -13,11 +13,11 @@ import { buildTaskChangeRequestOptions, canDisplayTaskChangesForOptions, } from '@renderer/utils/taskChangeRequest'; +import { deriveTaskDisplayId, formatTaskDisplayLabel } from '@shared/utils/taskIdentity'; import { isTeamTaskFinishedForDependency, isTeamTaskNeedsFixActionable, } from '@shared/utils/teamTaskState'; -import { deriveTaskDisplayId, formatTaskDisplayLabel } from '@shared/utils/taskIdentity'; import { ArrowLeftFromLine, ArrowRightFromLine, diff --git a/src/renderer/components/team/members/MemberTasksTab.tsx b/src/renderer/components/team/members/MemberTasksTab.tsx index 1ef96423..15d90066 100644 --- a/src/renderer/components/team/members/MemberTasksTab.tsx +++ b/src/renderer/components/team/members/MemberTasksTab.tsx @@ -7,11 +7,11 @@ import { TASK_STATUS_LABELS, TASK_STATUS_STYLES, } from '@renderer/utils/memberHelpers'; +import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity'; import { getTeamTaskWorkflowColumn, isTeamTaskNeedsFixActionable, } from '@shared/utils/teamTaskState'; -import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity'; import type { TeamTaskWithKanban } from '@shared/types'; diff --git a/src/renderer/components/team/tasks/TaskRow.tsx b/src/renderer/components/team/tasks/TaskRow.tsx index f2c067e8..852e7fda 100644 --- a/src/renderer/components/team/tasks/TaskRow.tsx +++ b/src/renderer/components/team/tasks/TaskRow.tsx @@ -5,11 +5,11 @@ import { REVIEW_STATE_DISPLAY, TASK_STATUS_LABELS, } from '@renderer/utils/memberHelpers'; +import { deriveTaskDisplayId, formatTaskDisplayLabel } from '@shared/utils/taskIdentity'; import { getTeamTaskWorkflowColumn, isTeamTaskNeedsFixActionable, } from '@shared/utils/teamTaskState'; -import { deriveTaskDisplayId, formatTaskDisplayLabel } from '@shared/utils/taskIdentity'; import type { TeamTaskWithKanban } from '@shared/types'; diff --git a/src/renderer/utils/memberHelpers.ts b/src/renderer/utils/memberHelpers.ts index 54caff24..60f9c618 100644 --- a/src/renderer/utils/memberHelpers.ts +++ b/src/renderer/utils/memberHelpers.ts @@ -779,7 +779,7 @@ function isQueuedOpenCodeLaunch( // Only label lanes as queued before runtime evidence appears. Once the // backend has any liveness signal, show the exact runtime state instead. - return runtimeEntry == null || runtimeEntry.livenessKind == null; + return runtimeEntry?.livenessKind == null; } function hasElapsedSinceIso( diff --git a/src/shared/types/team.ts b/src/shared/types/team.ts index 9a87c7e3..f8d23599 100644 --- a/src/shared/types/team.ts +++ b/src/shared/types/team.ts @@ -1133,8 +1133,8 @@ export interface RetryFailedOpenCodeSecondaryLanesResult { attempted: string[]; confirmed: string[]; pending: string[]; - failed: Array<{ memberName: string; error: string }>; - skipped: Array<{ memberName: string; reason: string }>; + failed: { memberName: string; error: string }[]; + skipped: { memberName: string; reason: string }[]; } export type MemberSpawnLivenessSource = 'heartbeat' | 'process'; diff --git a/test/features/recent-projects/main/adapters/output/CodexSessionFileRecentProjectsSourceAdapter.test.ts b/test/features/recent-projects/main/adapters/output/CodexSessionFileRecentProjectsSourceAdapter.test.ts index e33539f9..afc4418b 100644 --- a/test/features/recent-projects/main/adapters/output/CodexSessionFileRecentProjectsSourceAdapter.test.ts +++ b/test/features/recent-projects/main/adapters/output/CodexSessionFileRecentProjectsSourceAdapter.test.ts @@ -28,6 +28,7 @@ async function writeRollout( source?: string; timestamp?: string; branch?: string; + metadataPadding?: string; }, mtime: Date ): Promise { @@ -43,6 +44,9 @@ async function writeRollout( cwd: payload.cwd, source: payload.source ?? 'cli', git: payload.branch ? { branch: payload.branch } : undefined, + ...(payload.metadataPadding + ? { base_instructions: { text: payload.metadataPadding } } + : {}), }, })}\n${'x'.repeat(1024)}`, 'utf8' @@ -110,6 +114,40 @@ describe('CodexSessionFileRecentProjectsSourceAdapter', () => { expect(identityResolver.resolve).toHaveBeenCalledWith('/Users/test/projects/alpha'); }); + it('loads Codex projects from large session metadata lines without parsing the full line', async () => { + const codexHome = path.join(tempDir, '.codex'); + const logger = createLogger(); + const identityResolver = { + resolve: vi.fn().mockResolvedValue(null), + } as unknown as RecentProjectIdentityResolver; + const updatedAt = new Date('2026-04-14T12:00:00.000Z'); + await writeRollout( + path.join(codexHome, 'sessions', '2026', '04', '14', 'rollout-large.jsonl'), + { + cwd: '/Users/test/projects/large', + metadataPadding: 'x'.repeat(160_000), + }, + updatedAt + ); + + const adapter = new CodexSessionFileRecentProjectsSourceAdapter({ + getActiveContext: () => ({ type: 'local', id: 'local-1' }) as never, + getLocalContext: () => ({ type: 'local', id: 'local-1' }) as never, + identityResolver, + logger, + codexHome, + }); + + const result = await adapter.list(); + + expect(result.candidates).toEqual([ + expect.objectContaining({ + primaryPath: '/Users/test/projects/large', + sourceKind: 'codex', + }), + ]); + }); + it('deduplicates sessions by cwd and keeps the newest activity', async () => { const codexHome = path.join(tempDir, '.codex'); const logger = createLogger(); diff --git a/test/renderer/features/agent-graph/GraphMemberLogPreviewHud.test.tsx b/test/renderer/features/agent-graph/GraphMemberLogPreviewHud.test.tsx index fc08f322..738ca724 100644 --- a/test/renderer/features/agent-graph/GraphMemberLogPreviewHud.test.tsx +++ b/test/renderer/features/agent-graph/GraphMemberLogPreviewHud.test.tsx @@ -164,14 +164,16 @@ describe('GraphMemberLogPreviewHud', () => { expect(row).not.toBeUndefined(); expect(row?.querySelector('.float-left')).not.toBeNull(); expect(row?.querySelector('.line-clamp-3')).toBeNull(); - expect(row?.className).toContain('h-16'); - expect(row?.querySelector('span.text-slate-200')?.className).toContain('leading-4'); + expect(row?.className).toContain('h-[68px]'); + expect(row?.querySelector('span.text-slate-200')?.className).toContain('leading-[18px]'); expect(row?.textContent).toContain('pnpm test'); const errorRow = Array.from(host.querySelectorAll('button')).find((button) => button.textContent?.includes('OpenCode tool failed') ); + expect(errorRow?.className).toContain('border-rose-400/35'); expect(errorRow?.querySelector('svg.text-rose-300')).not.toBeNull(); + expect(errorRow?.querySelector('.text-rose-100')).not.toBeNull(); const resultRow = Array.from(host.querySelectorAll('button')).find((button) => button.textContent?.includes('Tests passed') @@ -203,6 +205,73 @@ describe('GraphMemberLogPreviewHud', () => { }); }); + it('caps long visible rows while preserving the full preview in the title', async () => { + const node: GraphNode = { + id: 'member:alpha-team:alice', + kind: 'member', + label: 'alice', + state: 'active', + domainRef: { kind: 'member', teamName: 'alpha-team', memberName: 'alice' }, + }; + const longPreview = + 'to team-lead inbox - #68a3a8cc blocked by dependencies and needs a follow-up investigation before merge final-token'; + const alicePreview = basePreviewsByMember.get('alice')!; + mockedPreviewsByMember = new Map(basePreviewsByMember); + mockedPreviewsByMember.set('alice', { + ...alicePreview, + items: [ + { + id: 'preview-long', + kind: 'tool_use' as const, + provider: 'claude_transcript' as const, + timestamp: '2026-04-03T00:00:00.000Z', + title: 'Send message', + preview: longPreview, + tone: 'warning' as const, + }, + ], + overflowCount: 0, + }); + + const host = document.createElement('div'); + document.body.appendChild(host); + const root = createRoot(host); + + await act(async () => { + root.render( + ({ + left: 40, + top: 80, + right: 300, + bottom: 372, + width: 260, + height: 292, + })} + getCameraZoom={() => 1} + worldToScreen={(x, y) => ({ x, y })} + getViewportSize={() => ({ width: 1200, height: 800 })} + focusNodeIds={null} + /> + ); + await Promise.resolve(); + }); + + const row = Array.from(host.querySelectorAll('button')).find((button) => + button.textContent?.includes('Send message') + ); + + expect(row?.textContent).toContain('...'); + expect(row?.textContent).not.toContain('final-token'); + expect(row?.getAttribute('title')).toContain('final-token'); + + act(() => { + root.unmount(); + }); + }); + it('briefly highlights a newly appeared preview row', async () => { const node: GraphNode = { id: 'member:alpha-team:alice',