refactor(session): improve session label formatting and enhance session item display

- Replaced direct access to session.firstMessage with formatSessionLabel for consistent label formatting across components.
- Updated SessionItem, TeamSessionsSection, and KanbanFilterPopover to utilize the new formatting function.
- Enhanced display logic in SessionItem to differentiate between regular and team sessions, improving user experience.
- Added new icons for team sessions and adjusted metadata display for better clarity.
This commit is contained in:
iliya 2026-03-29 01:16:04 +02:00
parent 304a2a7f79
commit 46355d87df
11 changed files with 533 additions and 59 deletions

View file

@ -8,9 +8,10 @@ import { useCallback, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { useStore } from '@renderer/store';
import { formatSessionLabel, parseSessionTitle } from '@renderer/utils/sessionTitleParser';
import { formatTokensCompact } from '@shared/utils/tokenFormatting';
import { formatDistanceToNowStrict } from 'date-fns';
import { EyeOff, MessageSquare, Pin } from 'lucide-react';
import { EyeOff, MessageSquare, Pin, Play, RotateCw, Users } from 'lucide-react';
import { useShallow } from 'zustand/react/shallow';
import { OngoingIndicator } from '../common/OngoingIndicator';
@ -178,7 +179,7 @@ export const SessionItem = ({
type: 'session',
sessionId: session.id,
projectId: activeProjectId,
label: session.firstMessage?.slice(0, 50) ?? 'Session',
label: formatSessionLabel(session.firstMessage),
},
forceNewTab ? { forceNewTab } : { replaceActiveTab: true }
);
@ -191,7 +192,7 @@ export const SessionItem = ({
setContextMenu({ x: e.clientX, y: e.clientY });
}, []);
const sessionLabel = session.firstMessage?.slice(0, 50) ?? 'Session';
const sessionLabel = formatSessionLabel(session.firstMessage);
const handleOpenInCurrentPane = useCallback(() => {
if (!activeProjectId) return;
@ -253,49 +254,86 @@ export const SessionItem = ({
...(isHidden ? { opacity: 0.5 } : {}),
}}
>
{/* First line: title + ongoing indicator + pin/hidden icons */}
<div className="flex items-center gap-1.5">
{multiSelectActive && (
<input
type="checkbox"
checked={isSelected ?? false}
onChange={() => onToggleSelect?.()}
onClick={(e) => e.stopPropagation()}
className="size-3.5 shrink-0 accent-blue-500"
/>
)}
{session.isOngoing && <OngoingIndicator />}
{isPinned && <Pin className="size-2.5 shrink-0 text-blue-400" />}
{isHidden && <EyeOff className="size-2.5 shrink-0 text-zinc-500" />}
<span
className="line-clamp-2 text-[13px] font-medium leading-tight"
style={{ color: isActive ? 'var(--color-text)' : 'var(--color-text-muted)' }}
>
{session.firstMessage ?? 'Untitled'}
</span>
</div>
{/* Second line: message count + time + context consumption */}
<div
className="mt-0.5 flex items-center gap-2 text-[10px] leading-tight"
style={{ color: 'var(--color-text-muted)' }}
>
<span className="flex items-center gap-0.5">
<MessageSquare className="size-2.5" />
{session.messageCount}
</span>
<span style={{ opacity: 0.5 }}>·</span>
<span className="tabular-nums">{formatShortTime(new Date(session.createdAt))}</span>
{session.contextConsumption != null && session.contextConsumption > 0 && (
{(() => {
const parsed = parseSessionTitle(session.firstMessage);
const isTeam = parsed.kind !== 'regular';
return (
<>
<span style={{ opacity: 0.5 }}>·</span>
<ConsumptionBadge
contextConsumption={session.contextConsumption}
phaseBreakdown={session.phaseBreakdown}
/>
{/* First line: title + ongoing indicator + pin/hidden icons */}
<div className="flex items-center gap-1.5">
{multiSelectActive && (
<input
type="checkbox"
checked={isSelected ?? false}
onChange={() => onToggleSelect?.()}
onClick={(e) => e.stopPropagation()}
className="size-3.5 shrink-0 accent-blue-500"
/>
)}
{session.isOngoing && <OngoingIndicator />}
{isPinned && <Pin className="size-2.5 shrink-0 text-blue-400" />}
{isHidden && <EyeOff className="size-2.5 shrink-0 text-zinc-500" />}
{isTeam ? (
<span
className="flex items-center gap-1.5 truncate text-[13px] font-medium leading-tight"
style={{ color: isActive ? 'var(--color-text)' : 'var(--color-text-muted)' }}
>
<Users className="size-3 shrink-0 text-blue-400" />
<span className="truncate">{parsed.displayText}</span>
</span>
) : (
<span
className="line-clamp-2 text-[13px] font-medium leading-tight"
style={{ color: isActive ? 'var(--color-text)' : 'var(--color-text-muted)' }}
>
{parsed.displayText}
</span>
)}
</div>
{/* Second line: metadata */}
<div
className="mt-0.5 flex items-center gap-2 text-[10px] leading-tight"
style={{ color: 'var(--color-text-muted)' }}
>
{isTeam && parsed.projectName && (
<>
<span className="truncate">{parsed.projectName}</span>
<span style={{ opacity: 0.5 }}>·</span>
</>
)}
{isTeam && (
<>
<span className="flex shrink-0 items-center gap-0.5">
{parsed.kind === 'team-resume' ? (
<RotateCw className="size-2.5" />
) : (
<Play className="size-2.5" />
)}
{parsed.kind === 'team-resume' ? 'resume' : 'new'}
</span>
<span style={{ opacity: 0.5 }}>·</span>
</>
)}
<span className="flex shrink-0 items-center gap-0.5">
<MessageSquare className="size-2.5" />
{session.messageCount}
</span>
<span style={{ opacity: 0.5 }}>·</span>
<span className="tabular-nums">{formatShortTime(new Date(session.createdAt))}</span>
{session.contextConsumption != null && session.contextConsumption > 0 && (
<>
<span style={{ opacity: 0.5 }}>·</span>
<ConsumptionBadge
contextConsumption={session.contextConsumption}
phaseBreakdown={session.phaseBreakdown}
/>
</>
)}
</div>
</>
)}
</div>
);
})()}
</button>
{contextMenu &&

View file

@ -3,6 +3,7 @@ import { useCallback, useMemo } from 'react';
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
import { useStore } from '@renderer/store';
import { resolveProjectIdByPath } from '@renderer/utils/projectLookup';
import { formatSessionLabel } from '@renderer/utils/sessionTitleParser';
import { formatDistanceToNowStrict } from 'date-fns';
import {
AlertCircle,
@ -69,7 +70,7 @@ export const TeamSessionsSection = ({
type: 'session',
sessionId: session.id,
projectId,
label: session.firstMessage?.slice(0, 50) ?? 'Session',
label: formatSessionLabel(session.firstMessage),
},
{ forceNewTab: true }
);
@ -173,7 +174,7 @@ const SessionRow = ({
onToggleFilter,
}: SessionRowProps): React.JSX.Element => {
const timeAgo = formatShortTime(new Date(session.createdAt));
const label = session.firstMessage ?? 'Untitled session';
const label = formatSessionLabel(session.firstMessage);
return (
<div

View file

@ -5,6 +5,10 @@ import { buildTaskChangeRequestOptions } from '@renderer/utils/taskChangeRequest
import { ExternalLink } from 'lucide-react';
import { useShallow } from 'zustand/react/shallow';
import {
hasSelectedTargetTeamData,
shouldKeepGlobalTaskDialogLoading,
} from './globalTaskDetailDialogLoading';
import { TaskDetailDialog } from './TaskDetailDialog';
import type { GlobalTask, TeamTaskWithKanban } from '@shared/types';
@ -21,6 +25,7 @@ export const GlobalTaskDetailDialog = (): React.JSX.Element | null => {
selectedTeamName,
selectedTeamData,
selectedTeamLoading,
selectedTeamError,
selectTeam,
openTeamTab,
setPendingReviewRequest,
@ -32,6 +37,7 @@ export const GlobalTaskDetailDialog = (): React.JSX.Element | null => {
selectedTeamName: s.selectedTeamName,
selectedTeamData: s.selectedTeamData,
selectedTeamLoading: s.selectedTeamLoading,
selectedTeamError: s.selectedTeamError,
selectTeam: s.selectTeam,
openTeamTab: s.openTeamTab,
setPendingReviewRequest: s.setPendingReviewRequest,
@ -41,6 +47,11 @@ export const GlobalTaskDetailDialog = (): React.JSX.Element | null => {
const teamName = globalTaskDetail?.teamName ?? '';
const taskId = globalTaskDetail?.taskId ?? '';
const hasTargetTeamData = hasSelectedTargetTeamData(
teamName,
selectedTeamName,
selectedTeamData?.teamName
);
// Load full team data in the background to enable "as before" details (logs/changes/members).
useEffect(() => {
@ -65,13 +76,7 @@ export const GlobalTaskDetailDialog = (): React.JSX.Element | null => {
teamName,
]);
const isFullTeamLoaded = selectedTeamName === teamName && !!selectedTeamData;
// Team data is still loading when:
// - selectTeam() hasn't updated selectedTeamName yet (team switch pending)
// - selectedTeamName matches but IPC fetch is still in flight
const isThisTeamLoading =
selectedTeamName !== teamName ||
(selectedTeamName === teamName && selectedTeamLoading && !selectedTeamData);
const isFullTeamLoaded = hasTargetTeamData;
const taskMap = useMemo(() => {
const map = new Map<string, TeamTaskWithKanban>();
@ -119,12 +124,21 @@ export const GlobalTaskDetailDialog = (): React.JSX.Element | null => {
const kanbanTaskState = isFullTeamLoaded
? selectedTeamData?.kanbanState.tasks[taskId]
: undefined;
const loading = shouldKeepGlobalTaskDialogLoading({
teamName,
taskId,
selectedTeamName,
selectedTeamDataPresent: hasTargetTeamData,
selectedTeamLoading,
selectedTeamError,
hasTaskInMap: taskMap.has(taskId),
});
return (
<TaskDetailDialog
open
variant={isFullTeamLoaded ? 'team' : 'global'}
loading={!isFullTeamLoaded && isThisTeamLoading}
loading={!isFullTeamLoaded && loading}
task={task}
teamName={teamName}
kanbanTaskState={kanbanTaskState}

View file

@ -627,7 +627,7 @@ export const TaskDetailDialog = ({
open={open}
onOpenChange={(v) => {
if (!v && lightboxOpenRef.current) return;
if (!v) onClose();
if (!v) handleClose();
}}
>
<DialogContent

View file

@ -0,0 +1,34 @@
interface GlobalTaskDialogLoadingParams {
teamName: string;
taskId: string;
selectedTeamName: string | null;
selectedTeamDataPresent: boolean;
selectedTeamLoading: boolean;
selectedTeamError: string | null;
hasTaskInMap: boolean;
}
export function hasSelectedTargetTeamData(
targetTeamName: string,
selectedTeamName: string | null,
selectedDataTeamName: string | null | undefined
): boolean {
return selectedTeamName === targetTeamName && selectedDataTeamName === targetTeamName;
}
export function shouldKeepGlobalTaskDialogLoading({
teamName,
taskId,
selectedTeamName,
selectedTeamDataPresent,
selectedTeamLoading,
selectedTeamError,
hasTaskInMap,
}: GlobalTaskDialogLoadingParams): boolean {
if (!teamName || !taskId) return false;
if (selectedTeamName !== teamName) return true;
if (selectedTeamLoading && !selectedTeamDataPresent) return true;
if (selectedTeamDataPresent) return false;
if (selectedTeamError) return false;
return !hasTaskInMap;
}

View file

@ -3,6 +3,7 @@ import { useMemo } from 'react';
import { Button } from '@renderer/components/ui/button';
import { Checkbox } from '@renderer/components/ui/checkbox';
import { Popover, PopoverContent, PopoverTrigger } from '@renderer/components/ui/popover';
import { formatSessionLabel } from '@renderer/utils/sessionTitleParser';
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
import { displayMemberName } from '@renderer/utils/memberHelpers';
import { Crown, Filter } from 'lucide-react';
@ -122,7 +123,7 @@ export const KanbanFilterPopover = ({
{sessions.map((session) => {
const isLead = session.id === leadSessionId;
const isSelected = filter.sessionId === session.id;
const label = session.firstMessage?.slice(0, 50) ?? session.id.slice(0, 8);
const label = formatSessionLabel(session.firstMessage) || session.id.slice(0, 8);
return (
<button
key={session.id}

View file

@ -17,6 +17,33 @@ interface UseViewportCommentReadOptions {
scrollContainer: HTMLElement | null;
}
const VISIBILITY_THRESHOLD = 0.1;
export function getVisibleCommentIdsFallback(
scrollContainer: HTMLElement | null,
elementsById: ReadonlyMap<string, HTMLElement>
): string[] {
if (!scrollContainer || elementsById.size === 0) return [];
const rootRect = scrollContainer.getBoundingClientRect();
const visibleIds: string[] = [];
for (const [commentId, element] of elementsById) {
const rect = element.getBoundingClientRect();
if (rect.width <= 0 || rect.height <= 0) continue;
const visibleWidth = Math.min(rect.right, rootRect.right) - Math.max(rect.left, rootRect.left);
const visibleHeight = Math.min(rect.bottom, rootRect.bottom) - Math.max(rect.top, rootRect.top);
if (visibleWidth <= 0 || visibleHeight <= 0) continue;
if (visibleHeight / rect.height < VISIBILITY_THRESHOLD) continue;
visibleIds.push(commentId);
}
return visibleIds;
}
/**
* Marks task comments as read based on viewport visibility.
*
@ -45,6 +72,7 @@ export function useViewportCommentRead({
flush: () => void;
} {
const seenIdsRef = useRef<Set<string>>(new Set());
const commentElementsRef = useRef<Map<string, HTMLElement>>(new Map());
const teamNameRef = useRef(teamName);
const taskIdRef = useRef(taskId);
@ -56,6 +84,7 @@ export function useViewportCommentRead({
// Reset tracked state when team/task changes
useEffect(() => {
seenIdsRef.current = new Set();
commentElementsRef.current.clear();
}, [teamName, taskId]);
const persistSeen = useCallback(() => {
@ -82,18 +111,37 @@ export function useViewportCommentRead({
const { registerElement } = useViewportObserver({
root: scrollContainer,
threshold: 0.1,
threshold: VISIBILITY_THRESHOLD,
onVisibleChange: handleVisibleChange,
});
const registerComment = useCallback(
(commentId: string) => registerElement(commentId),
(commentId: string) => {
const registerObservedElement = registerElement(commentId);
return (el: HTMLElement | null) => {
if (el) {
commentElementsRef.current.set(commentId, el);
} else {
commentElementsRef.current.delete(commentId);
}
registerObservedElement(el);
};
},
[registerElement]
);
const flush = useCallback(() => {
const fallbackVisibleIds = getVisibleCommentIdsFallback(
scrollContainer,
commentElementsRef.current
);
for (const commentId of fallbackVisibleIds) {
seenIdsRef.current.add(commentId);
}
persistSeen();
}, [persistSeen]);
}, [persistSeen, scrollContainer]);
return { registerComment, flush };
}

View file

@ -0,0 +1,68 @@
/**
* Parses session `firstMessage` into a structured title for sidebar display.
*
* Source formats (generated in src/main/services/team/TeamProvisioningService.ts):
* New team (line ~944): agent_teams_ui [Agent Team: "name" | Project: "proj" | Lead: "lead"] ...
* Resume (line ~1046): Team Start [Agent Team: "name" | Project: "proj" | Lead: "lead"] ...
* (line ~1044): Team Start (resume) [Agent Team: ...] ...
*/
export interface ParsedSessionTitle {
kind: 'team-new' | 'team-resume' | 'regular';
/** Cleaned display text — team name for team sessions, cleaned prompt for regular */
displayText: string;
teamName?: string;
projectName?: string;
}
// Matches: agent_teams_ui [Agent Team: "name" | Project: "proj" | Lead: "lead"]
// Handles both straight quotes ("") and smart quotes (\u201C\u201D)
const PROVISION_RE =
/^agent_teams_ui\s+\[Agent Team:\s*["\u201C]([^"\u201D]+)["\u201D]\s*\|\s*Project:\s*["\u201C]([^"\u201D]+)["\u201D]\s*\|\s*Lead:\s*["\u201C]([^"\u201D]+)["\u201D]\]/;
// Matches: Team Start [Agent Team: ...] or Team Start (resume) [Agent Team: ...]
const LAUNCH_RE =
/^Team Start(?:\s*\(resume\))?\s+\[Agent Team:\s*["\u201C]([^"\u201D]+)["\u201D]\s*\|\s*Project:\s*["\u201C]([^"\u201D]+)["\u201D]\s*\|\s*Lead:\s*["\u201C]([^"\u201D]+)["\u201D]\]/;
// Matches one or more [Image #N] prefixes
const IMAGE_PREFIX_RE = /^(?:\[Image\s+#\d+\]\s*)+/;
export function parseSessionTitle(firstMessage: string | undefined): ParsedSessionTitle {
if (!firstMessage) {
return { kind: 'regular', displayText: 'Untitled' };
}
// New team provisioning: agent_teams_ui [Agent Team: ...]
const provisionMatch = firstMessage.match(PROVISION_RE);
if (provisionMatch) {
return {
kind: 'team-new',
displayText: provisionMatch[1],
teamName: provisionMatch[1],
projectName: provisionMatch[2],
};
}
// Team resume/launch: Team Start [Agent Team: ...]
const launchMatch = firstMessage.match(LAUNCH_RE);
if (launchMatch) {
return {
kind: 'team-resume',
displayText: launchMatch[1],
teamName: launchMatch[1],
projectName: launchMatch[2],
};
}
// Regular session — strip [Image #N] prefixes
const cleaned = firstMessage.replace(IMAGE_PREFIX_RE, '').trim();
return {
kind: 'regular',
displayText: cleaned || 'Untitled',
};
}
/** Convenience: returns just the display label string. */
export function formatSessionLabel(firstMessage: string | undefined): string {
return parseSessionTitle(firstMessage).displayText;
}

View file

@ -0,0 +1,69 @@
import { describe, expect, it } from 'vitest';
import {
hasSelectedTargetTeamData,
shouldKeepGlobalTaskDialogLoading,
} from '../../../../../src/renderer/components/team/dialogs/globalTaskDetailDialogLoading';
describe('shouldKeepGlobalTaskDialogLoading', () => {
it('treats stale selectedTeamData from another team as not loaded', () => {
expect(hasSelectedTargetTeamData('alpha', 'alpha', 'beta')).toBe(false);
expect(hasSelectedTargetTeamData('alpha', 'alpha', 'alpha')).toBe(true);
});
it('keeps loading while team switch has not reached the target team yet', () => {
expect(
shouldKeepGlobalTaskDialogLoading({
teamName: 'alpha',
taskId: 'task-1',
selectedTeamName: 'beta',
selectedTeamDataPresent: false,
selectedTeamLoading: false,
selectedTeamError: null,
hasTaskInMap: false,
})
).toBe(true);
});
it('keeps loading when team data is not ready yet and the task is still absent', () => {
expect(
shouldKeepGlobalTaskDialogLoading({
teamName: 'alpha',
taskId: 'task-1',
selectedTeamName: 'alpha',
selectedTeamDataPresent: false,
selectedTeamLoading: false,
selectedTeamError: null,
hasTaskInMap: false,
})
).toBe(true);
});
it('stops loading once a fallback task snapshot is already available', () => {
expect(
shouldKeepGlobalTaskDialogLoading({
teamName: 'alpha',
taskId: 'task-1',
selectedTeamName: 'alpha',
selectedTeamDataPresent: false,
selectedTeamLoading: false,
selectedTeamError: null,
hasTaskInMap: true,
})
).toBe(false);
});
it('stops loading after a real load error', () => {
expect(
shouldKeepGlobalTaskDialogLoading({
teamName: 'alpha',
taskId: 'task-1',
selectedTeamName: 'alpha',
selectedTeamDataPresent: false,
selectedTeamLoading: false,
selectedTeamError: 'boom',
hasTaskInMap: false,
})
).toBe(false);
});
});

View file

@ -0,0 +1,77 @@
import { describe, expect, it, vi } from 'vitest';
import { getVisibleCommentIdsFallback } from '../../../src/renderer/hooks/useViewportCommentRead';
function makeRect({
top,
bottom,
left = 0,
right = 100,
}: {
top: number;
bottom: number;
left?: number;
right?: number;
}): DOMRect {
return {
x: left,
y: top,
top,
bottom,
left,
right,
width: right - left,
height: bottom - top,
toJSON: () => ({}),
} as DOMRect;
}
describe('getVisibleCommentIdsFallback', () => {
it('returns comment IDs that are visibly inside the scroll container', () => {
const container = document.createElement('div');
const visible = document.createElement('div');
const hidden = document.createElement('div');
vi.spyOn(container, 'getBoundingClientRect').mockReturnValue(
makeRect({ top: 100, bottom: 300 })
);
vi.spyOn(visible, 'getBoundingClientRect').mockReturnValue(makeRect({ top: 120, bottom: 180 }));
vi.spyOn(hidden, 'getBoundingClientRect').mockReturnValue(makeRect({ top: 320, bottom: 380 }));
const result = getVisibleCommentIdsFallback(
container,
new Map([
['visible-comment', visible],
['hidden-comment', hidden],
])
);
expect(result).toEqual(['visible-comment']);
});
it('requires at least 10% of the comment height to be visible', () => {
const container = document.createElement('div');
const barelyVisible = document.createElement('div');
const enoughVisible = document.createElement('div');
vi.spyOn(container, 'getBoundingClientRect').mockReturnValue(
makeRect({ top: 100, bottom: 300 })
);
vi.spyOn(barelyVisible, 'getBoundingClientRect').mockReturnValue(
makeRect({ top: 295, bottom: 405 })
);
vi.spyOn(enoughVisible, 'getBoundingClientRect').mockReturnValue(
makeRect({ top: 290, bottom: 390 })
);
const result = getVisibleCommentIdsFallback(
container,
new Map([
['barely-visible', barelyVisible],
['enough-visible', enoughVisible],
])
);
expect(result).toEqual(['enough-visible']);
});
});

View file

@ -0,0 +1,124 @@
import { describe, expect, it } from 'vitest';
import { formatSessionLabel, parseSessionTitle } from '@renderer/utils/sessionTitleParser';
describe('parseSessionTitle', () => {
it('returns regular/Untitled for undefined', () => {
expect(parseSessionTitle(undefined)).toEqual({
kind: 'regular',
displayText: 'Untitled',
});
});
it('returns regular/Untitled for empty string', () => {
expect(parseSessionTitle('')).toEqual({
kind: 'regular',
displayText: 'Untitled',
});
});
it('parses new team provisioning with straight quotes', () => {
const msg =
'agent_teams_ui [Agent Team: "summit-ops" | Project: "sol_team_proj" | Lead: "team-lead"] — team does NOT exist yet.';
const result = parseSessionTitle(msg);
expect(result).toEqual({
kind: 'team-new',
displayText: 'summit-ops',
teamName: 'summit-ops',
projectName: 'sol_team_proj',
});
});
it('parses new team provisioning with smart quotes', () => {
const msg =
'agent_teams_ui [Agent Team: \u201Csummit-ops\u201D | Project: \u201Csol_team_proj\u201D | Lead: \u201Cteam-lead\u201D] \u2014 team does NOT exist yet.';
const result = parseSessionTitle(msg);
expect(result).toEqual({
kind: 'team-new',
displayText: 'summit-ops',
teamName: 'summit-ops',
projectName: 'sol_team_proj',
});
});
it('parses Team Start as resume', () => {
const msg =
'Team Start [Agent Team: "atlas-hq-2" | Project: "sol_team_proj" | Lead: "team-lead"] You are running in a non-interactive CLI session.';
const result = parseSessionTitle(msg);
expect(result).toEqual({
kind: 'team-resume',
displayText: 'atlas-hq-2',
teamName: 'atlas-hq-2',
projectName: 'sol_team_proj',
});
});
it('parses Team Start (resume) as resume', () => {
const msg =
'Team Start (resume) [Agent Team: "atlas-hq-2" | Project: "sol_team_proj" | Lead: "team-lead"] You are running...';
const result = parseSessionTitle(msg);
expect(result).toEqual({
kind: 'team-resume',
displayText: 'atlas-hq-2',
teamName: 'atlas-hq-2',
projectName: 'sol_team_proj',
});
});
it('passes through regular text as-is', () => {
const msg = 'Fix the login bug in auth module';
const result = parseSessionTitle(msg);
expect(result).toEqual({
kind: 'regular',
displayText: 'Fix the login bug in auth module',
});
});
it('strips single [Image #N] prefix', () => {
const msg = '[Image #1] Сделай чтобы было без иконки';
const result = parseSessionTitle(msg);
expect(result).toEqual({
kind: 'regular',
displayText: 'Сделай чтобы было без иконки',
});
});
it('strips multiple [Image #N] prefixes', () => {
const msg = '[Image #1] [Image #2] Something with two images';
const result = parseSessionTitle(msg);
expect(result).toEqual({
kind: 'regular',
displayText: 'Something with two images',
});
});
it('returns Untitled when only image prefixes remain', () => {
const msg = '[Image #1] ';
const result = parseSessionTitle(msg);
expect(result).toEqual({
kind: 'regular',
displayText: 'Untitled',
});
});
});
describe('formatSessionLabel', () => {
it('returns team name for new team session', () => {
const msg =
'agent_teams_ui [Agent Team: "my-team" | Project: "my-proj" | Lead: "lead"] — team does NOT exist yet.';
expect(formatSessionLabel(msg)).toBe('my-team');
});
it('returns team name for resume session', () => {
const msg = 'Team Start [Agent Team: "my-team" | Project: "proj" | Lead: "lead"] ...';
expect(formatSessionLabel(msg)).toBe('my-team');
});
it('returns cleaned text for regular session', () => {
expect(formatSessionLabel('Hello world')).toBe('Hello world');
});
it('returns Untitled for undefined', () => {
expect(formatSessionLabel(undefined)).toBe('Untitled');
});
});