团队功能在 0.1.7 上整条失效:team-routes.ts 当初镜像的是 0.1.6 的 Remote 词汇
(remoteView / remoteCreateTask / remoteUpdateTask),而 0.1.7 把这三个方法删掉了,
读路径改到 Lead Session 的 `agentTeam` Session projection。真机 3080 复现:
teams.view 对带团队的会话返回 400 "membership.svc.remoteView is not a function",
于是 TeamBoard 从不渲染、节点富化与任务窗连带失效。
- 读:删掉 teams.view 与 5s 轮询,改读 SessionListState.projectionsBySession[root]
.values.agentTeam(与 subagentCatalog 同源,推送式);新增 client/team-projection.ts
做纯适配器,成员运行状态由 subagents.live 的 running 叠加、Lead 回退 session list
(官方 README 的 "overlays activity from Session status")。projection 缺席 =
这里没有团队,与旧的 {available:false}/{team:null} 合并成同一静默降级。
- 写:taskCreate/taskUpdate 改调 0.1.7 的 createTask/updateTask(返回已提交的 view,
拒绝改为抛 TeamError),TEAM_TASK_STALE_REVISION → 409 team-conflict,其余 → 400
team-error(wire 新增 team-conflict)。api.ts 的 writeTask 把失败信封还原成
{ok:false,code,message} —— 原先窗口永远走不到该分支(readEnvelope 是抛)。
- 任务窗:外壳换成上一轮的 FloatingWindow(可拖拽/可拉伸/只在关闭或 Esc 时消失),
动作行下沉到 shell 的 footer,长描述不再把动作推出视野。
- 真机抓到的两个单测抓不到的问题:
1. 队列中任务的「完成」被宿主状态机拒绝(pending → claim → in_progress →
complete → completed → reopen)→ 动作行跟随状态,pending 出「认领」;
2. 认领后 Tag 变「阻塞」—— ready 的语义只是「pending 且无未完成 blocker」,
对所有非 pending 任务都是 false → 抽出唯一规则 taskBlocked/taskStatusLabel/
taskTone/taskDotState,四处展示点全部改走它。
- i18n:新增 teamTaskClaim / taskWindowCreate / taskWindowDetail /
tasksStateProvisioning 词条 ×20 词典。
验证:typecheck / lint 通过;pnpm test 1276 passed / 9 skipped;
pnpm test:mount 7/7(真实 DSH 0.1.7-rc.2 挂载 + tab 全扫无崩溃)。
真机 3080(0.1.7-rc.1 + Agent Teams 实验层):看板从 projection 渲染、
队列任务「认领」→「完成」全链路写通(负责人 lead / 进行中 / 已完成)、
外部点击不关窗、Escape 关窗,截图见 docs/plans/2026-09-27-agent-teams-dsh-0.1.7-adaptation.md。
(cherry picked from commit dbc524e0b350c5d3564843f4776cd15d797506d8)
123 lines
5.6 KiB
TypeScript
123 lines
5.6 KiB
TypeScript
/**
|
|
* The plugin's view of DSH 0.1.7's `agentTeam` Session projection — the Lead
|
|
* Session's team board, read straight off the client snapshot.
|
|
*
|
|
* 0.1.6 exposed the board through the experimental service's Remote vocabulary
|
|
* (`agentTeams.remoteView`, reached over the wire) and the page had to poll it
|
|
* every 5s. 0.1.7 deleted that trio and publishes the same state as a Session
|
|
* projection instead: `SessionListState.projectionsBySession[leadId].values
|
|
* .agentTeam`, loaded once per connection like every other projection. This
|
|
* module is therefore a pure adapter — no request, no poller, no observer —
|
|
* exactly like ./subagent-catalog.ts for the catalog.
|
|
*
|
|
* Two facts the durable projection does not carry are DERIVED, not invented:
|
|
*
|
|
* - TURN ACTIVITY. A projection row is `{ id, name, role, phase, error? }`
|
|
* where `phase` is the durable lifecycle (`provisioning | active | failed`,
|
|
* the Lead row always `active`). Whether a member is running right now comes
|
|
* from the session's own status — upstream's own web UI "reads the shared
|
|
* Session projections and overlays activity from Session status" — and this
|
|
* plugin already has that overlay: `subagents.live` reports EVERY tree child
|
|
* with an explicit `running` flag (see ./subagent-catalog.ts's `childLive`),
|
|
* with the session list's own coarse `running` as the fallback for the Lead.
|
|
* - `diagnostics`, the 0.1.6 view's array of provisioning notes, collapses to
|
|
* the projection's single durable `error` string.
|
|
*
|
|
* What is genuinely gone is the member's `model` (and `description` /
|
|
* `provider` / `context`): the wire view never carried them, so the node
|
|
* detail no longer has a model row to show. See the design note
|
|
* docs/plans/2026-09-27-agent-teams-dsh-0.1.7-adaptation.md.
|
|
*/
|
|
import type {
|
|
SidebarChildLiveView,
|
|
SidebarSessionList,
|
|
SidebarSessionSummary,
|
|
SidebarTeamMemberProjection,
|
|
SidebarTeamProjection,
|
|
} from '../context-types.ts'
|
|
|
|
/**
|
|
* One roster row as this page consumes it: the projection's durable half plus
|
|
* the derived runtime status. A THIRD type next to the host service's
|
|
* `SidebarTeamMemberView` on purpose — that one is the 0.1.6 `listMembers`
|
|
* shape (status/description/provider/model) this plugin no longer reads, and
|
|
* conflating the two is exactly how the field drift that broke the board went
|
|
* unnoticed.
|
|
*/
|
|
export interface TeamMemberRow {
|
|
/** The member's session id (the teammate's child session under the lead). */
|
|
id: string
|
|
name: string
|
|
role: 'lead' | 'teammate'
|
|
/** Durable lifecycle, as the projection publishes it. */
|
|
phase: SidebarTeamMemberProjection['phase']
|
|
/** Derived display status: durable phase wins, else the live activity flag. */
|
|
status: 'running' | 'idle' | 'provisioning' | 'failed'
|
|
/** The durable provisioning error as a one-item list (see the header). */
|
|
diagnostics: string[]
|
|
}
|
|
|
|
/**
|
|
* The team board led by `rootId`, or undefined when that session leads none.
|
|
*
|
|
* ABSENT MEANS NO TEAM, and there are three ways to get there: the deployment
|
|
* never mounted the experimental layer (no projection key at all), the root is
|
|
* a teammate rather than the Lead, or the session simply has no team. All
|
|
* three collapse into "the page draws no Teams block" — the same silent
|
|
* degradation the 0.1.6 route expressed as `{available:false}` (layer absent)
|
|
* and `{team:null}` (root leads none), with one lookup instead of two states
|
|
* the page had to distinguish and never did.
|
|
*
|
|
* @param projections - `SessionListState.projectionsBySession`.
|
|
* @param rootId - the tree's root session (the only session that can be Lead).
|
|
*/
|
|
export function teamProjectionOf(
|
|
projections: SidebarSessionList['projectionsBySession'],
|
|
rootId: string | undefined,
|
|
): SidebarTeamProjection | undefined {
|
|
if (rootId === undefined) return undefined
|
|
return projections?.[rootId]?.values.agentTeam
|
|
}
|
|
|
|
/**
|
|
* Overlay runtime activity onto the durable roster.
|
|
*
|
|
* Status precedence is deliberately narrow: the durable phase wins for the two
|
|
* states it owns (`failed` never becomes "running" because a retry is in
|
|
* flight, `provisioning` is not yet a turn), and only an `active` member asks
|
|
* the live channel whether it is running right now. A member the channel does
|
|
* not mention yet (the first poll of a freshly rooted tree) reads as `idle`,
|
|
* which is the safe direction: it never claims work that is not happening.
|
|
*
|
|
* @param team - the Lead's projection ({@link teamProjectionOf}).
|
|
* @param live - the `subagents.live` map (`session id → live view`).
|
|
* @param byId - the session list by id (the Lead's own running flag lives here).
|
|
*/
|
|
export function teamMembersOf(
|
|
team: SidebarTeamProjection | undefined,
|
|
live: Readonly<Record<string, SidebarChildLiveView | undefined>>,
|
|
byId: Readonly<Record<string, SidebarSessionSummary | undefined>>,
|
|
): TeamMemberRow[] {
|
|
if (team === undefined) return []
|
|
return team.members.map(member => ({
|
|
id: member.id,
|
|
name: member.name,
|
|
role: member.role,
|
|
phase: member.phase,
|
|
status: memberStatus(member, live, byId),
|
|
diagnostics: member.error === undefined ? [] : [member.error],
|
|
}))
|
|
}
|
|
|
|
/** One member's runtime status (see {@link teamMembersOf} for the rule). */
|
|
function memberStatus(
|
|
member: SidebarTeamMemberProjection,
|
|
live: Readonly<Record<string, SidebarChildLiveView | undefined>>,
|
|
byId: Readonly<Record<string, SidebarSessionSummary | undefined>>,
|
|
): TeamMemberRow['status'] {
|
|
if (member.phase === 'failed') return 'failed'
|
|
if (member.phase === 'provisioning') return 'provisioning'
|
|
const running = live[member.id]?.running ?? byId[member.id]?.running
|
|
return running === true ? 'running' : 'idle'
|
|
}
|