fix(web): deduplicate subagent navigation

This commit is contained in:
Dudu-0223
2026-07-30 11:34:57 +08:00
committed by Tianyi Cui
parent 16ffd63115
commit f0ab04273d
53 changed files with 294 additions and 95 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/client/ui-workspace/README.md
README.md: 2854c678fd93d56267d0aec8515455b77d4bcbf1
README.zh.md: 51db6cacfe83d176a6cc68b01fe3dd91acf49e25
README.md: 2670bdfa2fb1a223bf0c0ea65fbacc1cfb30c607
README.zh.md: 1e68cfb1a94c240c32949059ca6a3c6adc25208b

View File

@@ -14,6 +14,8 @@ The Session row's Fork action forks at the source's last completed turn, increme
Both target slots are declared by other plugins, so `apply` registers through declaration-aware deferral and re-registers after a declaring slot is restored.
The shared sidebar projection hides rows whose durable Session summary has `origin: 'subagent'`; users enter those conversations through the selected parent's subagent header catalog. Ordinary forks remain visible because lineage alone does not set that origin. The runtime keeps hidden rows available for conversation, title, and addressed transport state.
## Model Experience
None, as the picker is browser chrome; nothing here reaches a model request.

View File

@@ -14,6 +14,8 @@ Session 行内的 Fork 操作在源会话最后一个已完成轮次处 fork,
两个目标 slot 都由其他插件声明,因此 `apply` 通过声明感知的延迟机制完成注册,并在声明该 slot 的插件恢复后重新注册。
共享侧边栏投影会隐藏持久化 Session 摘要中带有 `origin: 'subagent'` 的行;用户从所选 parent 的 subagent 页头目录进入这些对话。普通 fork 仍然可见,因为仅有谱系不会设置该 origin。运行时仍保留隐藏行,供对话、标题与已寻址传输状态使用。
## 模型体验
无。选择器属于浏览器 chrome;这里没有任何内容进入模型请求。

View File

@@ -92,11 +92,14 @@ function byRecency(a: SessionSummary, b: SessionSummary): number {
/**
* Ordinary sessions are visible; among blank sessions, only the current one
* is visible; archived sessions are visible nowhere (their accounting slots
* remain, so unarchiving restores position).
* is visible. Subagent children use their parent header catalog; archived
* sessions are visible nowhere, while their accounting slots remain so
* unarchiving restores position.
*/
function sessionVisible(session: SessionSummary, current: SessionId | undefined, archived: ReadonlySet<SessionId>): boolean {
return !archived.has(session.id) && (!session.blank || session.id === current)
return session.origin !== 'subagent'
&& !archived.has(session.id)
&& (!session.blank || session.id === current)
}
/**

View File

@@ -69,6 +69,22 @@ describe('deriveGroups', () => {
expect(strayGroups.map(group => group.key)).toEqual(['first'])
})
it('hides subagent-origin sessions without hiding ordinary forks', () => {
const parent = summary('parent', 1)
const fork = { ...summary('fork', 2), parentId: parent.id }
const subagent = { ...summary('subagent', 3), parentId: parent.id, origin: 'subagent' as const }
const sessions = { ...list(parent, fork, subagent), current: subagent.id }
const groups = deriveGroups(
sessions,
[workspace('first', ['parent', 'fork', 'subagent'])],
noArchive,
view(['first']),
)
expect(groups[0]!.sessions.map(node => node.id)).toEqual([parent.id, fork.id])
expect(groups[0]!.sessionCount).toBe(2)
})
it('ignores fork lineage and sorts every ungrouped session as a top-level row', () => {
const parent = summary('parent', 1)
const oldChild = { ...summary('old-child', 10), parentId: parent.id }
@@ -143,6 +159,17 @@ describe('deriveFlat', () => {
expect(rows.map(row => row.id)).toEqual([sid('child'), sid('tie-a'), sid('tie-b'), sid('parent')])
})
it('hides subagent-origin rows but keeps ordinary forks', () => {
const parent = summary('parent', 1)
const fork = { ...summary('fork', 2), parentId: parent.id }
const subagent = { ...summary('subagent', 3), parentId: parent.id, origin: 'subagent' as const }
const rows = deriveFlat(
{ ...list(parent, fork, subagent), current: subagent.id },
noArchive,
)
expect(rows.map(row => row.id)).toEqual([fork.id, parent.id])
})
it('tolerates ids whose summary has not landed yet', () => {
const partial: SessionListState = { ...list(summary('present', 1)), ids: [sid('ghost'), sid('present')] }
expect(deriveFlat(partial, noArchive).map(row => row.id)).toEqual([sid('present')])