From ed01d0ed3fb68bcaf33528404f310644e1a2aa37 Mon Sep 17 00:00:00 2001 From: jean-baptiste Date: Fri, 21 Aug 2026 15:46:13 +0200 Subject: [PATCH 1/3] fix(sidebar): attach subagent children to sessions rendered inside slug groups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit buildSlugGroup() appended its sessions via the raw buildSessionItem(session), never through appendSubagentChildren — only the ungrouped top-level render path called that helper. Any session rendered inside a slug group (e.g. a schedule rerun sharing a slug with an earlier run, the only real producer of grouped top-level sessions) silently lost its subagent caret/children, and because the group
carries no dataset.sessionId, the orphan-subagents pass in buildSessionsList didn't recognize the grouped session as accounted for either, duplicating its subagents into the "Orphan subagents" bucket. appendSubagentChildren() is hoisted to module scope so buildSlugGroup can call it per session, and a new collectTopLevelSessionIds() walks into a group element for the orphan-detection pass. Adds test/dom-slug-group-subagent-nesting.test.js, seeded with the exact session_cache row shape schedule-runner.js:createScheduleSession() produces for two reruns of the same schedule — confirmed failing on both assertions before the fix, per rehab-plan.md A3+A4. Refs #128 (Part 2 of the cleanup plan) --- .ai/contexts/subagent-observability.md | 29 +++++ public/sidebar.js | 109 ++++++++++------- test/dom-slug-group-subagent-nesting.test.js | 122 +++++++++++++++++++ 3 files changed, 214 insertions(+), 46 deletions(-) create mode 100644 test/dom-slug-group-subagent-nesting.test.js diff --git a/.ai/contexts/subagent-observability.md b/.ai/contexts/subagent-observability.md index 653e3e43..c9aa66d5 100644 --- a/.ai/contexts/subagent-observability.md +++ b/.ai/contexts/subagent-observability.md @@ -167,6 +167,35 @@ an agent tracked from its first line. The renderer's 60 s TTL has the same shape. Anything that needs true liveness would have to come from the parent process, not from mtime. +## Subagent children inside a slug group (issue #128 ask 4, rehab-plan.md A3) + +- **The bug**: `buildSlugGroup()` used to append its sessions via the raw + `buildSessionItem(session)`, never through `appendSubagentChildren()` — only + the ungrouped/top-level render path (`buildSessionsList`) called that + helper. Any session rendered inside a slug group (i.e. any second-or-later + rerun of a schedule sharing a slug — the only real producer, see + `schedule-runner.js:createScheduleSession()`) silently lost its subagent + caret/children. +- **The fix**: `appendSubagentChildren()` was hoisted from a closure inside + `renderProjects()` to module scope (it never captured any of that + function's locals) so `buildSlugGroup(slug, sessions, subagentIndex)` can + call it directly for each session it renders, exactly like + `buildSessionsList` does for ungrouped sessions. +- **The knock-on bug this caused**: a slug-group `
` carries no + `dataset.sessionId` of its own, so the "orphan subagents" pass in + `buildSessionsList` — which built `allTopLevelIds` from + `item.element.dataset.sessionId` — never counted the sessions grouped + inside it as accounted-for. Their subagents were treated as parentless and + duplicated into the project's "Orphan subagents" bucket even after the fix + above attached them correctly inside the group. `collectTopLevelSessionIds(el)` + fixes this by walking into `el` for nested `[data-session-id]` session-items + (excluding subagent ones) when `el` itself isn't a session item. +- **Coverage**: `test/dom-slug-group-subagent-nesting.test.js` seeds two + schedule-rerun-shaped sessions sharing a slug plus a subagent parented to + one of them, and pins both the caret-attachment fix and the + no-duplicate-orphan fix (failed on both before the fix, confirmed by + reverting it locally during development). + ## If you change this, also check - `eslint.config.js` `rendererCrossFileGlobals` — must list any new renderer-global functions (e.g. `showSubagentTranscript`, `drainViewerWatches`) or lint fails on `no-undef` diff --git a/public/sidebar.js b/public/sidebar.js index 1af1c0ab..b849c1ce 100644 --- a/public/sidebar.js +++ b/public/sidebar.js @@ -226,7 +226,56 @@ function buildSubagentItem(session) { return item; } -function buildSlugGroup(slug, sessions) { +// Shared by buildSessionsList and buildSlugGroup — see .ai/contexts/subagent-observability.md +function appendSubagentChildren(parentEl, parentSessionId, subagentIndex) { + const children = subagentIndex && subagentIndex.get(parentSessionId); + if (!children || children.length === 0) return; + + const expandedSet = getExpandedSubagents(); + const caretId = caretIdFor(parentSessionId); + const isExpanded = expandedSet.has(parentSessionId); + + const caret = document.createElement('div'); + caret.className = 'sidebar-children-caret js-stateful'; + caret.id = caretId; + if (isExpanded) caret.classList.add('expanded'); + if (parentHasActiveSubagent(parentSessionId)) caret.classList.add('has-running-child'); + caret.innerHTML = ` ${children.length} subagent${children.length !== 1 ? 's' : ''}`; + + const childrenContainer = document.createElement('div'); + childrenContainer.className = 'sidebar-subagents-container js-stateful'; + childrenContainer.id = 'subc-' + parentSessionId.replace(/[^a-zA-Z0-9_-]/g, '_'); + childrenContainer.style.display = isExpanded ? '' : 'none'; + + for (const child of children) { + childrenContainer.appendChild(buildSubagentItem(child)); + } + + caret.addEventListener('click', (e) => { + e.stopPropagation(); + const open = childrenContainer.style.display !== 'none'; + childrenContainer.style.display = open ? 'none' : ''; + caret.classList.toggle('expanded', !open); + const set = getExpandedSubagents(); + if (open) { set.delete(parentSessionId); } else { set.add(parentSessionId); } + saveExpandedSubagents(set); + }); + + parentEl.after(caret); + caret.after(childrenContainer); +} + +// See .ai/contexts/subagent-observability.md (slug-group orphan-detection fix) +function collectTopLevelSessionIds(el) { + const ids = []; + if (el.dataset && el.dataset.sessionId && !el.dataset.subagent) ids.push(el.dataset.sessionId); + el.querySelectorAll('[data-session-id]').forEach((child) => { + if (!child.dataset.subagent) ids.push(child.dataset.sessionId); + }); + return ids; +} + +function buildSlugGroup(slug, sessions, subagentIndex) { const group = document.createElement('div'); const id = slugId(slug); const expanded = getExpandedSlugs().has(id); @@ -290,7 +339,9 @@ function buildSlugGroup(slug, sessions) { if (promoted.length > 0) { group.classList.add('has-promoted'); for (const session of promoted) { - sessionsContainer.appendChild(buildSessionItem(session)); + const sessionEl = buildSessionItem(session); + sessionsContainer.appendChild(sessionEl); + appendSubagentChildren(sessionEl, session.sessionId, subagentIndex); } if (rest.length > 0) { const moreBtn = document.createElement('div'); @@ -302,7 +353,9 @@ function buildSlugGroup(slug, sessions) { olderDiv.className = 'slug-group-older js-stateful'; olderDiv.id = 'sgo-' + id; for (const session of rest) { - olderDiv.appendChild(buildSessionItem(session)); + const sessionEl = buildSessionItem(session); + olderDiv.appendChild(sessionEl); + appendSubagentChildren(sessionEl, session.sessionId, subagentIndex); } sessionsContainer.appendChild(moreBtn); @@ -310,7 +363,9 @@ function buildSlugGroup(slug, sessions) { } } else { for (const session of sessions) { - sessionsContainer.appendChild(buildSessionItem(session)); + const sessionEl = buildSessionItem(session); + sessionsContainer.appendChild(sessionEl); + appendSubagentChildren(sessionEl, session.sessionId, subagentIndex); } } @@ -416,7 +471,7 @@ function renderProjects(projects, resort) { const mostRecentTime = Math.max(...sessions.map(s => new Date(s.modified).getTime())); const hasRunning = sessions.some(s => activePtyIds.has(s.sessionId) || pendingSessions.has(s.sessionId)); const hasPinned = sessions.some(s => s.starred); - const element = sessions.length === 1 ? buildSessionItem(sessions[0]) : buildSlugGroup(slug, sessions); + const element = sessions.length === 1 ? buildSessionItem(sessions[0]) : buildSlugGroup(slug, sessions, subagentIndex); allItems.push({ sortTime: mostRecentTime, pinned: hasPinned, running: hasRunning, element }); } @@ -466,45 +521,6 @@ function renderProjects(projects, resort) { }; } - // Append subagent children beneath a session item element. - function appendSubagentChildren(parentEl, parentSessionId, subagentIndex) { - const children = subagentIndex && subagentIndex.get(parentSessionId); - if (!children || children.length === 0) return; - - const expandedSet = getExpandedSubagents(); - const caretId = caretIdFor(parentSessionId); - const isExpanded = expandedSet.has(parentSessionId); - - const caret = document.createElement('div'); - caret.className = 'sidebar-children-caret js-stateful'; - caret.id = caretId; - if (isExpanded) caret.classList.add('expanded'); - if (parentHasActiveSubagent(parentSessionId)) caret.classList.add('has-running-child'); - caret.innerHTML = ` ${children.length} subagent${children.length !== 1 ? 's' : ''}`; - - const childrenContainer = document.createElement('div'); - childrenContainer.className = 'sidebar-subagents-container js-stateful'; - childrenContainer.id = 'subc-' + parentSessionId.replace(/[^a-zA-Z0-9_-]/g, '_'); - childrenContainer.style.display = isExpanded ? '' : 'none'; - - for (const child of children) { - childrenContainer.appendChild(buildSubagentItem(child)); - } - - caret.addEventListener('click', (e) => { - e.stopPropagation(); - const open = childrenContainer.style.display !== 'none'; - childrenContainer.style.display = open ? 'none' : ''; - caret.classList.toggle('expanded', !open); - const set = getExpandedSubagents(); - if (open) { set.delete(parentSessionId); } else { set.add(parentSessionId); } - saveExpandedSubagents(set); - }); - - parentEl.after(caret); - caret.after(childrenContainer); - } - // Build the sessions list DOM (shared between projects and worktrees) function buildSessionsList(fId, visible, older, subagentIndex, projectPath) { const sessionsList = document.createElement('div'); @@ -534,9 +550,10 @@ function renderProjects(projects, resort) { sessionsList.appendChild(olderList); } - // Orphan subagents: children whose parentSessionId has no top-level session in this project + // Orphan subagents: children whose parentSessionId has no top-level session in this project. + // See .ai/contexts/subagent-observability.md for why collectTopLevelSessionIds is needed here. if (subagentIndex) { - const allTopLevelIds = new Set([...visible, ...older].map(i => i.element.dataset && i.element.dataset.sessionId).filter(Boolean)); + const allTopLevelIds = new Set([...visible, ...older].flatMap(i => collectTopLevelSessionIds(i.element))); const orphans = []; for (const [parentId, kids] of subagentIndex) { if (!allTopLevelIds.has(parentId)) { diff --git a/test/dom-slug-group-subagent-nesting.test.js b/test/dom-slug-group-subagent-nesting.test.js new file mode 100644 index 00000000..9356d161 --- /dev/null +++ b/test/dom-slug-group-subagent-nesting.test.js @@ -0,0 +1,122 @@ +// Regression coverage for issue #128 (ask 4) / rehab-plan.md A3+A4 — subagent +// children rendered inside a slug group used to vanish from the sidebar. +// +// buildSlugGroup() appended its sessions via the raw buildSessionItem(session), +// never through appendSubagentChildren — only the top-level (non-grouped) +// render path called that helper. Any session rendered inside a slug group +// therefore lost its subagent caret/children, and — because the group element +// itself carries no data-session-id — the orphan-subagents pass in +// buildSessionsList() didn't recognize the grouped session as "already +// accounted for" either, so its subagents were duplicated into the project's +// "Orphan subagents" bucket instead. +// +// The fixture below reproduces the one real producer of shared slugs across +// top-level sessions in this app: schedule-runner.js's createScheduleSession() +// writes `slug: schedule.slug` on the JSONL's first line for every rerun of the +// same schedule (schedule-runner.js:176). Two reruns of the same schedule share +// a slug and land in session_cache (session-cache.js:buildProjectsFromCache) +// with identical `slug`, distinct `sessionId`/`modified`, and no +// `parentSessionId` — the shape asserted here. + +const test = require('node:test'); +const assert = require('node:assert/strict'); + +const { setupSidebarDom } = require('./dom-setup'); + +function scheduleRerunProject(overrides = {}) { + const baseTime = Date.parse('2026-08-20T09:00:00Z'); + const t = (offsetMs) => new Date(baseTime + offsetMs).toISOString(); + // summary as read-session-file.js derives it for a createScheduleSession() + // JSONL: the first user message is `'Scheduled Task: ' + schedule.prompt`, + // which doesn't match the tag pattern, so it + // falls through to text.slice(0, 120). + const summary = 'Scheduled Task: Check Hacker News for the current top article and any posts related to AI docume'; + + return { + projectPath: '/home/dev/hn-watcher', + sessions: [ + { + sessionId: 'sched-run-1', + slug: 'hn-top-articles', + summary, + firstPrompt: summary, + modified: t(-120000), // an earlier rerun + messageCount: 1, + starred: false, + archived: 0, + }, + { + sessionId: 'sched-run-2', + slug: 'hn-top-articles', + summary, + firstPrompt: summary, + modified: t(0), // the current (running) rerun + messageCount: 1, + starred: false, + archived: 0, + }, + { + sessionId: 'sub:sched-run-2:agent-1', + parentSessionId: 'sched-run-2', + agentId: 'agent-1', + subagentType: 'explore', + description: 'explore subagent', + modified: t(-1000), + messageCount: 1, + }, + ], + ...overrides, + }; +} + +test('a session rendered inside a slug group still gets its subagent children attached', () => { + const ctx = setupSidebarDom(); + try { + ctx.window.activePtyIds.add('sched-run-2'); + ctx.sidebar.renderProjects([scheduleRerunProject()], true); + + const group = ctx.document.getElementById('slug-hn-top-articles'); + assert.ok(group, 'the two schedule reruns must be grouped by shared slug'); + assert.ok(group.classList.contains('slug-group'), 'grouped element must carry slug-group'); + + const groupedSessionItems = group.querySelectorAll('.session-item:not([data-subagent])'); + assert.equal(groupedSessionItems.length, 2, 'both reruns render as session-items inside the group'); + + const caret = ctx.document.getElementById('sub-caret-sched-run-2'); + assert.ok(caret, 'subagent caret for the grouped, running rerun must exist'); + assert.ok(group.contains(caret), 'the caret must be nested inside the slug group, next to its parent session-item'); + + const subagentItem = ctx.document.getElementById('si-sub:sched-run-2:agent-1'); + assert.ok(subagentItem, 'the subagent item must be rendered'); + assert.ok(group.contains(subagentItem), 'the subagent item must be nested inside the slug group, not dropped to the orphan bucket'); + } finally { + ctx.destroy(); + } +}); + +test('the grouped session\'s subagent is not duplicated into the project\'s orphan-subagents bucket', () => { + const ctx = setupSidebarDom(); + try { + ctx.window.activePtyIds.add('sched-run-2'); + ctx.sidebar.renderProjects([scheduleRerunProject()], true); + + const orphanGroup = ctx.document.querySelector('.sidebar-orphan-subagents'); + assert.ok(!orphanGroup, 'a subagent whose parent is a grouped session is not an orphan — no orphan bucket should render'); + } finally { + ctx.destroy(); + } +}); + +test('the slug-group dot lights up while one of the grouped, nested sessions has an active PTY', () => { + const ctx = setupSidebarDom(); + try { + ctx.window.activePtyIds.add('sched-run-2'); + ctx.sidebar.renderProjects([scheduleRerunProject()], true); + + const dot = ctx.document.querySelector('#slug-hn-top-articles .slug-group-dot'); + assert.ok(dot, 'slug-group-dot must be rendered'); + assert.ok(dot.classList.contains('running'), 'group dot must reflect that sched-run-2 has an active PTY'); + } finally { + ctx.destroy(); + } +}); From 4e80273a0ace5fdb536dca55adbed1eb1e9bbb9d Mon Sep 17 00:00:00 2001 From: jean-baptiste Date: Fri, 21 Aug 2026 16:02:06 +0200 Subject: [PATCH 2/3] fix(sidebar): exclude nested subagent items from slug-group archive-all MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review finding F1 on PR #134 (reviewer-r1-deliverable.md): nesting the subagent's caret/children as DOM siblings inside a slug group means group.querySelectorAll('.session-item') in the "Archive all sessions in group" handler now also matches the nested subagent item (its className includes session-item for shared styling), so archiveSession/stopSession were reached with a subagent id. Apply the same :not([data-subagent]) guard already used elsewhere in this file. Adds a 4th test to dom-slug-group-subagent-nesting.test.js pinning the fix (archive-all on a group with an attached subagent only reaches the two top-level ids) — confirmed failing without the guard, restored. --- .ai/contexts/subagent-observability.md | 11 ++++++ public/sidebar.js | 2 +- test/dom-slug-group-subagent-nesting.test.js | 35 ++++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/.ai/contexts/subagent-observability.md b/.ai/contexts/subagent-observability.md index c9aa66d5..a27cdf63 100644 --- a/.ai/contexts/subagent-observability.md +++ b/.ai/contexts/subagent-observability.md @@ -195,6 +195,17 @@ process, not from mtime. one of them, and pins both the caret-attachment fix and the no-duplicate-orphan fix (failed on both before the fix, confirmed by reverting it locally during development). +- **A second knock-on bug (PR #134 review F1)**: nesting the subagent's + caret/children as DOM siblings inside the group means every DOM query + scoped to `.slug-group` that matches on `.session-item` alone now also + matches the nested subagent item (`buildSubagentItem` includes + `session-item` in its className for shared styling). The "Archive all + sessions in group" handler (`rebindSidebarEvents`, `.slug-group-archive-btn`) + had exactly this query and, unguarded, called `archiveSession`/`stopSession` + on the subagent's id. Fixed with the same `:not([data-subagent])` guard + already used elsewhere in this file (e.g. the per-item click wiring). Any + *new* query scoped to a slug-group's subtree must apply this guard too — + it's not automatic. ## If you change this, also check diff --git a/public/sidebar.js b/public/sidebar.js index b849c1ce..5ebcf744 100644 --- a/public/sidebar.js +++ b/public/sidebar.js @@ -909,7 +909,7 @@ function rebindSidebarEvents(projects) { archiveBtn.onclick = async (e) => { e.stopPropagation(); const group = header.parentElement; - const sessionItems = group.querySelectorAll('.session-item'); + const sessionItems = group.querySelectorAll('.session-item:not([data-subagent])'); for (const item of sessionItems) { const sid = item.dataset.sessionId; const session = sessionMap.get(sid); diff --git a/test/dom-slug-group-subagent-nesting.test.js b/test/dom-slug-group-subagent-nesting.test.js index 9356d161..9f5aa5db 100644 --- a/test/dom-slug-group-subagent-nesting.test.js +++ b/test/dom-slug-group-subagent-nesting.test.js @@ -120,3 +120,38 @@ test('the slug-group dot lights up while one of the grouped, nested sessions has ctx.destroy(); } }); + +// PR #134 review F1 — nesting the subagent's caret/children as DOM siblings +// inside the group (the fix above) means group.querySelectorAll('.session-item') +// in the "Archive all sessions in group" handler now also matches the nested +// subagent item (buildSubagentItem's className includes 'session-item' for +// shared styling), so the handler must exclude it the same way every other +// subagent-item consumer in this file does (:not([data-subagent])). +test('"Archive all sessions in group" only archives the two top-level reruns, not the nested subagent', async () => { + const ctx = setupSidebarDom(); + try { + ctx.window.activePtyIds.add('sched-run-2'); + ctx.window.sessionMap.set('sched-run-1', { sessionId: 'sched-run-1', archived: 0 }); + ctx.window.sessionMap.set('sched-run-2', { sessionId: 'sched-run-2', archived: 0 }); + ctx.window.sessionMap.set('sub:sched-run-2:agent-1', { sessionId: 'sub:sched-run-2:agent-1', archived: 0 }); + + const archiveCalls = []; + const stopCalls = []; + ctx.window.api.archiveSession = (sid, val) => { archiveCalls.push([sid, val]); return Promise.resolve({ ok: true }); }; + ctx.window.api.stopSession = (sid) => { stopCalls.push(sid); return Promise.resolve({ ok: true }); }; + + ctx.sidebar.renderProjects([scheduleRerunProject()], true); + + const archiveBtn = ctx.document.querySelector('#slug-hn-top-articles .slug-group-archive-btn'); + assert.ok(archiveBtn, 'slug-group-archive-btn must be rendered'); + await archiveBtn.onclick({ stopPropagation: () => {} }); + + const archivedIds = archiveCalls.map(([sid]) => sid).sort(); + assert.deepEqual(archivedIds, ['sched-run-1', 'sched-run-2'], + 'only the two top-level reruns must be archived — the nested subagent must not reach archiveSession'); + assert.ok(!stopCalls.includes('sub:sched-run-2:agent-1'), + 'the nested subagent must not reach stopSession either'); + } finally { + ctx.destroy(); + } +}); From b714f67bb2015033f366d2a5ee1f721f5fc4da29 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Date: Sun, 23 Aug 2026 22:39:12 +0200 Subject: [PATCH 3/3] test(sidebar): cover the rest and no-promoted branches of slug-group subagent nesting Adversarial review of #134 found appendSubagentChildren called from three sites in buildSlugGroup (promoted / rest-under-more / no-active-session), with only the promoted branch actually exercised by a test with a subagent child. Add the two missing fixtures; mutation-tested by removing each call in turn (restored afterward). --- test/dom-slug-group-subagent-nesting.test.js | 57 ++++++++++++++++++-- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/test/dom-slug-group-subagent-nesting.test.js b/test/dom-slug-group-subagent-nesting.test.js index 9f5aa5db..8e8db37d 100644 --- a/test/dom-slug-group-subagent-nesting.test.js +++ b/test/dom-slug-group-subagent-nesting.test.js @@ -23,7 +23,7 @@ const assert = require('node:assert/strict'); const { setupSidebarDom } = require('./dom-setup'); -function scheduleRerunProject(overrides = {}) { +function scheduleRerunProject({ subagentParentId = 'sched-run-2', ...overrides } = {}) { const baseTime = Date.parse('2026-08-20T09:00:00Z'); const t = (offsetMs) => new Date(baseTime + offsetMs).toISOString(); // summary as read-session-file.js derives it for a createScheduleSession() @@ -56,8 +56,8 @@ function scheduleRerunProject(overrides = {}) { archived: 0, }, { - sessionId: 'sub:sched-run-2:agent-1', - parentSessionId: 'sched-run-2', + sessionId: `sub:${subagentParentId}:agent-1`, + parentSessionId: subagentParentId, agentId: 'agent-1', subagentType: 'explore', description: 'explore subagent', @@ -155,3 +155,54 @@ test('"Archive all sessions in group" only archives the two top-level reruns, no ctx.destroy(); } }); + +// Adversarial review of #134 found buildSlugGroup() calling appendSubagentChildren +// from three separate loops (promoted / rest-under-"more" / all-sessions-else) and +// only the first was exercised — see .ai/contexts/subagent-observability.md. +test('a session in the "+N more" (non-promoted) part of a slug group still gets its subagent children attached', () => { + const ctx = setupSidebarDom(); + try { + ctx.window.activePtyIds.add('sched-run-2'); // only sched-run-2 is promoted; sched-run-1 lands in "rest" + ctx.sidebar.renderProjects([scheduleRerunProject({ subagentParentId: 'sched-run-1' })], true); + + const group = ctx.document.getElementById('slug-hn-top-articles'); + assert.ok(group, 'the two schedule reruns must be grouped by shared slug'); + assert.ok(group.classList.contains('has-promoted'), 'sched-run-2 being active must promote it, pushing sched-run-1 into rest'); + + const olderDiv = ctx.document.getElementById('sgo-slug-hn-top-articles'); + assert.ok(olderDiv, 'the "+N more" container must be rendered since rest.length > 0'); + + const caret = ctx.document.getElementById('sub-caret-sched-run-1'); + assert.ok(caret, 'subagent caret for the non-promoted rerun must exist'); + assert.ok(olderDiv.contains(caret), 'the caret must be nested inside the "+N more" container, next to its parent session-item'); + + const subagentItem = ctx.document.getElementById('si-sub:sched-run-1:agent-1'); + assert.ok(subagentItem, 'the subagent item must be rendered'); + assert.ok(olderDiv.contains(subagentItem), 'the subagent item must be nested inside the "+N more" container, not dropped to the orphan bucket'); + } finally { + ctx.destroy(); + } +}); + +test('a session in a slug group with no active PTY at all still gets its subagent children attached', () => { + const ctx = setupSidebarDom(); + try { + // No session in this group has an active PTY, so buildSlugGroup() takes + // the plain "all sessions" branch rather than the promoted/rest split. + ctx.sidebar.renderProjects([scheduleRerunProject({ subagentParentId: 'sched-run-2' })], true); + + const group = ctx.document.getElementById('slug-hn-top-articles'); + assert.ok(group, 'the two schedule reruns must be grouped by shared slug'); + assert.ok(!group.classList.contains('has-promoted'), 'no session is active, so the group must not be in the promoted state'); + + const caret = ctx.document.getElementById('sub-caret-sched-run-2'); + assert.ok(caret, 'subagent caret must exist even when no session in the group is running'); + assert.ok(group.contains(caret), 'the caret must be nested inside the slug group, next to its parent session-item'); + + const subagentItem = ctx.document.getElementById('si-sub:sched-run-2:agent-1'); + assert.ok(subagentItem, 'the subagent item must be rendered'); + assert.ok(group.contains(subagentItem), 'the subagent item must be nested inside the slug group, not dropped to the orphan bucket'); + } finally { + ctx.destroy(); + } +});