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',