Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/web/src/pages/project-chat/MobileSessionDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ export function MobileSessionDrawer({
<nav aria-label="Chat sessions" className="flex-1 overflow-y-auto min-h-0">
<SessionList
sessions={filteredR}
allSessions={sessions}
selectedSessionId={selectedSessionId}
onSelect={onSelect}
onFork={onFork}
Expand All @@ -179,6 +180,7 @@ export function MobileSessionDrawer({
{showOlder && (
<SessionList
sessions={filteredS}
allSessions={sessions}
selectedSessionId={selectedSessionId}
onSelect={onSelect}
onFork={onFork}
Expand Down
4 changes: 4 additions & 0 deletions apps/web/src/pages/project-chat/SessionItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export function SessionItem({
progressBar,
blockedBadge,
blockedByTitle,
ariaLabel,
}: {
session: ChatSessionResponse;
isSelected: boolean;
Expand All @@ -39,6 +40,8 @@ export function SessionItem({
blockedBadge?: boolean;
/** Title of the task this item is blocked by. */
blockedByTitle?: string;
/** Accessible label override for the select button (e.g., context-anchor annotation). */
ariaLabel?: string;
}) {
const state = getSessionState(session);
const dotColor = blockedBadge ? 'var(--sam-color-danger, #ef4444)' : STATE_COLORS[state];
Expand Down Expand Up @@ -76,6 +79,7 @@ export function SessionItem({
<button
type="button"
onClick={() => onSelect(session.id)}
aria-label={ariaLabel}
className="block w-full text-left bg-transparent border-none cursor-pointer p-0 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-[var(--sam-color-focus-ring)]"
>
{/* Idea tag — only for default/parent variants */}
Expand Down
94 changes: 29 additions & 65 deletions apps/web/src/pages/project-chat/SessionList.tsx
Original file line number Diff line number Diff line change
@@ -1,94 +1,58 @@
import { Clock } from 'lucide-react';
import { useMemo } from 'react';

import type { ChatSessionResponse } from '../../lib/api';
import { SessionItem } from './SessionItem';
import { TaskGroup } from './TaskGroup';
import { groupHasMatchingChild, groupSessions, type TaskInfo } from './useTaskGroups';

/** Returns a small clock badge for triggered sessions, or undefined. */
function triggerBadge(session: ChatSessionResponse, taskInfoMap: Map<string, TaskInfo>) {
if (!session.taskId) return undefined;
const info = taskInfoMap.get(session.taskId);
if (!info || info.triggeredBy === 'user') return undefined;
return (
<span
className="inline-flex items-center gap-0.5 text-[9px] font-semibold px-1 py-0 rounded-full whitespace-nowrap"
style={{
color: 'var(--sam-color-info, #3b82f6)',
background: 'color-mix(in srgb, var(--sam-color-info, #3b82f6) 12%, transparent)',
}}
title="Triggered by automation"
>
<Clock size={8} /> AUTO
</span>
);
}
import { buildSessionTree } from './sessionTree';
import { SessionTreeItem } from './SessionTreeItem';
import type { TaskInfo } from './useTaskGroups';

/**
* Renders a list of sessions with task-hierarchy grouping.
* Renders a list of sessions as a hierarchical tree derived from
* `task.parentTaskId`. Supports arbitrary nesting depth.
*
* Parent tasks with sub-tasks are rendered in grouped card containers.
* Standalone sessions render as normal flat items.
* When `allSessions` is provided, ancestors that exist in the full session
* list but NOT in `sessions` (e.g. a stopped/stale parent of a currently
* active nested session) are lifted in as dimmed "context anchor" rows so
* the descendant remains visible with its lineage intact.
*/
export function SessionList({
sessions,
allSessions,
selectedSessionId,
onSelect,
onFork,
taskTitleMap,
taskInfoMap,
searchQuery = '',
}: {
/** Sessions to display (already filtered to the visible bucket, e.g. recent or stale). */
sessions: ChatSessionResponse[];
/** Full session list for resolving lineage across buckets (optional). */
allSessions?: ChatSessionResponse[];
selectedSessionId: string | null;
onSelect: (id: string) => void;
onFork?: (session: ChatSessionResponse) => void;
taskTitleMap: Map<string, string>;
/** Retained for API compatibility but no longer used directly — titles come from taskInfoMap. */
taskTitleMap?: Map<string, string>;

Check warning on line 34 in apps/web/src/pages/project-chat/SessionList.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'taskTitleMap' PropType is defined but prop is never used

See more on https://sonarcloud.io/project/issues?id=raphaeltm_simple-agent-manager&issues=AZ2iqGlj6Xp1rfcOcfgJ&open=AZ2iqGlj6Xp1rfcOcfgJ&pullRequest=759
taskInfoMap: Map<string, TaskInfo>;
searchQuery?: string;
}) {
const renderItems = useMemo(
() => groupSessions(sessions, taskInfoMap),
[sessions, taskInfoMap],
const roots = useMemo(
() => buildSessionTree(sessions, taskInfoMap, { allSessions }),
[sessions, allSessions, taskInfoMap],
);

return (
<>
{renderItems.map((item) => {
if (item.type === 'group') {
const { group } = item;
// When searching, auto-expand if a child matches
const hasChildMatch = searchQuery
? groupHasMatchingChild(group, searchQuery, taskInfoMap)
: false;

return (
<TaskGroup
key={group.parent.id}
group={group}
selectedSessionId={selectedSessionId}
onSelect={onSelect}
onFork={onFork}
taskInfoMap={taskInfoMap}
defaultExpanded={hasChildMatch}
/>
);
}

const session = item.session;
return (
<SessionItem
key={session.id}
session={session}
isSelected={session.id === selectedSessionId}
onSelect={onSelect}
onFork={onFork}
ideaTitle={session.taskId ? taskTitleMap.get(session.taskId) : undefined}
badge={triggerBadge(session, taskInfoMap)}
/>
);
})}
{roots.map((root) => (
<SessionTreeItem
key={root.session.id}
node={root}
selectedSessionId={selectedSessionId}
onSelect={onSelect}
onFork={onFork}
taskInfoMap={taskInfoMap}
searchQuery={searchQuery}
/>
))}
</>
);
}
Loading
Loading