feat(tasks-ui): tasks on their owner nodes, host-primitive controls, icon glyphs, draggable output
Round-two review feedback, all three points: - TASKS ON NODES: the model maps each shared task (ownerName -> member name -> member session id) onto its owner's node; graph cards and tree rows render a fourth line (icon + subject + status + '+N'), and the node detail popover lists every owned task. - TEAM BOARD: member Pills double as the owner filter; each task row is a status dot + subject + owner + status Tag + ONE overflow Menu (complete / reopen / edit / reassign submenu / delete with a two-step arm). Creating and editing moved into a real Modal with labeled Inputs and owner Pills — the narrow strip is no longer where you type. - HOST PRIMITIVES ONLY: the board and the jobs drawer now use Menu / Modal / Input / Button / Pill / Tag / Switch / StateDot; no native select or input remains (guarded by a spec). - ICONS, NOT HAND-DRAWN GLYPHS: every text glyph (circle/diamond/triangle/ check/chevrons/i/house) is replaced by a host icon — agent preset, user (teammate), branch (workflow run), checklist (fold + tasks), tree corner, fullscreen (fit), ellipsis (node details), copy/stop (jobs). The live line now shows the tool's own icon + name + args without the drawn box. - JOBS UX: the output popover is DRAGGABLE (offset clamped to the viewport, double click re-anchors, controls do not start a drag), defaults to 380px, and gains copy-output, a follow-latest switch, kind/status Tags and a drag hint; the drawer highlights the open row. - 19 new copy keys across zh/en/ja and the 18 third-language dictionaries. Tests: 4 new specs (task rendered on its node, no native select/input + menu-driven board, create-through-dialog posts the CAS-free create, job popover drag moves the card). Full suite 1404 passed / 133 files.
This commit is contained in:
@@ -25,6 +25,7 @@ import type {
|
||||
SidebarSubagentCatalog,
|
||||
SidebarSubagentChildEntry,
|
||||
SidebarTeamMemberView,
|
||||
SidebarTeamTaskView,
|
||||
} from '../context-types.ts'
|
||||
import type { LastActivity } from '../subagent-activity.ts'
|
||||
import type { WorkflowRunView } from '../workflow-runs.ts'
|
||||
@@ -33,6 +34,15 @@ import { isSideThreadSummary } from './subagent-detect.ts'
|
||||
/** Display state of one agent node (drives the dot + fold candidacy). */
|
||||
export type TasksNodeState = 'running' | 'idle' | 'done' | 'error'
|
||||
|
||||
/** One shared task as a node shows it (the board owns the full editing). */
|
||||
export interface TasksNodeTask {
|
||||
id: string
|
||||
subject: string
|
||||
status: 'pending' | 'in_progress' | 'completed'
|
||||
/** Not ready = blocked by another task. */
|
||||
ready: boolean
|
||||
}
|
||||
|
||||
/** One agent node (root, subagent, teammate, or synthesized workflow member). */
|
||||
export interface TasksAgentNode {
|
||||
kind: 'agent'
|
||||
@@ -60,6 +70,8 @@ export interface TasksAgentNode {
|
||||
}
|
||||
/** The catalog's durable children signal (fold candidacy's leaf test). */
|
||||
hasChildren?: boolean
|
||||
/** Shared tasks owned by this agent (team boards only; empty otherwise). */
|
||||
tasks?: TasksNodeTask[]
|
||||
/** Synthesized from a workflow run's member row (no catalog entry). */
|
||||
synthesized?: boolean
|
||||
/** Jump target of the row (absent on the root node). */
|
||||
@@ -97,6 +109,8 @@ export interface TasksModelInput {
|
||||
live: Readonly<Record<string, LastActivity | undefined>>
|
||||
runs: readonly WorkflowRunView[]
|
||||
teamMembers: readonly SidebarTeamMemberView[]
|
||||
/** The team's shared tasks; each lands on its OWNER's node. */
|
||||
teamTasks?: readonly SidebarTeamTaskView[]
|
||||
/** Whether settled leaves collapse into fold nodes. */
|
||||
folded: boolean
|
||||
}
|
||||
@@ -138,7 +152,23 @@ function memberOutcomeState(outcome: 'completed' | 'failed' | 'cancelled' | unde
|
||||
*/
|
||||
export function buildTasksModel(input: TasksModelInput): TasksNode[] {
|
||||
const { byId, catalogs, rootId, currentSessionId, live, runs, teamMembers, folded } = input
|
||||
const teamTasks = input.teamTasks ?? []
|
||||
const teamById = new Map(teamMembers.map(member => [member.id, member]))
|
||||
/** Owner display name → node id (the board assigns by member name). */
|
||||
const nodeByOwner = new Map<string, string>()
|
||||
for (const member of teamMembers) nodeByOwner.set(member.name, member.id)
|
||||
const tasksByNode = new Map<string, TasksNodeTask[]>()
|
||||
for (const task of teamTasks) {
|
||||
if (task.status === 'deleted' || task.ownerName === undefined) continue
|
||||
const nodeId = nodeByOwner.get(task.ownerName)
|
||||
if (nodeId === undefined) continue
|
||||
const list = tasksByNode.get(nodeId)
|
||||
const entry: TasksNodeTask = {
|
||||
id: task.id, subject: task.subject, status: task.status, ready: task.ready,
|
||||
}
|
||||
if (list === undefined) tasksByNode.set(nodeId, [entry])
|
||||
else list.push(entry)
|
||||
}
|
||||
const runsByOrigin = new Map<string, WorkflowRunView[]>()
|
||||
for (const run of runs) {
|
||||
const list = runsByOrigin.get(run.originSessionId)
|
||||
@@ -183,6 +213,7 @@ export function buildTasksModel(input: TasksModelInput): TasksNode[] {
|
||||
current: entry.id === currentSessionId,
|
||||
...(live[entry.id] !== undefined ? { live: live[entry.id] } : {}),
|
||||
...(team !== undefined ? { team } : {}),
|
||||
...(tasksByNode.get(entry.id) !== undefined ? { tasks: tasksByNode.get(entry.id) } : {}),
|
||||
hasChildren: entry.hasChildren,
|
||||
childAddress: { parentSessionId: parentId, childSessionId: entry.id, mode: entry.mode },
|
||||
})
|
||||
@@ -203,6 +234,7 @@ export function buildTasksModel(input: TasksModelInput): TasksNode[] {
|
||||
: undefined
|
||||
if (existing !== undefined) {
|
||||
existing.parentId = runNode.id
|
||||
if (tasksByNode.get(existing.id) !== undefined) existing.tasks = tasksByNode.get(existing.id)
|
||||
members.push(existing)
|
||||
} else {
|
||||
const childKnown = member.childId !== ''
|
||||
@@ -215,6 +247,8 @@ export function buildTasksModel(input: TasksModelInput): TasksNode[] {
|
||||
activity: member.outcome === undefined ? 'running' : 'inactive',
|
||||
current: member.childId !== '' && member.childId === currentSessionId,
|
||||
synthesized: true,
|
||||
...(childKnown && tasksByNode.get(member.childId) !== undefined
|
||||
? { tasks: tasksByNode.get(member.childId) } : {}),
|
||||
...(childKnown
|
||||
? { childAddress: { parentSessionId: parentId, childSessionId: member.childId, mode: 'one-shot' as const } }
|
||||
: {}),
|
||||
@@ -298,6 +332,7 @@ export function buildTasksModel(input: TasksModelInput): TasksNode[] {
|
||||
activity: rootSummary?.running === true ? 'running' : 'inactive',
|
||||
current: rootId === currentSessionId,
|
||||
...(rootTeam !== undefined ? { team: rootTeam } : {}),
|
||||
...(tasksByNode.get(rootId) !== undefined ? { tasks: tasksByNode.get(rootId) } : {}),
|
||||
})
|
||||
appendChildren(rootId)
|
||||
return out
|
||||
|
||||
Reference in New Issue
Block a user