feat(client): unified tasks view model + graph layout + api methods

- tasks-model.ts: one pre-order node list for graph AND tree modes —
  catalog walking, workflow-run attachment with member re-parenting and
  synthesis, team enrichment, per-parent settled-leaf fold aggregates.
- tasks-graph-layout.ts: dependency-free tidy-tree layered layout
  (bundle-purity: no graph library in the core bundle).
- api.ts: workflowsList/teamsView/teamsTaskCreate/teamsTaskUpdate with
  the wire result types mirrored from the host routes.
- 11 unit tests across model derivation, edge classification, fold, and
  layout geometry.
This commit is contained in:
Menghuan1918
2026-09-27 22:18:14 +08:00
parent c445495f8b
commit fdc4520080
5 changed files with 741 additions and 1 deletions
+48 -1
View File
@@ -9,7 +9,16 @@
import { encodeHtmlUrl } from '../html-route.ts'
import type { LastActivity } from '../subagent-activity.ts'
import type { SidechatLiveEvent, SidechatLogEvent, SidechatThreadInfo } from '../sidechat-core.ts'
import type { SidebarJobView, SidebarSessionEvent } from '../context-types.ts'
import type {
SidebarCreateTeamTaskRequest,
SidebarJobView,
SidebarSessionEvent,
SidebarTeamMemberView,
SidebarTeamTaskMutationResult,
SidebarTeamTaskView,
SidebarUpdateTeamTaskRequest,
} from '../context-types.ts'
import type { WorkflowRunView } from '../workflow-runs.ts'
/** One wire failure. */
export class SidebarApiError extends Error {
@@ -113,6 +122,23 @@ export interface JobOutputResult {
/** The `subagents.live` response: running child id → latest activity. */
export type SubagentLiveResult = { live: Record<string, LastActivity> }
/** The `workflows.list` response: the tree's folded workflow runs. */
export type WorkflowsListResult = { runs: WorkflowRunView[] }
/** The `teams.view` response (structural degradation contract). */
export type TeamsViewResult =
| { available: false }
| { available: true; team: { members: SidebarTeamMemberView[]; tasks: SidebarTeamTaskView[] } | null }
/** The `teams.taskCreate` request payload (minus the rootSessionId). */
export type TeamsTaskCreateRequest = SidebarCreateTeamTaskRequest
/** The `teams.taskUpdate` request payload (minus the rootSessionId). */
export type TeamsTaskUpdateRequest = SidebarUpdateTeamTaskRequest
/** The task-mutation result union (CAS conflict stays distinct). */
export type TeamsTaskMutationResult = SidebarTeamTaskMutationResult
/**
* Parse one `/sidebar` JSON response envelope into its value. A non-ok
* status, an unparseable body, or any shape other than `{ok: true, value}`
@@ -353,6 +379,27 @@ export const api = {
*/
subagentsLive: (rootSessionId: string, signal?: AbortSignal) =>
call<SubagentLiveResult>('subagents.live', { rootSessionId }, signal),
/**
* The workflow runs of the whole tree, folded host-side from the
* `tool-workflow/*` session events (the same four types the official
* workflow-run panel folds). Empty when the tree never ran a workflow —
* absence is the normal case, never an error.
*/
workflowsList: (rootSessionId: string, signal?: AbortSignal) =>
call<WorkflowsListResult>('workflows.list', { rootSessionId }, signal),
/**
* The team led by the tree's root agent (experimental Agent Teams layer):
* `{available:false}` hides the whole block; `{team:null}` means the root
* leads no team. Poll-driven (the official panel is manual-refresh too).
*/
teamsView: (rootSessionId: string, signal?: AbortSignal) =>
call<TeamsViewResult>('teams.view', { rootSessionId }, signal),
/** Create one shared task on the root-led team. */
teamsTaskCreate: (rootSessionId: string, req: TeamsTaskCreateRequest) =>
call<TeamsTaskMutationResult>('teams.taskCreate', { rootSessionId, ...req }),
/** CAS-mutate one shared task; conflicts ride the returned union. */
teamsTaskUpdate: (rootSessionId: string, req: TeamsTaskUpdateRequest) =>
call<TeamsTaskMutationResult>('teams.taskUpdate', { rootSessionId, ...req }),
/** Create a Side Chat thread: a child session seeded with the parent's
* full log up to now. Empty question = immediate create (Codex-style):
* the thread opens empty, the first prompt carries the boundary. */