diff --git a/README.md b/README.md index 76429aea..2318bb71 100644 --- a/README.md +++ b/README.md @@ -220,7 +220,7 @@ If an AI assistant is helping with install, reinstall, bootstrap, provider setup - Session actions via `⋯` dropdown per session — pin, move to project, archive, duplicate, delete - Pin/star sessions to the top of the sidebar (gold indicator) - Archive sessions (hide without deleting, toggle to show) -- Session projects -- named groups with colors for organizing sessions +- Session projects -- named groups with colors for organizing sessions; delegated subagent sessions have no project of their own and follow their nearest ancestor's project in the project filter and the Unassigned chip - Session tags -- add #tag to titles for colored chips and click-to-filter - Grouped by Today / Yesterday / Earlier in the sidebar (collapsible date groups) - Download as Markdown transcript, full JSON export, or import from JSON diff --git a/static/sessions.js b/static/sessions.js index 565d0a9a..1aa949c0 100644 --- a/static/sessions.js +++ b/static/sessions.js @@ -7622,6 +7622,11 @@ function _sidebarRowsById(rows){ return byId; } +// True when any row resolves to no project, using the filter's own ancestor map. +function _sidebarHasUnprojectedRows(rows, rowsById){ + return rows.some(s=>!_sidebarProjectIdForRow(s, rowsById)); +} + function _partitionSidebarSessionRows(allMatched, activeSidForSidebar){ const rowsById=_sidebarRowsById([allMatched, typeof _sidebarReferenceSessions!=='undefined'?_sidebarReferenceSessions:null]); let cliSessionCount=0; @@ -7670,6 +7675,7 @@ function _partitionSidebarSessionRows(allMatched, activeSidForSidebar){ cliReferenceRaw, webuiSessionsRaw, cliSessionsRaw, + rowsById, }; } @@ -7797,6 +7803,7 @@ function renderSessionListFromCache(){ cliReferenceRaw, webuiSessionsRaw, cliSessionsRaw, + rowsById, }=_partitionSidebarSessionRows(allMatched, activeSidForSidebar); const referenceRaw=_sessionSourceFilter==='cli'?cliReferenceRaw:webuiReferenceRaw; const isCliView=_sessionSourceFilter==='cli'; @@ -7871,8 +7878,7 @@ function renderSessionListFromCache(){ } // Project filter bar — show when there are real projects OR there are // unassigned sessions (so the Unassigned chip has something to filter to). - const profileRowsById=_sidebarRowsById([profileFiltered]); - const hasUnprojected=profileFiltered.some(s=>!_sidebarProjectIdForRow(s, profileRowsById)); + const hasUnprojected=_sidebarHasUnprojectedRows(profileFiltered, rowsById); if(_allProjects.length>0||hasUnprojected){ const bar=document.createElement('div'); bar.className='project-bar'; diff --git a/tests/test_sidebar_unassigned_filter.py b/tests/test_sidebar_unassigned_filter.py index 33e6895c..96c7894b 100644 --- a/tests/test_sidebar_unassigned_filter.py +++ b/tests/test_sidebar_unassigned_filter.py @@ -76,7 +76,7 @@ def test_unassigned_chip_only_shown_when_relevant(): — though that's vanishingly rare). """ js = _js() - assert "const hasUnprojected=profileFiltered.some(s=>!_sidebarProjectIdForRow(s, profileRowsById));" in js, ( + assert "const hasUnprojected=_sidebarHasUnprojectedRows(profileFiltered, rowsById);" in js, ( "The render function must compute whether unassigned sessions exist" ) assert "if(_allProjects.length>0||hasUnprojected){" in js, ( diff --git a/tests/test_subagent_project_filter.py b/tests/test_subagent_project_filter.py index b7ab1f8e..57d95308 100644 --- a/tests/test_subagent_project_filter.py +++ b/tests/test_subagent_project_filter.py @@ -44,7 +44,7 @@ global._sessionSourceFilter = 'webui'; for (const fn of ['_isSessionLocallyStreaming','_hasPendingUserMessageSignal','_isSessionEffectivelyStreaming', '_isChildSession','_isForkWithResolvableParent','_sessionLineageKey','_sidebarLineageKeyForRow', '_collapseSessionLineageForSidebar','_attachChildSessionsToSidebarRows','_sessionAttentionState', - '_sidebarRowHasVisibleMessages','_sidebarProjectIdForRow','_sidebarRowsById', + '_sidebarRowHasVisibleMessages','_sidebarProjectIdForRow','_sidebarRowsById','_sidebarHasUnprojectedRows', '_partitionSidebarSessionRows','_scopedSidebarReferenceRows','_renderSidebarRowsFromRawSessions']) { eval.call(global, extractFunc(fn)); } @@ -115,4 +115,18 @@ console.log(JSON.stringify({raw: rows.raw, scoped})); def test_unassigned_chip_ignores_subagents_of_project_parents(): - assert "const hasUnprojected=profileFiltered.some(s=>!_sidebarProjectIdForRow(s, profileRowsById));" in SESSIONS_JS + out = _run("console.log(JSON.stringify(_sidebarHasUnprojectedRows([parent, child], _sidebarRowsById([[parent, child]]))));") + assert out is False + + +def test_unassigned_chip_uses_reference_parent(): + """A child whose project parent is only a reference row is not unassigned.""" + out = _run(""" +global._sidebarReferenceSessions = [parent]; +global._activeProject = null; +const part = _partitionSidebarSessionRows([child], null); +console.log(JSON.stringify({has: _sidebarHasUnprojectedRows(part.profileFiltered, part.rowsById), + unassigned: render(NO_PROJECT_FILTER, [child]).raw})); +""") + assert out["unassigned"] == [] + assert out["has"] is False