From ef0a5a30da573085f05f327c7784ba1cacd8851d Mon Sep 17 00:00:00 2001 From: Jiawei Zhao Date: Thu, 3 Sep 2026 12:39:31 +0800 Subject: [PATCH 1/4] feat(ui): show pinned tasks by project Pinning appeared to do nothing in the project view because only the time view exposed the global pinned section. Keep the presentation consistent while preserving each task's project membership. CLOSES #4612 Generated-by: Codex Signed-off-by: Jiawei Zhao --- .../session-history-row-actions.test.tsx | 39 +++++++++++++++++++ packages/ui/src/session-history-list.tsx | 28 ++++++++++--- .../ui/stories/session-list-panel.stories.tsx | 1 + 3 files changed, 62 insertions(+), 6 deletions(-) diff --git a/packages/ui/src/__tests__/session-history-row-actions.test.tsx b/packages/ui/src/__tests__/session-history-row-actions.test.tsx index 15d7523263..040d761947 100644 --- a/packages/ui/src/__tests__/session-history-row-actions.test.tsx +++ b/packages/ui/src/__tests__/session-history-row-actions.test.tsx @@ -303,6 +303,45 @@ test('renders collapsible project navigation and row actions as sibling controls assertNoNestedButtons(markup); }); +test('renders pinned tasks once above project groups', () => { + const pinnedSession: SessionSummary = { + ...session, + id: 'session-pinned', + name: 'Pinned task', + isFlagged: true, + }; + const projectSession: SessionSummary = { + ...session, + id: 'session-project', + name: 'Project task', + }; + const markup = renderToStaticMarkup( + + + , + ); + + const { document } = parseHTML(markup); + const projectRow = document.querySelector('.maka-project-row'); + assert.ok(projectRow); + assert.match(markup, />Pinned { const locallyStreaming = { ...session, diff --git a/packages/ui/src/session-history-list.tsx b/packages/ui/src/session-history-list.tsx index 7d9d986871..be3d8bd5ee 100644 --- a/packages/ui/src/session-history-list.tsx +++ b/packages/ui/src/session-history-list.tsx @@ -352,7 +352,8 @@ function SessionListGroups(props: { }>; }) { const rail = useSessionRailData(); - const copy = getConversationCopy(useUiLocale()).sessions; + const locale = useUiLocale(); + const copy = getConversationCopy(locale).sessions; const [renameTarget, setRenameTarget] = useState(null); /** * The control the rename was started from, so focus can go back to it. @@ -470,18 +471,27 @@ function SessionListGroups(props: { if (rail.groupVariant === 'project') { const activeGroups = props.groups.filter((group) => group.project?.archivedAt === undefined); const archivedGroups = props.groups.filter((group) => group.project?.archivedAt !== undefined); + const pinnedSessions = groupSessionsForHistory( + activeGroups.flatMap((group) => group.sessions.filter((session) => session.isFlagged)), + locale, + ).find((group) => group.id === 'pinned')?.sessions ?? []; function renderProjectGroup( group: (typeof props.groups)[number], + includePinned = false, ): ReactNode { const project = group.project; + const sessions = includePinned + ? group.sessions + : group.sessions.filter((session) => !session.isFlagged); return ( { @@ -497,7 +507,12 @@ function SessionListGroups(props: { return ( <> {renameDialog} - {activeGroups.map(renderProjectGroup)} + {pinnedSessions.length > 0 && ( + + {pinnedSessions.map((session) => renderSessionRow(session))} + + )} + {activeGroups.map((group) => renderProjectGroup(group))} {archivedGroups.length > 0 && ( renderProjectGroup(group, true))} )} @@ -545,6 +560,7 @@ function ProjectNavRow(props: { label: string; project?: ProjectRecord; sessions: SessionSummary[]; + summarySessions?: SessionSummary[]; streamingSessionIds?: ReadonlySet; projectActions?: ProjectRowActions; onStartRename(opener: HTMLElement | null): void; @@ -556,10 +572,10 @@ function ProjectNavRow(props: { () => createProjectHoverCardSummary( props.project, - props.sessions, + props.summarySessions ?? props.sessions, props.streamingSessionIds, ), - [props.project, props.sessions, props.streamingSessionIds], + [props.project, props.sessions, props.streamingSessionIds, props.summarySessions], ); // Collapsible only when there is a real session subtree. An empty VStack is // still truthy children for Astryx (!!children) and fabricates a disclosure. diff --git a/packages/ui/stories/session-list-panel.stories.tsx b/packages/ui/stories/session-list-panel.stories.tsx index 32f89c84ad..7224049fc9 100644 --- a/packages/ui/stories/session-list-panel.stories.tsx +++ b/packages/ui/stories/session-list-panel.stories.tsx @@ -414,6 +414,7 @@ export const ProjectGroups: Story = { makeSession({ id: 'proj-main', name: '主仓会话', + isFlagged: true, lastMessageAt: NOW - 4 * 60 * 1000, }), makeSession({ From 7c23d373a8cf6a41148c5d3b83105ba0a5496368 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Thu, 3 Sep 2026 14:06:56 +0800 Subject: [PATCH 2/4] refactor(ui): group by project as two sibling SideNavSections The pinned zone and the project rows are two different Astryx primitives laid out as siblings. SideNavSection is a group of navigation items, not one of them: it renders role="group" with aria-labelledby, and its documented shape is a Section containing Items. Flattening one Section next to a row of SideNavItems puts a group heading and navigation items on the same level, and the seam shows. The pinned heading is small grey text where the project rows are full items with a folder icon; the project rows collapse and the pinned zone cannot; the project rows carry a row menu and the pinned zone does not. The one thing the two do share is the indent under them, so the layout promises a hierarchy it gives no way to operate. Render the two as sibling sections instead, "Pinned" and "Projects". The containment relationship is restored, project rows stay items inside a section with their icon, disclosure and row menu untouched, and the two grouping modes end up structurally symmetric: Pinned / Recent by time, Pinned / Projects by project. Archived projects stay the last item of the Projects section, because what that row holds is projects. Hoisting the pinned tasks out of a project also left the project row describing rows it no longer shows. The row derived its disclosure and its menu placement from the filtered list while the hover summary read the unfiltered one, so a project whose only visible task was pinned drew no chevron and no subtree while its accessible description still announced 1 task. Drop the second list: the summary reads the same sessions the row renders, so the count, the chevron and the menu placement have one source. A story fixture covers that shape. Generated-by: Claude Code --- .../session-history-row-actions.test.tsx | 65 +++++++++++++-- packages/ui/src/conversation-copy.ts | 6 +- packages/ui/src/session-history-list.tsx | 47 ++++++----- .../ui/stories/session-list-panel.stories.tsx | 81 +++++++++++++++++++ 4 files changed, 174 insertions(+), 25 deletions(-) diff --git a/packages/ui/src/__tests__/session-history-row-actions.test.tsx b/packages/ui/src/__tests__/session-history-row-actions.test.tsx index 040d761947..63b74314c0 100644 --- a/packages/ui/src/__tests__/session-history-row-actions.test.tsx +++ b/packages/ui/src/__tests__/session-history-row-actions.test.tsx @@ -85,6 +85,17 @@ const projectActions: ProjectRowActions = { onRestore: () => undefined, }; +/** The list's top-level `SideNavSection`s, in document order, by their title. */ +function readSections(document: Document): Array<{ title: string; element: Element }> { + return [...document.querySelectorAll('.maka-session-list > [role="group"]')].map((element) => { + const labelId = element.getAttribute('aria-labelledby'); + return { + title: (labelId ? document.getElementById(labelId)?.textContent : undefined) ?? '', + element, + }; + }); +} + function assertNoNestedButtons(markup: string): void { // Structural check. A real regression here moves the action menu inside the // navigation control, and the menu always ships wrapped in @@ -333,15 +344,59 @@ test('renders pinned tasks once above project groups', () => { ); const { document } = parseHTML(markup); - const projectRow = document.querySelector('.maka-project-row'); - assert.ok(projectRow); - assert.match(markup, />Pinned section.title), + ['Pinned', 'Projects'], + 'pinned tasks and project rows are sibling sections, not a section beside bare items', + ); + const [pinned, projects] = sections; + assert.ok(pinned && projects); assert.equal(markup.match(/Pinned task/g)?.length, 1); - assert.doesNotMatch(projectRow.textContent, /Pinned task/); + assert.match(pinned.element.textContent, /Pinned task/); + assert.doesNotMatch(projects.element.textContent, /Pinned task/); + const projectRow = projects.element.querySelector('.maka-project-row'); + assert.ok(projectRow, 'project rows are items inside the Projects section'); assert.match(projectRow.textContent, /Project task/); }); +test('a project whose only task is pinned describes itself as empty', () => { + const pinnedSession: SessionSummary = { + ...session, + id: 'session-pinned', + name: 'Pinned task', + isFlagged: true, + }; + const markup = renderToStaticMarkup( + + + , + ); + + const { document } = parseHTML(markup); + const projectRow = document.querySelector('.maka-project-row'); + assert.ok(projectRow); + const navigation = projectRow.querySelector(':scope > div > button'); + assert.ok(navigation); + assert.equal(navigation.getAttribute('aria-controls'), null, 'no disclosure without a subtree'); + const describedBy = navigation.getAttribute('aria-describedby'); + assert.ok(describedBy); + const description = document.getElementById(describedBy); + assert.ok(description); + assert.match( + description.getAttribute('aria-label') ?? '', + /\b0 tasks\b/, + 'the hover description counts what the row actually shows', + ); + const action = document.querySelector('button[aria-label="Maka project actions"]'); + assert.ok(action); +}); + test('keeps project running totals aligned with renderer-local task streaming', () => { const locallyStreaming = { ...session, diff --git a/packages/ui/src/conversation-copy.ts b/packages/ui/src/conversation-copy.ts index 98ae0115c0..3042b31f3c 100644 --- a/packages/ui/src/conversation-copy.ts +++ b/packages/ui/src/conversation-copy.ts @@ -416,6 +416,8 @@ export interface ConversationCopy { pinned: string; /** Time-sort unpinned section title (SideNavSection). */ recent: string; + /** Project-sort section title, sibling of `pinned` (SideNavSection). */ + projects: string; groupByTime: string; groupByProject: string; groupingAriaLabel: string; @@ -571,7 +573,7 @@ const CONVERSATION_COPY = { sessions: { status: { active: '可继续', running: '进行中', waiting_for_user: '等你确认', blocked: '需要处理', aborted: '已中止' }, blockedReason: { NO_REAL_CONNECTION: '等待配置可用模型连接', auth: '需要重新登录', permission_required: '等待权限确认', tool_failed: '工具调用失败', unknown: '运行中断,可重试' }, - listAriaLabel: '任务列表', showMore: '显示更多', showMoreAriaLabel: (count) => `显示 ${count} 条更多任务`, renameAriaLabel: '重命名任务', renameProjectTitle: '重命名项目', renameSubmit: '保存', respondingAriaLabel: '正在响应', respondingTitle: '任务正在流式响应中', staleTitle: '此任务使用的模型连接已不可用,发送时会切换到默认连接', staleAriaLabel: '任务已过期', stale: '已过期', unreadAriaLabel: '未读消息', actionsAriaLabel: (name) => `${name} 任务操作`, pin: '置顶', unpin: '取消置顶', rename: '重命名', archive: '归档', unarchive: '取消归档', delete: '删除', pinned: '置顶', recent: '最近', groupByTime: '按时间', groupByProject: '按项目', groupingAriaLabel: '任务分组方式', projectActionsAriaLabel: (name) => `${name} 项目操作`, projectNewTask: '新建任务', projectRename: '重命名', projectArchive: '归档', projectRestore: '恢复', projectRelink: '重新定位', projectUnavailable: '项目目录不可用', archivedProjects: '已归档项目', archivedProjectsAriaLabel: '展开已归档项目', worktreeAriaLabel: 'Git 工作树', promptRailAriaLabel: '按提问跳转', emptyPrompt: '(空提问)', jumpToPrompt: (preview) => `跳到提问:${preview}`, pickedAriaLabel: '已选中', pinCount: (count) => `置顶 ${count} 项`, unpinCount: (count) => `取消置顶 ${count} 项`, archiveCount: (count) => `归档 ${count} 项`, + listAriaLabel: '任务列表', showMore: '显示更多', showMoreAriaLabel: (count) => `显示 ${count} 条更多任务`, renameAriaLabel: '重命名任务', renameProjectTitle: '重命名项目', renameSubmit: '保存', respondingAriaLabel: '正在响应', respondingTitle: '任务正在流式响应中', staleTitle: '此任务使用的模型连接已不可用,发送时会切换到默认连接', staleAriaLabel: '任务已过期', stale: '已过期', unreadAriaLabel: '未读消息', actionsAriaLabel: (name) => `${name} 任务操作`, pin: '置顶', unpin: '取消置顶', rename: '重命名', archive: '归档', unarchive: '取消归档', delete: '删除', pinned: '置顶', recent: '最近', projects: '项目', groupByTime: '按时间', groupByProject: '按项目', groupingAriaLabel: '任务分组方式', projectActionsAriaLabel: (name) => `${name} 项目操作`, projectNewTask: '新建任务', projectRename: '重命名', projectArchive: '归档', projectRestore: '恢复', projectRelink: '重新定位', projectUnavailable: '项目目录不可用', archivedProjects: '已归档项目', archivedProjectsAriaLabel: '展开已归档项目', worktreeAriaLabel: 'Git 工作树', promptRailAriaLabel: '按提问跳转', emptyPrompt: '(空提问)', jumpToPrompt: (preview) => `跳到提问:${preview}`, pickedAriaLabel: '已选中', pinCount: (count) => `置顶 ${count} 项`, unpinCount: (count) => `取消置顶 ${count} 项`, archiveCount: (count) => `归档 ${count} 项`, }, }, en: { @@ -729,7 +731,7 @@ const CONVERSATION_COPY = { sessions: { status: { active: 'Ready', running: 'Running', waiting_for_user: 'Waiting for you', blocked: 'Needs attention', aborted: 'Stopped' }, blockedReason: { NO_REAL_CONNECTION: 'Waiting for an available model connection', auth: 'Sign in again', permission_required: 'Waiting for permission', tool_failed: 'Tool call failed', unknown: 'Run interrupted; retry available' }, - listAriaLabel: 'Task list', showMore: 'Show more', showMoreAriaLabel: (count) => `Show ${count} more tasks`, renameAriaLabel: 'Rename task', renameProjectTitle: 'Rename project', renameSubmit: 'Save', respondingAriaLabel: 'Responding', respondingTitle: 'This task is streaming a response', staleTitle: 'This task\'s model connection is unavailable; sending will switch to the default connection', staleAriaLabel: 'Stale task', stale: 'Stale', unreadAriaLabel: 'Unread messages', actionsAriaLabel: (name) => `${name} task actions`, pin: 'Pin', unpin: 'Unpin', rename: 'Rename', archive: 'Archive', unarchive: 'Unarchive', delete: 'Delete', pinned: 'Pinned', recent: 'Recent', groupByTime: 'By time', groupByProject: 'By project', groupingAriaLabel: 'Task grouping', projectActionsAriaLabel: (name) => `${name} project actions`, projectNewTask: 'New task', projectRename: 'Rename', projectArchive: 'Archive', projectRestore: 'Restore', projectRelink: 'Relocate', projectUnavailable: 'Project directory unavailable', archivedProjects: 'Archived projects', archivedProjectsAriaLabel: 'Expand archived projects', worktreeAriaLabel: 'Git worktree', promptRailAriaLabel: 'Jump by prompt', emptyPrompt: '(empty prompt)', jumpToPrompt: (preview) => `Jump to prompt: ${preview}`, pickedAriaLabel: 'Selected', pinCount: (count) => `Pin ${count} tasks`, unpinCount: (count) => `Unpin ${count} tasks`, archiveCount: (count) => `Archive ${count} tasks`, + listAriaLabel: 'Task list', showMore: 'Show more', showMoreAriaLabel: (count) => `Show ${count} more tasks`, renameAriaLabel: 'Rename task', renameProjectTitle: 'Rename project', renameSubmit: 'Save', respondingAriaLabel: 'Responding', respondingTitle: 'This task is streaming a response', staleTitle: 'This task\'s model connection is unavailable; sending will switch to the default connection', staleAriaLabel: 'Stale task', stale: 'Stale', unreadAriaLabel: 'Unread messages', actionsAriaLabel: (name) => `${name} task actions`, pin: 'Pin', unpin: 'Unpin', rename: 'Rename', archive: 'Archive', unarchive: 'Unarchive', delete: 'Delete', pinned: 'Pinned', recent: 'Recent', projects: 'Projects', groupByTime: 'By time', groupByProject: 'By project', groupingAriaLabel: 'Task grouping', projectActionsAriaLabel: (name) => `${name} project actions`, projectNewTask: 'New task', projectRename: 'Rename', projectArchive: 'Archive', projectRestore: 'Restore', projectRelink: 'Relocate', projectUnavailable: 'Project directory unavailable', archivedProjects: 'Archived projects', archivedProjectsAriaLabel: 'Expand archived projects', worktreeAriaLabel: 'Git worktree', promptRailAriaLabel: 'Jump by prompt', emptyPrompt: '(empty prompt)', jumpToPrompt: (preview) => `Jump to prompt: ${preview}`, pickedAriaLabel: 'Selected', pinCount: (count) => `Pin ${count} tasks`, unpinCount: (count) => `Unpin ${count} tasks`, archiveCount: (count) => `Archive ${count} tasks`, }, }, } satisfies UiCatalog; diff --git a/packages/ui/src/session-history-list.tsx b/packages/ui/src/session-history-list.tsx index be3d8bd5ee..f45c1d485d 100644 --- a/packages/ui/src/session-history-list.tsx +++ b/packages/ui/src/session-history-list.tsx @@ -491,7 +491,6 @@ function SessionListGroups(props: { label={group.label} project={project} sessions={sessions} - summarySessions={group.sessions} streamingSessionIds={rail.streamingSessionIds} projectActions={rail.projectActions} onStartRename={(opener) => { @@ -504,6 +503,11 @@ function SessionListGroups(props: { ); } + // Two sibling sections, the same shape the time view has. A section groups + // items; it is not one of them. Putting the pinned section next to bare + // project rows would make the same level hold both a group heading and + // navigation items, and the pinned zone would be the only one there without + // a folder icon, a disclosure or a row menu. return ( <> {renameDialog} @@ -512,20 +516,24 @@ function SessionListGroups(props: { {pinnedSessions.map((session) => renderSessionRow(session))} )} - {activeGroups.map((group) => renderProjectGroup(group))} - {archivedGroups.length > 0 && ( - setArchivedExpanded(!collapsed), - }} - > - {/* Always mount children: Astryx derives collapsible chrome from - !!children. Nulling on collapse removes the chevron and makes - the controlled isCollapsed prop a no-op. */} - {archivedGroups.map((group) => renderProjectGroup(group, true))} - + {(activeGroups.length > 0 || archivedGroups.length > 0) && ( + + {activeGroups.map((group) => renderProjectGroup(group))} + {archivedGroups.length > 0 && ( + setArchivedExpanded(!collapsed), + }} + > + {/* Always mount children: Astryx derives collapsible chrome from + !!children. Nulling on collapse removes the chevron and makes + the controlled isCollapsed prop a no-op. */} + {archivedGroups.map((group) => renderProjectGroup(group, true))} + + )} + )} ); @@ -560,7 +568,6 @@ function ProjectNavRow(props: { label: string; project?: ProjectRecord; sessions: SessionSummary[]; - summarySessions?: SessionSummary[]; streamingSessionIds?: ReadonlySet; projectActions?: ProjectRowActions; onStartRename(opener: HTMLElement | null): void; @@ -568,14 +575,18 @@ function ProjectNavRow(props: { }) { const containerRef = useRef(null); const hoverDescriptionId = useId(); + // The same list the row draws its subtree from. A summary counting rows that + // were hoisted into the pinned section describes a project row that has no + // disclosure and no children, and puts its menu somewhere else than the count + // implies. const hoverSummary = useMemo( () => createProjectHoverCardSummary( props.project, - props.summarySessions ?? props.sessions, + props.sessions, props.streamingSessionIds, ), - [props.project, props.sessions, props.streamingSessionIds, props.summarySessions], + [props.project, props.sessions, props.streamingSessionIds], ); // Collapsible only when there is a real session subtree. An empty VStack is // still truthy children for Astryx (!!children) and fabricates a disclosure. diff --git a/packages/ui/stories/session-list-panel.stories.tsx b/packages/ui/stories/session-list-panel.stories.tsx index 7224049fc9..9a5700d44d 100644 --- a/packages/ui/stories/session-list-panel.stories.tsx +++ b/packages/ui/stories/session-list-panel.stories.tsx @@ -471,3 +471,84 @@ export const ProjectGroups: Story = { ); }, }; + +// Group-by-project where a project's only task is pinned, so the project row +// has nothing left to show. What the row says about itself — disclosure, +// action placement, the hover card's task count — has to follow what is +// actually under it, and an archived project sits below the live ones. +export const ProjectGroupsPinnedOnlyTask: Story = { + render: () => { + const solo = makeProject({ + id: 'project-solo', + name: '独苗项目', + preferredPath: '/workspace/solo', + }); + const docs = makeProject({ + id: 'project-docs', + name: '产品文档', + preferredPath: '/workspace/docs', + }); + const retired = makeProject({ + id: 'project-retired', + name: '旧版桌面端', + preferredPath: '/workspace/legacy', + archivedAt: NOW - 30 * 24 * 60 * 60 * 1000, + }); + const sessions = [ + makeSession({ + id: 'solo-only', + name: '唯一的任务', + isFlagged: true, + lastMessageAt: NOW - 6 * 60 * 1000, + }), + makeSession({ + id: 'docs-a', + name: '文档站改版', + lastMessageAt: NOW - 30 * 60 * 1000, + }), + makeSession({ + id: 'retired-a', + name: '旧版遗留任务', + lastMessageAt: NOW - 40 * 24 * 60 * 60 * 1000, + }), + ]; + return ( + + + + ); + }, +}; From bb78723508ca8fb898f056623a60e546236e7f8e Mon Sep 17 00:00:00 2001 From: Jiawei Zhao Date: Thu, 3 Sep 2026 10:40:25 +0800 Subject: [PATCH 3/4] test(desktop): request top history explicitly The scroll E2E recurred because moving to zero was treated as the request even though a settled scroller cannot move farther. Dispatching the upward wheel matches the production trigger and removes timing dependence on a scroll event. Signed-off-by: Jiawei Zhao Generated-by: OpenAI Codex --- apps/desktop/e2e/transcript-scroll.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/desktop/e2e/transcript-scroll.spec.ts b/apps/desktop/e2e/transcript-scroll.spec.ts index 2c81b5a68d..b29634fc12 100644 --- a/apps/desktop/e2e/transcript-scroll.spec.ts +++ b/apps/desktop/e2e/transcript-scroll.spec.ts @@ -640,6 +640,7 @@ test('history asked for at the very top of the scroller still lands above the re const root = document.querySelector(selector); if (!root) throw new Error('the chat scroll container is missing'); root.scrollTop = 0; + root.dispatchEvent(new WheelEvent('wheel', { deltaY: -120, bubbles: true })); }, SCROLLER); await expect.poll(firstLoadedTurn, { timeout: 20_000 }).not.toBe(firstBefore); From af54c482d2570a8131f4cad3a19e392ad469908f Mon Sep 17 00:00:00 2001 From: Jiawei Zhao Date: Thu, 3 Sep 2026 14:36:26 +0800 Subject: [PATCH 4/4] refactor(ui): simplify pinned group lookup The history grouping helper already owns the pinned predicate and label. Using its result directly keeps that definition in one place. Signed-off-by: Jiawei Zhao Generated-by: OpenAI Codex --- packages/ui/src/session-history-list.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/ui/src/session-history-list.tsx b/packages/ui/src/session-history-list.tsx index f45c1d485d..447c1018c5 100644 --- a/packages/ui/src/session-history-list.tsx +++ b/packages/ui/src/session-history-list.tsx @@ -471,10 +471,10 @@ function SessionListGroups(props: { if (rail.groupVariant === 'project') { const activeGroups = props.groups.filter((group) => group.project?.archivedAt === undefined); const archivedGroups = props.groups.filter((group) => group.project?.archivedAt !== undefined); - const pinnedSessions = groupSessionsForHistory( - activeGroups.flatMap((group) => group.sessions.filter((session) => session.isFlagged)), + const pinnedGroup = groupSessionsForHistory( + activeGroups.flatMap((group) => group.sessions), locale, - ).find((group) => group.id === 'pinned')?.sessions ?? []; + ).find((group) => group.id === 'pinned'); function renderProjectGroup( group: (typeof props.groups)[number], @@ -511,9 +511,9 @@ function SessionListGroups(props: { return ( <> {renameDialog} - {pinnedSessions.length > 0 && ( - - {pinnedSessions.map((session) => renderSessionRow(session))} + {pinnedGroup && ( + + {pinnedGroup.sessions.map((session) => renderSessionRow(session))} )} {(activeGroups.length > 0 || archivedGroups.length > 0) && (