feat(tasks): 适配 DSH 0.1.7 的 Agent Teams(读 projection + 写路由对接真实服务)
团队功能在 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)
This commit is contained in:
+28
-48
@@ -9,13 +9,14 @@
|
||||
* - WORKFLOW RUNS: the host folds `tool-workflow/*` session events of the
|
||||
* whole tree (workflows.list); a run hangs under its origin agent with its
|
||||
* member agents re-parented below it and phase frames behind them;
|
||||
* - AGENT TEAMS (experimental host layer): when the root leads a team, the
|
||||
* roster enriches matching nodes and a header chip opens the shared task
|
||||
* board popover (CAS operations); without the layer the UI hides itself
|
||||
* entirely;
|
||||
* - AGENT TEAMS (experimental host layer): when the root leads a team, its
|
||||
* `agentTeam` Session projection feeds the always-visible team strip and
|
||||
* enriches the matching nodes; the projection is absent without the layer,
|
||||
* so the whole surface hides itself with no banner and no polling;
|
||||
* - BACKGROUND JOBS: a bottom drawer replaces the old in-page section and
|
||||
* auto-collapses once the tree has many agents; job output opens as an
|
||||
* anchored popover (event replay — never the model's cursor).
|
||||
* auto-collapses once the tree has many agents; job output opens as a
|
||||
* persistent floating window (the host's own client jobs service — never
|
||||
* the model's cursor).
|
||||
*
|
||||
* Node click jumps straight into the transcript (root → main session); the
|
||||
* ⓘ button opens the detail popover. Completed leaf agents fold into one
|
||||
@@ -46,9 +47,10 @@ import {
|
||||
rootAncestor,
|
||||
} from './subagent-detect.ts'
|
||||
import { subagentCatalogs } from './subagent-catalog.ts'
|
||||
import { teamMembersOf, teamProjectionOf } from './team-projection.ts'
|
||||
import { treeSessionIds } from './subagent-lineage.ts'
|
||||
import { orderJobs, type TreeJob } from './subagent-jobs.ts'
|
||||
import { api, type TeamsViewResult } from './api.ts'
|
||||
import { api } from './api.ts'
|
||||
import { usePolling } from './use-polling.ts'
|
||||
import { useNarrowViewport } from './breakpoints.ts'
|
||||
import {
|
||||
@@ -62,7 +64,7 @@ import { JobsDrawer, JobOutputWindow } from './JobsDrawer.tsx'
|
||||
import { AnchoredPopover } from './AnchoredPopover.tsx'
|
||||
import { AgentNodePopover, WorkflowNodePopover } from './TasksPopovers.tsx'
|
||||
import { TeamBoard } from './TeamBoard.tsx'
|
||||
import { TaskPopover } from './TaskWindow.tsx'
|
||||
import { TaskWindow } from './TaskWindow.tsx'
|
||||
import type { SidebarStore } from './state.ts'
|
||||
import type { WorkflowRunView } from '../workflow-runs.ts'
|
||||
import legacy from './SubagentView.module.css'
|
||||
@@ -133,31 +135,6 @@ function useWorkflowRuns(rootId: string | undefined, active: boolean): readonly
|
||||
return runs
|
||||
}
|
||||
|
||||
/** The team view of the tree's root (poll-driven, structurally degraded). */
|
||||
function useTeamView(
|
||||
rootId: string | undefined,
|
||||
active: boolean,
|
||||
): { view: TeamsViewResult | undefined; refresh(): void } {
|
||||
const [view, setView] = useState<TeamsViewResult | undefined>(undefined)
|
||||
const [epoch, setEpoch] = useState(0)
|
||||
useEffect(() => { setView(undefined) }, [rootId])
|
||||
const poll = useCallback(async (signal: AbortSignal): Promise<void> => {
|
||||
if (rootId === undefined) return
|
||||
const result = await api.teamsView(rootId, signal)
|
||||
if (!signal.aborted) setView(result)
|
||||
// `epoch` is the manual-refresh trigger: bumping it changes the task
|
||||
// identity, which restarts the poller with an immediate tick.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [rootId, epoch])
|
||||
usePolling(rootId !== undefined && active, poll, {
|
||||
intervalMs: TELEMETRY_POLL_MS,
|
||||
mode: 'self-scheduling',
|
||||
immediate: true,
|
||||
})
|
||||
const refresh = useCallback((): void => { setEpoch(current => current + 1) }, [])
|
||||
return { view, refresh }
|
||||
}
|
||||
|
||||
/**
|
||||
* The tree's background jobs, read through the HOST's client jobs service
|
||||
* (`ctx.jobs` — see jobs-client.ts). Watching is per tree session (the host
|
||||
@@ -233,11 +210,16 @@ export function SubagentView(props: {
|
||||
const rootSummary = rootId === undefined ? undefined : byId[rootId]
|
||||
const live = useSubagentLive(rootId, active)
|
||||
const runs = useWorkflowRuns(rootId, active)
|
||||
const team = useTeamView(rootId, active)
|
||||
const teamView = team.view
|
||||
// The root-led team, straight off the SAME snapshot the catalogs come from
|
||||
// (see team-projection.ts): undefined = no team here (no experimental layer,
|
||||
// or the root is not a Lead), which is exactly the "hide the strip" state.
|
||||
const team = useMemo(
|
||||
() => teamProjectionOf(list.projectionsBySession, rootId),
|
||||
[list.projectionsBySession, rootId],
|
||||
)
|
||||
const teamMembers = useMemo(
|
||||
() => (teamView?.available === true ? teamView.team?.members ?? [] : []),
|
||||
[teamView],
|
||||
() => teamMembersOf(team, live, byId),
|
||||
[team, live, byId],
|
||||
)
|
||||
|
||||
// The default view mode comes from the side card prefs (settings select);
|
||||
@@ -277,11 +259,11 @@ export function SubagentView(props: {
|
||||
live,
|
||||
runs,
|
||||
teamMembers,
|
||||
teamTasks: teamView?.available === true ? teamView.team?.tasks ?? [] : [],
|
||||
teamTasks: team?.tasks ?? [],
|
||||
folded,
|
||||
foldedIds,
|
||||
})),
|
||||
[byId, catalogs, rootId, sessionId, live, runs, teamMembers, teamView, folded, foldedIds],
|
||||
[byId, catalogs, rootId, sessionId, live, runs, teamMembers, team, folded, foldedIds],
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -439,19 +421,18 @@ export function SubagentView(props: {
|
||||
const popoverContent = ((): ReactNode => {
|
||||
if (popover === null) return null
|
||||
if (popover.kind === 'task') {
|
||||
if (teamView?.available !== true || teamView.team === null || rootId === undefined) return null
|
||||
if (team === undefined || rootId === undefined) return null
|
||||
const task = popover.taskId === undefined
|
||||
? undefined
|
||||
: teamView.team.tasks.find(candidate => candidate.id === popover.taskId)
|
||||
: team.tasks.find(candidate => candidate.id === popover.taskId)
|
||||
// A task that vanished (deleted elsewhere) closes the window instead of
|
||||
// showing a stale card.
|
||||
if (popover.taskId !== undefined && task === undefined) return null
|
||||
return (
|
||||
<TaskPopover
|
||||
<TaskWindow
|
||||
rootId={rootId}
|
||||
task={task}
|
||||
members={teamView.team.members}
|
||||
onChanged={team.refresh}
|
||||
members={teamMembers}
|
||||
onClose={closePopover}
|
||||
anchor={popover.anchor}
|
||||
/>
|
||||
@@ -529,15 +510,14 @@ export function SubagentView(props: {
|
||||
disabled={rootId === undefined}
|
||||
onClick={() => {
|
||||
if (rootId !== undefined) refresh(rootId)
|
||||
team.refresh()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{rootId !== undefined && teamView?.available === true && teamView.team !== null && (
|
||||
{rootId !== undefined && team !== undefined && (
|
||||
<TeamBoard
|
||||
rootId={rootId}
|
||||
members={teamView.team.members}
|
||||
tasks={teamView.team.tasks}
|
||||
members={teamMembers}
|
||||
tasks={team.tasks}
|
||||
onOpenTask={openTaskFromBoard}
|
||||
collapsed={teamBoardCollapsed}
|
||||
onToggleCollapsed={() => { setTeamBoardCollapsed(current => !current) }}
|
||||
|
||||
+185
-166
@@ -4,41 +4,40 @@
|
||||
* popover) opens THIS component — pointer targets stay thin, the window owns
|
||||
* the behaviour.
|
||||
*
|
||||
* Shape: a non-fullscreen, DRAGGABLE card (never a modal dialog). Details
|
||||
* render as multi-line MARKDOWN by default; 编辑 switches the same card into
|
||||
* multi-line editing, so reading and writing never jump between surfaces.
|
||||
* Shape: the plugin's shared {@link FloatingWindow} — a draggable, resizable,
|
||||
* body-portaled card with a title bar and a fixed action row, NOT a popover.
|
||||
* A task edit is a sustained interaction (a long markdown description), so it
|
||||
* gets the same lifecycle a watched job output gets: it survives clicking
|
||||
* elsewhere and only the close button or Escape ends it. Details render as
|
||||
* multi-line MARKDOWN; 编辑 switches the same window into multi-line editing,
|
||||
* and the footer keeps its actions while the body scrolls.
|
||||
* Actions: owner reassignment (Pills, CAS-immediate), edit/save, reopen or
|
||||
* complete, and a two-step delete.
|
||||
*
|
||||
* Host primitives everywhere (Button / Input / Pill / Tag / StateDot) plus
|
||||
* the plugin's own `MultilineField` (the host set ships no multi-line input)
|
||||
* and the shared `MarkdownText` renderer with the plugin's copy labels.
|
||||
* Host primitives everywhere (Button / Input / Pill / Tag / StateDot) plus the
|
||||
* plugin's own `MultilineField` (the host set ships no multi-line input) and
|
||||
* the shared `MarkdownText` renderer with the plugin's copy labels.
|
||||
*/
|
||||
import { useEffect, useId, useRef, useState, type ReactNode } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import {
|
||||
Button, IconCheckOutlineRegular, IconCloseOutlineRegular, IconEditOutlineRegular,
|
||||
Button, IconCheckOutlineRegular, IconEditOutlineRegular,
|
||||
IconRefreshOutlineRegular, IconTrashOutlineRegular, Input, MarkdownText, Pill, StateDot, Tag,
|
||||
} from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { SidebarTeamMemberView, SidebarTeamTaskView } from '../context-types.ts'
|
||||
import type { SidebarTeamTaskView } from '../context-types.ts'
|
||||
import { api } from './api.ts'
|
||||
import { markdownTextProps } from './markdown-labels.tsx'
|
||||
import { AnchoredPopover } from './AnchoredPopover.tsx'
|
||||
import { t, type CopyKey } from './locales.ts'
|
||||
import { FloatingWindow } from './FloatingWindow.tsx'
|
||||
import { taskBlocked, taskDotState, taskStatusLabel, taskTone } from './tasks-shared.tsx'
|
||||
import type { TeamMemberRow } from './team-projection.ts'
|
||||
import { t } from './locales.ts'
|
||||
import css from './tasks-graph.module.css'
|
||||
|
||||
/** One mutation outcome (the host route's own union). */
|
||||
type MutationResult = { ok: true } | { ok: false; error: { code: string; message: string } }
|
||||
/** The wire code of a stale-revision write (see src/team-routes.ts). */
|
||||
const CONFLICT_CODE = 'team-conflict'
|
||||
|
||||
/** The task status label key. */
|
||||
function taskStatusKey(status: SidebarTeamTaskView['status']): CopyKey {
|
||||
switch (status) {
|
||||
case 'pending': return 'teamTaskPending'
|
||||
case 'in_progress': return 'teamTaskInProgress'
|
||||
case 'completed': return 'teamTaskCompleted'
|
||||
case 'deleted': return 'teamTaskDeleted'
|
||||
}
|
||||
}
|
||||
/** One mutation outcome: success, or a rejection the window must report. */
|
||||
type MutationOutcome = { ok: true } | { ok: false; code?: string; message: string }
|
||||
|
||||
/**
|
||||
* The plugin's own multi-line input: the host primitive set ships no
|
||||
@@ -59,7 +58,7 @@ export function MultilineField(props: {
|
||||
rows={props.rows ?? 7}
|
||||
placeholder={props.placeholder}
|
||||
aria-label={props.label}
|
||||
data-popover-no-drag
|
||||
data-window-no-drag
|
||||
onChange={(event) => { props.onChange?.(event.target.value) }}
|
||||
/>
|
||||
)
|
||||
@@ -67,7 +66,7 @@ export function MultilineField(props: {
|
||||
|
||||
/** The owner picker (Pills; CAS-immediate, so it is never a draft field). */
|
||||
function OwnerPicker(props: {
|
||||
members: readonly SidebarTeamMemberView[]
|
||||
members: readonly TeamMemberRow[]
|
||||
owner: string | undefined
|
||||
disabled: boolean
|
||||
onPick(owner: string): void
|
||||
@@ -97,10 +96,14 @@ function OwnerPicker(props: {
|
||||
)
|
||||
}
|
||||
|
||||
/** The read-only body: markdown description, meta, owner picker. */
|
||||
/**
|
||||
* The read-only body: the owner line and the markdown description. The status
|
||||
* and the owner NAME live in the window's own head row (see `TaskWindow`); this
|
||||
* body carries the editable owner picker beside the prose it belongs to.
|
||||
*/
|
||||
function TaskViewBody(props: {
|
||||
task: SidebarTeamTaskView
|
||||
teammates: readonly SidebarTeamMemberView[]
|
||||
teammates: readonly TeamMemberRow[]
|
||||
busy: boolean
|
||||
onReassign(owner: string): void
|
||||
}): ReactNode {
|
||||
@@ -108,16 +111,6 @@ function TaskViewBody(props: {
|
||||
const body = task.description.trim()
|
||||
return (
|
||||
<>
|
||||
<div className={css.jobPopTitle}>
|
||||
<StateDot
|
||||
size={6}
|
||||
state={task.status === 'completed' ? 'done' : task.ready ? 'ongoing' : 'warning'}
|
||||
/>
|
||||
<span className={css.popTitle} title={task.subject}>{task.subject}</span>
|
||||
<Tag tone={task.status === 'completed' ? 'success' : task.ready ? 'info' : 'warning'}>
|
||||
{t(task.ready ? taskStatusKey(task.status) : 'teamTaskBlocked')}
|
||||
</Tag>
|
||||
</div>
|
||||
<div className={css.jobPopMeta}>
|
||||
<span className={css.popKey}>{t('teamTaskOwner')}</span>
|
||||
<span className={css.jobPopValue}>{task.ownerName ?? t('teamTaskUnowned')}</span>
|
||||
@@ -175,7 +168,7 @@ function TaskEditBody(props: {
|
||||
}): ReactNode {
|
||||
const subjectHintId = useId()
|
||||
return (
|
||||
<div className={css.taskForm} data-popover-no-drag>
|
||||
<div className={css.taskForm} data-window-no-drag>
|
||||
<label className={css.taskField}>
|
||||
<span className={css.taskLabel}>{t('teamTaskSubject')}</span>
|
||||
<Input
|
||||
@@ -210,16 +203,20 @@ export interface TaskWindowProps {
|
||||
rootId: string
|
||||
/** The task under view/edit; undefined = create mode. */
|
||||
task: SidebarTeamTaskView | undefined
|
||||
members: readonly SidebarTeamMemberView[]
|
||||
/** Re-pull `teams.view` after a mutation (the parent owns the poller). */
|
||||
onChanged(): void
|
||||
members: readonly TeamMemberRow[]
|
||||
/** Where the window first appears (the row that opened it). */
|
||||
anchor?: HTMLElement | null
|
||||
/** Close the window (the caller's popover state). */
|
||||
onClose(): void
|
||||
}
|
||||
|
||||
/** The task window content (the caller supplies the popover shell). */
|
||||
/**
|
||||
* The ONE task window: the plugin's floating shell wrapping the view/edit body
|
||||
* and the action row, so reading, writing and the mutation state all stay in
|
||||
* one component.
|
||||
*/
|
||||
export function TaskWindow(props: TaskWindowProps): ReactNode {
|
||||
const { rootId, task, members, onChanged, onClose } = props
|
||||
const { rootId, task, members, onClose } = props
|
||||
const creating = task === undefined
|
||||
const [editing, setEditing] = useState(creating)
|
||||
const [subject, setSubject] = useState(task?.subject ?? '')
|
||||
@@ -230,7 +227,7 @@ export function TaskWindow(props: TaskWindowProps): ReactNode {
|
||||
const [note, setNote] = useState<string | undefined>(undefined)
|
||||
const [armedDelete, setArmedDelete] = useState(false)
|
||||
|
||||
// Re-seed the draft when the caller swaps the subject task: the poller
|
||||
// Re-seed the draft when the caller swaps the subject task: the projection
|
||||
// hands us fresh revisions, and unsaved text must not leak across tasks.
|
||||
// Keyed by id on purpose — a same-id revision bump must NOT clobber the
|
||||
// reader's in-flight edit (the seed rides a render-time ref so the effect
|
||||
@@ -257,7 +254,7 @@ export function TaskWindow(props: TaskWindowProps): ReactNode {
|
||||
|
||||
/** Run one CAS mutation with the shared busy/conflict handling. */
|
||||
const mutate = async (
|
||||
action: () => Promise<MutationResult>,
|
||||
action: () => Promise<MutationOutcome>,
|
||||
options: { close?: boolean } = {},
|
||||
): Promise<void> => {
|
||||
if (busy) return
|
||||
@@ -265,16 +262,21 @@ export function TaskWindow(props: TaskWindowProps): ReactNode {
|
||||
setNote(undefined)
|
||||
try {
|
||||
const result = await action()
|
||||
if (!result.ok) {
|
||||
setNote(result.error.code === 'team-task-conflict'
|
||||
? t('teamTaskConflict')
|
||||
: t('teamTaskError', { message: result.error.message }))
|
||||
} else {
|
||||
if (result.ok) {
|
||||
if (options.close === true) onClose()
|
||||
else setEditing(false)
|
||||
// No refresh call: the board is read off the Lead Session's
|
||||
// projection, so a committed revision lands in this window's props
|
||||
// by itself.
|
||||
} else {
|
||||
setNote(result.code === CONFLICT_CODE
|
||||
? t('teamTaskConflict')
|
||||
: t('teamTaskError', { message: result.message }))
|
||||
}
|
||||
onChanged()
|
||||
} catch (error) {
|
||||
// `writeTask` turns every WIRE failure into a result above; this arm is
|
||||
// for a programming error or a caller bug, which must not turn into an
|
||||
// unhandled rejection inside a click handler.
|
||||
setNote(t('teamTaskError', { message: error instanceof Error ? error.message : String(error) }))
|
||||
} finally {
|
||||
setBusy(false)
|
||||
@@ -308,19 +310,140 @@ export function TaskWindow(props: TaskWindowProps): ReactNode {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={css.popCard} data-popover-handle>
|
||||
<div className={css.popHead}>
|
||||
<span>{creating ? t('teamTaskCreate') : editing ? t('teamTaskEdit') : t('teamTaskDetail')}</span>
|
||||
<span className={css.popHeadActions} data-popover-no-drag>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
icon={<IconCloseOutlineRegular size={12} />}
|
||||
aria-label={t('teamTaskCancel')}
|
||||
title={t('teamTaskCancel')}
|
||||
onClick={onClose}
|
||||
/>
|
||||
<FloatingWindow
|
||||
title={creating ? t('taskWindowCreate') : t('taskWindowDetail')}
|
||||
onClose={onClose}
|
||||
initialSize={{ width: 460, height: 360 }}
|
||||
minSize={{ width: 320, height: 240 }}
|
||||
anchor={props.anchor}
|
||||
footer={(
|
||||
// The action row is the shell's FOOTER, so it never scrolls away from
|
||||
// the body it acts on. One state owner: `busy` and `armedDelete` are
|
||||
// the mutation state above, not a second copy.
|
||||
<div className={css.jobPopActions}>
|
||||
{editing
|
||||
? (
|
||||
<>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
icon={<IconCheckOutlineRegular size={12} />}
|
||||
disabled={busy || subject.trim() === ''}
|
||||
onClick={save}
|
||||
>
|
||||
{creating ? t('teamTaskCreate') : t('teamTaskSave')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={busy}
|
||||
onClick={() => {
|
||||
if (creating) onClose()
|
||||
else if (task !== undefined) {
|
||||
setSubject(task.subject)
|
||||
setDescription(task.description)
|
||||
setSubjectTouched(false)
|
||||
setEditing(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('teamTaskCancel')}
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
: task === undefined ? null : (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
icon={<IconEditOutlineRegular size={12} />}
|
||||
onClick={() => { setEditing(true) }}
|
||||
>
|
||||
{t('teamTaskEdit')}
|
||||
</Button>
|
||||
{/*
|
||||
The board's state machine, in the window: pending → claim →
|
||||
in_progress → complete → completed → reopen. 0.1.7's service
|
||||
REJECTS `complete` on a queued task ("only an in-progress
|
||||
task can complete"), so the primary action has to follow the
|
||||
transition rather than promise one that cannot happen. One
|
||||
primary per window: it is the affirmative change, so 编辑
|
||||
stays a plain outline and 删除 keeps the danger ink.
|
||||
*/}
|
||||
{task.status === 'completed'
|
||||
? (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
icon={<IconRefreshOutlineRegular size={12} />}
|
||||
disabled={busy}
|
||||
onClick={() => void mutate(() => api.teamsTaskUpdate(rootId, {
|
||||
taskId: task.id, expectedRevision: task.revision, action: 'reopen',
|
||||
}))}
|
||||
>
|
||||
{t('teamTaskReopen')}
|
||||
</Button>
|
||||
)
|
||||
: task.status === 'pending'
|
||||
? (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
icon={<IconCheckOutlineRegular size={12} />}
|
||||
// Claiming is refused while a blocker is open
|
||||
// (`TEAM_TASK_BLOCKED`); the Tag beside the subject
|
||||
// already says why, so the button just says "not yet".
|
||||
disabled={busy || taskBlocked(task)}
|
||||
title={taskBlocked(task) ? t('teamTaskBlocked') : undefined}
|
||||
onClick={() => void mutate(() => api.teamsTaskUpdate(rootId, {
|
||||
taskId: task.id, expectedRevision: task.revision, action: 'claim',
|
||||
}))}
|
||||
>
|
||||
{t('teamTaskClaim')}
|
||||
</Button>
|
||||
)
|
||||
: (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
icon={<IconCheckOutlineRegular size={12} />}
|
||||
disabled={busy}
|
||||
onClick={() => void mutate(() => api.teamsTaskUpdate(rootId, {
|
||||
taskId: task.id, expectedRevision: task.revision, action: 'complete',
|
||||
}))}
|
||||
>
|
||||
{t('teamTaskComplete')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className={clsx(css.taskDanger, armedDelete && css.taskDangerArmed)}
|
||||
icon={<IconTrashOutlineRegular size={12} />}
|
||||
disabled={busy}
|
||||
onClick={() => {
|
||||
if (!armedDelete) { setArmedDelete(true); return }
|
||||
setArmedDelete(false)
|
||||
void mutate(() => api.teamsTaskUpdate(rootId, {
|
||||
taskId: task.id, expectedRevision: task.revision, action: 'delete',
|
||||
}), { close: true })
|
||||
}}
|
||||
>
|
||||
{armedDelete ? t('teamTaskDeleteConfirm') : t('teamTaskDelete')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div className={css.taskHeadRow}>
|
||||
{task !== undefined && <StateDot size={6} state={taskDotState(task)} />}
|
||||
<span className={css.popTitle} title={task?.subject ?? subject}>
|
||||
{task === undefined ? t('taskWindowCreate') : task.subject}
|
||||
</span>
|
||||
{task !== undefined && (
|
||||
<Tag tone={taskTone(task)}>{taskStatusLabel(task)}</Tag>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{editing || task === undefined
|
||||
@@ -343,110 +466,6 @@ export function TaskWindow(props: TaskWindowProps): ReactNode {
|
||||
)}
|
||||
|
||||
{note !== undefined && <div className={css.teamNote}>{note}</div>}
|
||||
|
||||
<div className={css.jobPopActions} data-popover-no-drag>
|
||||
{editing
|
||||
? (
|
||||
<>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
icon={<IconCheckOutlineRegular size={12} />}
|
||||
disabled={busy || subject.trim() === ''}
|
||||
onClick={save}
|
||||
>
|
||||
{creating ? t('teamTaskCreate') : t('teamTaskSave')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={busy}
|
||||
onClick={() => {
|
||||
if (creating) onClose()
|
||||
else if (task !== undefined) {
|
||||
setSubject(task.subject)
|
||||
setDescription(task.description)
|
||||
setSubjectTouched(false)
|
||||
setEditing(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('teamTaskCancel')}
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
: task === undefined ? null : (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
icon={<IconEditOutlineRegular size={12} />}
|
||||
onClick={() => { setEditing(true) }}
|
||||
>
|
||||
{t('teamTaskEdit')}
|
||||
</Button>
|
||||
{/* One primary per card: the affirmative state change (完成 /
|
||||
重新打开) is it, so 编辑 stays a plain outline and 删除 keeps
|
||||
the danger ink beside it. */}
|
||||
{task.status === 'completed'
|
||||
? (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
icon={<IconRefreshOutlineRegular size={12} />}
|
||||
disabled={busy}
|
||||
onClick={() => void mutate(() => api.teamsTaskUpdate(rootId, {
|
||||
taskId: task.id, expectedRevision: task.revision, action: 'reopen',
|
||||
}))}
|
||||
>
|
||||
{t('teamTaskReopen')}
|
||||
</Button>
|
||||
)
|
||||
: (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
icon={<IconCheckOutlineRegular size={12} />}
|
||||
disabled={busy}
|
||||
onClick={() => void mutate(() => api.teamsTaskUpdate(rootId, {
|
||||
taskId: task.id, expectedRevision: task.revision, action: 'complete',
|
||||
}))}
|
||||
>
|
||||
{t('teamTaskComplete')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className={clsx(css.taskDanger, armedDelete && css.taskDangerArmed)}
|
||||
icon={<IconTrashOutlineRegular size={12} />}
|
||||
disabled={busy}
|
||||
onClick={() => {
|
||||
if (!armedDelete) { setArmedDelete(true); return }
|
||||
setArmedDelete(false)
|
||||
void mutate(() => api.teamsTaskUpdate(rootId, {
|
||||
taskId: task.id, expectedRevision: task.revision, action: 'delete',
|
||||
}), { close: true })
|
||||
}}
|
||||
>
|
||||
{armedDelete ? t('teamTaskDeleteConfirm') : t('teamTaskDelete')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The task window inside its draggable popover shell — the single entry point
|
||||
* every task surface uses (board row, node task line, node detail list).
|
||||
*/
|
||||
export function TaskPopover(props: TaskWindowProps & { anchor: HTMLElement | null }): ReactNode {
|
||||
const { anchor, onClose, ...windowProps } = props
|
||||
return (
|
||||
<AnchoredPopover anchor={anchor} onClose={onClose} draggable width={430}>
|
||||
{anchor === null ? null : <TaskWindow {...windowProps} onClose={onClose} />}
|
||||
</AnchoredPopover>
|
||||
</FloatingWindow>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -30,7 +30,8 @@ import { Button, IconRightUpOutlineRegular, StateDot, Tag } from '@deepseek-ai/d
|
||||
import type { SidebarSubagentAddress } from '../context-types.ts'
|
||||
import type { TasksAgentNode, TasksWorkflowNode } from './tasks-model.ts'
|
||||
import {
|
||||
AgentGlyph, WorkflowGlyph, flatten, modeLabel, nodeDotState, taskDotState, taskStatusKey, workflowStatusKey,
|
||||
AgentGlyph, WorkflowGlyph, flatten, modeLabel, nodeDotState, taskDotState,
|
||||
taskStatusLabel, taskTone, workflowStatusKey,
|
||||
} from './tasks-shared.tsx'
|
||||
import { liveActivityLabel } from './process-labels.ts'
|
||||
import { t, type CopyKey } from './locales.ts'
|
||||
@@ -103,9 +104,6 @@ export function AgentNodePopover(props: {
|
||||
{node.team.role === 'lead' ? 'lead' : node.team.name}
|
||||
</PopRow>
|
||||
)}
|
||||
{node.team?.model !== undefined && (
|
||||
<PopRow label={t('tasksNodeModel')} mono>{node.team.model}</PopRow>
|
||||
)}
|
||||
{(liveTool !== undefined || liveText !== undefined) && (
|
||||
<PopRow label={t('tasksNodeActivity')} mono>{liveTool ?? liveText}</PopRow>
|
||||
)}
|
||||
@@ -129,9 +127,7 @@ export function AgentNodePopover(props: {
|
||||
>
|
||||
<StateDot state={taskDotState(task)} size={6} />
|
||||
<span className={css.popListLabel}>{task.subject}</span>
|
||||
<Tag tone={task.status === 'completed' ? 'success' : task.ready ? 'info' : 'warning'}>
|
||||
{t(taskStatusKey(task.status))}
|
||||
</Tag>
|
||||
<Tag tone={taskTone(task)}>{taskStatusLabel(task)}</Tag>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
+21
-33
@@ -23,29 +23,27 @@ import {
|
||||
Button, IconChecklistOutlineRegular, IconCheckOutlineRegular, IconChevronUpOutlineRegular, IconEditOutlineRegular,
|
||||
IconEllipsisOutlineRegular, IconPlusOutlineRegular, IconRefreshOutlineRegular, IconTrashOutlineRegular,
|
||||
IconUserOutlineRegular, Menu, Pill, StateDot, Tag,
|
||||
type MenuEntry, type TagTone,
|
||||
type MenuEntry,
|
||||
} from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { SidebarTeamMemberView, SidebarTeamTaskView } from '../context-types.ts'
|
||||
import { t, type CopyKey } from './locales.ts'
|
||||
import type { SidebarTeamTaskView } from '../context-types.ts'
|
||||
import type { TeamMemberRow } from './team-projection.ts'
|
||||
import { taskDotState, taskStatusLabel, taskTone } from './tasks-shared.tsx'
|
||||
import { t } from './locales.ts'
|
||||
import css from './tasks-graph.module.css'
|
||||
|
||||
/** The task status label key. */
|
||||
function taskStatusKey(status: SidebarTeamTaskView['status']): CopyKey {
|
||||
/**
|
||||
* The status dot of one roster row: the member's derived status (see
|
||||
* ./team-projection.ts — durable phase first, live activity second).
|
||||
*/
|
||||
function memberDotState(status: TeamMemberRow['status']): 'ongoing' | 'idle' | 'error' {
|
||||
switch (status) {
|
||||
case 'pending': return 'teamTaskPending'
|
||||
case 'in_progress': return 'teamTaskInProgress'
|
||||
case 'completed': return 'teamTaskCompleted'
|
||||
case 'deleted': return 'teamTaskDeleted'
|
||||
case 'running': return 'ongoing'
|
||||
case 'provisioning': return 'ongoing'
|
||||
case 'failed': return 'error'
|
||||
case 'idle': return 'idle'
|
||||
}
|
||||
}
|
||||
|
||||
/** The status Tag tone of one task. */
|
||||
function taskTone(task: SidebarTeamTaskView): TagTone {
|
||||
if (task.status === 'completed') return 'success'
|
||||
if (!task.ready) return 'warning'
|
||||
return task.status === 'in_progress' ? 'info' : 'neutral'
|
||||
}
|
||||
|
||||
/**
|
||||
* One task's row menu: the shared task window's own affordance list hoisted
|
||||
* onto the row — complete (or reopen, once completed), edit, reassign the
|
||||
@@ -59,7 +57,7 @@ function taskTone(task: SidebarTeamTaskView): TagTone {
|
||||
*/
|
||||
function taskMenuItems(
|
||||
task: SidebarTeamTaskView,
|
||||
teammates: readonly SidebarTeamMemberView[],
|
||||
teammates: readonly TeamMemberRow[],
|
||||
): MenuEntry[] {
|
||||
const completed = task.status === 'completed'
|
||||
return [
|
||||
@@ -96,7 +94,7 @@ function taskMenuItems(
|
||||
*/
|
||||
function TeamTaskRow(props: {
|
||||
task: SidebarTeamTaskView
|
||||
teammates: readonly SidebarTeamMemberView[]
|
||||
teammates: readonly TeamMemberRow[]
|
||||
onOpenTask(task: SidebarTeamTaskView, anchor: HTMLElement): void
|
||||
}): ReactNode {
|
||||
const { task, teammates, onOpenTask } = props
|
||||
@@ -121,17 +119,12 @@ function TeamTaskRow(props: {
|
||||
title={t('teamTaskDetail')}
|
||||
onClick={(event) => { onOpenTask(task, event.currentTarget) }}
|
||||
>
|
||||
<StateDot
|
||||
size={6}
|
||||
state={task.status === 'completed' ? 'done' : task.ready ? 'ongoing' : 'warning'}
|
||||
/>
|
||||
<StateDot size={6} state={taskDotState(task)} />
|
||||
<span className={css.teamTaskSubject} title={task.subject}>{task.subject}</span>
|
||||
{task.ownerName !== undefined && (
|
||||
<span className={css.teamTaskOwner} title={task.ownerName}>{task.ownerName}</span>
|
||||
)}
|
||||
<Tag tone={taskTone(task)}>
|
||||
{t(task.ready ? taskStatusKey(task.status) : 'teamTaskBlocked')}
|
||||
</Tag>
|
||||
<Tag tone={taskTone(task)}>{taskStatusLabel(task)}</Tag>
|
||||
</Button>
|
||||
<Menu
|
||||
open={menuOpen}
|
||||
@@ -163,7 +156,7 @@ function TeamTaskRow(props: {
|
||||
|
||||
export interface TeamBoardProps {
|
||||
rootId: string
|
||||
members: readonly SidebarTeamMemberView[]
|
||||
members: readonly TeamMemberRow[]
|
||||
tasks: readonly SidebarTeamTaskView[]
|
||||
/** Open the shared task window (undefined = create) anchored at the click. */
|
||||
onOpenTask(task: SidebarTeamTaskView | undefined, anchor: HTMLElement): void
|
||||
@@ -184,7 +177,7 @@ export function TeamBoard(props: TeamBoardProps): ReactNode {
|
||||
const teammates = useMemo(() => members.filter(member => member.role === 'teammate'), [members])
|
||||
|
||||
return (
|
||||
<section className={css.teamBoard} aria-label={t('teamBoard')}>
|
||||
<section className={css.teamBoard} aria-label={t('teamBoard')} data-team-board>
|
||||
<button
|
||||
type="button"
|
||||
className={css.teamBoardBar}
|
||||
@@ -222,12 +215,7 @@ export function TeamBoard(props: TeamBoardProps): ReactNode {
|
||||
setOwnerFilter(current => (current === member.name ? undefined : member.name))
|
||||
}}
|
||||
>
|
||||
<StateDot
|
||||
size={6}
|
||||
state={member.status === 'running' || member.status === 'provisioning'
|
||||
? 'ongoing'
|
||||
: member.status === 'failed' ? 'error' : 'idle'}
|
||||
/>
|
||||
<StateDot size={6} state={memberDotState(member.status)} />
|
||||
<span className={css.teamMemberName}>{member.name}</span>
|
||||
</Pill>
|
||||
))}
|
||||
|
||||
+38
-19
@@ -12,8 +12,6 @@ import type {
|
||||
SidebarCreateTeamTaskRequest,
|
||||
SidebarChildLiveView,
|
||||
SidebarSessionEvent,
|
||||
SidebarTeamMemberView,
|
||||
SidebarTeamTaskMutationResult,
|
||||
SidebarTeamTaskView,
|
||||
SidebarUpdateTeamTaskRequest,
|
||||
} from '../context-types.ts'
|
||||
@@ -109,19 +107,24 @@ export type SubagentLiveResult = { live: Record<string, SidebarChildLiveView> }
|
||||
/** 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
|
||||
/**
|
||||
* One team-task write as CALLERS consume it. The board's READ path has no
|
||||
* route at all (it rides the Lead Session's `agentTeam` projection, see
|
||||
* team-projection.ts), and a rejected write is a RESULT rather than an
|
||||
* exception: the route answers 409 `team-conflict` for a stale revision and
|
||||
* 400 `team-error` otherwise, {@link writeTask} turns that failed envelope
|
||||
* back into `{ok:false, code, message}`, and the task window routes on the
|
||||
* code (a stale revision deserves its own wording, not "operation failed").
|
||||
*/
|
||||
export type TeamsTaskMutationResult =
|
||||
| { ok: true; value: SidebarTeamTaskView }
|
||||
| { ok: false; code?: string; message: string }
|
||||
|
||||
/**
|
||||
* Parse one `/sidebar` JSON response envelope into its value. A non-ok
|
||||
@@ -157,6 +160,29 @@ async function call<T>(method: string, payload: Record<string, unknown>, signal?
|
||||
return readEnvelope<T>(response)
|
||||
}
|
||||
|
||||
/**
|
||||
* One team-task write with the rejection turned back into a RESULT. Every
|
||||
* other `/sidebar/api` caller lets a failed envelope throw (that is what
|
||||
* {@link readEnvelope} is for); here the route's error CODE is data the task
|
||||
* window needs — `team-conflict` means "someone changed this task, refresh",
|
||||
* which is a different sentence from every other failure.
|
||||
* @param method - `teams.taskCreate` or `teams.taskUpdate`.
|
||||
* @param payload - the request plus its `rootSessionId`.
|
||||
*/
|
||||
async function writeTask(
|
||||
method: string,
|
||||
payload: Record<string, unknown>,
|
||||
): Promise<TeamsTaskMutationResult> {
|
||||
try {
|
||||
return { ok: true, value: await call<SidebarTeamTaskView>(method, payload) }
|
||||
} catch (error) {
|
||||
if (error instanceof SidebarApiError) {
|
||||
return { ok: false, code: error.code, message: error.message }
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload one file to the sidebar's raw upload route: the File goes straight
|
||||
* into the POST body (no JSON/base64 re-encoding — the host streams it into
|
||||
@@ -348,19 +374,12 @@ export const api = {
|
||||
*/
|
||||
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. */
|
||||
writeTask('teams.taskCreate', { rootSessionId, ...req }),
|
||||
/** CAS-mutate one shared task; a stale revision yields `team-conflict`. */
|
||||
teamsTaskUpdate: (rootSessionId: string, req: TeamsTaskUpdateRequest) =>
|
||||
call<TeamsTaskMutationResult>('teams.taskUpdate', { rootSessionId, ...req }),
|
||||
writeTask('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. */
|
||||
|
||||
@@ -388,11 +388,14 @@ export const ar: Record<string, string> = {
|
||||
tasksNodeActivity: 'آخر نشاط',
|
||||
tasksStateRunning: 'قيد التشغيل',
|
||||
tasksStateIdle: 'خامل',
|
||||
tasksStateProvisioning: 'Provisioning',
|
||||
tasksStateDone: 'مكتمل',
|
||||
tasksStateError: 'خطأ',
|
||||
tasksKindSubagent: 'وكيل فرعي',
|
||||
tasksKindTeammate: 'عضو فريق',
|
||||
tasksKindWorkflow: 'سير عمل',
|
||||
taskWindowCreate: 'New task',
|
||||
taskWindowDetail: 'Task detail',
|
||||
teamBoard: 'لوحة مهام الفريق',
|
||||
teamChip: 'فريق {members} أعضاء · {tasks} مهام',
|
||||
teamTasksEmpty: 'لا مهام مشتركة بعد',
|
||||
@@ -405,6 +408,7 @@ export const ar: Record<string, string> = {
|
||||
teamTaskInProgress: 'قيد التنفيذ',
|
||||
teamTaskCompleted: 'مكتملة',
|
||||
teamTaskBlocked: 'محجوبة',
|
||||
teamTaskClaim: 'Claim',
|
||||
teamTaskComplete: 'إكمال',
|
||||
teamTaskReopen: 'إعادة فتح',
|
||||
teamTaskDelete: 'حذف',
|
||||
|
||||
@@ -373,11 +373,14 @@ export const de: Record<string, string> = {
|
||||
tasksNodeActivity: 'Letzte Aktivität',
|
||||
tasksStateRunning: 'Läuft',
|
||||
tasksStateIdle: 'Bereit',
|
||||
tasksStateProvisioning: 'Provisioning',
|
||||
tasksStateDone: 'Abgeschlossen',
|
||||
tasksStateError: 'Fehler',
|
||||
tasksKindSubagent: 'Subagent',
|
||||
tasksKindTeammate: 'Teammitglied',
|
||||
tasksKindWorkflow: 'Workflow',
|
||||
taskWindowCreate: 'New task',
|
||||
taskWindowDetail: 'Task detail',
|
||||
teamBoard: 'Team-Aufgabenboard',
|
||||
teamChip: 'Team {members} Mitglieder · {tasks} Aufgaben',
|
||||
teamTasksEmpty: 'Noch keine geteilten Aufgaben',
|
||||
@@ -390,6 +393,7 @@ export const de: Record<string, string> = {
|
||||
teamTaskInProgress: 'In Arbeit',
|
||||
teamTaskCompleted: 'Abgeschlossen',
|
||||
teamTaskBlocked: 'Blockiert',
|
||||
teamTaskClaim: 'Claim',
|
||||
teamTaskComplete: 'Abschließen',
|
||||
teamTaskReopen: 'Wieder öffnen',
|
||||
teamTaskDelete: 'Löschen',
|
||||
|
||||
@@ -380,11 +380,14 @@ export const fr: Record<string, string> = {
|
||||
tasksNodeActivity: 'Dernière activité',
|
||||
tasksStateRunning: 'En cours',
|
||||
tasksStateIdle: 'Inactif',
|
||||
tasksStateProvisioning: 'Provisioning',
|
||||
tasksStateDone: 'Terminé',
|
||||
tasksStateError: 'Erreur',
|
||||
tasksKindSubagent: 'Sous-agent',
|
||||
tasksKindTeammate: 'Membre',
|
||||
tasksKindWorkflow: 'Workflow',
|
||||
taskWindowCreate: 'New task',
|
||||
taskWindowDetail: 'Task detail',
|
||||
teamBoard: 'Tableau des tâches d’équipe',
|
||||
teamChip: 'Équipe {members} membres · {tasks} tâches',
|
||||
teamTasksEmpty: 'Aucune tâche partagée pour l’instant',
|
||||
@@ -397,6 +400,7 @@ export const fr: Record<string, string> = {
|
||||
teamTaskInProgress: 'En cours',
|
||||
teamTaskCompleted: 'Terminée',
|
||||
teamTaskBlocked: 'Bloquée',
|
||||
teamTaskClaim: 'Claim',
|
||||
teamTaskComplete: 'Terminer',
|
||||
teamTaskReopen: 'Rouvrir',
|
||||
teamTaskDelete: 'Supprimer',
|
||||
|
||||
@@ -387,11 +387,14 @@ export const hi: Record<string, string> = {
|
||||
tasksNodeActivity: 'नवीनतम गतिविधि',
|
||||
tasksStateRunning: 'चल रहा है',
|
||||
tasksStateIdle: 'निष्क्रिय',
|
||||
tasksStateProvisioning: 'Provisioning',
|
||||
tasksStateDone: 'पूर्ण',
|
||||
tasksStateError: 'त्रुटि',
|
||||
tasksKindSubagent: 'उप-एजेंट',
|
||||
tasksKindTeammate: 'टीम सदस्य',
|
||||
tasksKindWorkflow: 'वर्कफ़्लो',
|
||||
taskWindowCreate: 'New task',
|
||||
taskWindowDetail: 'Task detail',
|
||||
teamBoard: 'टीम कार्य बोर्ड',
|
||||
teamChip: 'टीम {members} सदस्य · {tasks} कार्य',
|
||||
teamTasksEmpty: 'अभी कोई साझा कार्य नहीं',
|
||||
@@ -404,6 +407,7 @@ export const hi: Record<string, string> = {
|
||||
teamTaskInProgress: 'प्रगति पर',
|
||||
teamTaskCompleted: 'पूर्ण',
|
||||
teamTaskBlocked: 'अवरुद्ध',
|
||||
teamTaskClaim: 'Claim',
|
||||
teamTaskComplete: 'पूर्ण करें',
|
||||
teamTaskReopen: 'पुनः खोलें',
|
||||
teamTaskDelete: 'हटाएँ',
|
||||
|
||||
@@ -385,11 +385,14 @@ export const id: Record<string, string> = {
|
||||
tasksNodeActivity: 'Aktivitas terbaru',
|
||||
tasksStateRunning: 'Berjalan',
|
||||
tasksStateIdle: 'Siaga',
|
||||
tasksStateProvisioning: 'Provisioning',
|
||||
tasksStateDone: 'Selesai',
|
||||
tasksStateError: 'Error',
|
||||
tasksKindSubagent: 'Subagen',
|
||||
tasksKindTeammate: 'Anggota tim',
|
||||
tasksKindWorkflow: 'Alur kerja',
|
||||
taskWindowCreate: 'New task',
|
||||
taskWindowDetail: 'Task detail',
|
||||
teamBoard: 'Papan tugas tim',
|
||||
teamChip: 'Tim {members} anggota · {tasks} tugas',
|
||||
teamTasksEmpty: 'Belum ada tugas bersama',
|
||||
@@ -402,6 +405,7 @@ export const id: Record<string, string> = {
|
||||
teamTaskInProgress: 'Sedang berjalan',
|
||||
teamTaskCompleted: 'Selesai',
|
||||
teamTaskBlocked: 'Terblokir',
|
||||
teamTaskClaim: 'Claim',
|
||||
teamTaskComplete: 'Selesaikan',
|
||||
teamTaskReopen: 'Buka kembali',
|
||||
teamTaskDelete: 'Hapus',
|
||||
|
||||
@@ -378,11 +378,14 @@ export const it: Record<string, string> = {
|
||||
tasksNodeActivity: 'Ultima attività',
|
||||
tasksStateRunning: 'In esecuzione',
|
||||
tasksStateIdle: 'In attesa',
|
||||
tasksStateProvisioning: 'Provisioning',
|
||||
tasksStateDone: 'Completato',
|
||||
tasksStateError: 'Errore',
|
||||
tasksKindSubagent: 'Subagent',
|
||||
tasksKindTeammate: 'Membro del team',
|
||||
tasksKindWorkflow: 'Workflow',
|
||||
taskWindowCreate: 'New task',
|
||||
taskWindowDetail: 'Task detail',
|
||||
teamBoard: 'Bacheca delle attività del team',
|
||||
teamChip: 'Team {members} membri · {tasks} attività',
|
||||
teamTasksEmpty: 'Ancora nessuna attività condivisa',
|
||||
@@ -395,6 +398,7 @@ export const it: Record<string, string> = {
|
||||
teamTaskInProgress: 'In corso',
|
||||
teamTaskCompleted: 'Completata',
|
||||
teamTaskBlocked: 'Bloccata',
|
||||
teamTaskClaim: 'Claim',
|
||||
teamTaskComplete: 'Completa',
|
||||
teamTaskReopen: 'Riapri',
|
||||
teamTaskDelete: 'Elimina',
|
||||
|
||||
@@ -387,11 +387,14 @@ export const ja: Record<string, string> = {
|
||||
tasksNodeActivity: '最新アクティビティ',
|
||||
tasksStateRunning: '実行中',
|
||||
tasksStateIdle: '待機',
|
||||
tasksStateProvisioning: '準備中',
|
||||
tasksStateDone: '完了',
|
||||
tasksStateError: 'エラー',
|
||||
tasksKindSubagent: 'サブエージェント',
|
||||
tasksKindTeammate: 'チームメンバー',
|
||||
tasksKindWorkflow: 'ワークフロー',
|
||||
taskWindowCreate: 'タスクを作成',
|
||||
taskWindowDetail: 'タスクの詳細',
|
||||
teamBoard: 'チームタスクボード',
|
||||
teamChip: 'チーム {members} 名 · {tasks} タスク',
|
||||
teamTasksEmpty: '共有タスクはまだありません',
|
||||
@@ -404,6 +407,7 @@ export const ja: Record<string, string> = {
|
||||
teamTaskInProgress: '進行中',
|
||||
teamTaskCompleted: '完了',
|
||||
teamTaskBlocked: 'ブロック',
|
||||
teamTaskClaim: '引き受ける',
|
||||
teamTaskComplete: '完了',
|
||||
teamTaskReopen: '再オープン',
|
||||
teamTaskDelete: '削除',
|
||||
|
||||
@@ -379,11 +379,14 @@ export const ko: Record<string, string> = {
|
||||
tasksNodeActivity: '최근 활동',
|
||||
tasksStateRunning: '실행 중',
|
||||
tasksStateIdle: '대기 중',
|
||||
tasksStateProvisioning: 'Provisioning',
|
||||
tasksStateDone: '완료됨',
|
||||
tasksStateError: '오류',
|
||||
tasksKindSubagent: '하위 에이전트',
|
||||
tasksKindTeammate: '팀 멤버',
|
||||
tasksKindWorkflow: '워크플로',
|
||||
taskWindowCreate: 'New task',
|
||||
taskWindowDetail: 'Task detail',
|
||||
teamBoard: '팀 작업 보드',
|
||||
teamChip: '팀 멤버 {members}명 · 작업 {tasks}개',
|
||||
teamTasksEmpty: '공유 작업이 없습니다',
|
||||
@@ -396,6 +399,7 @@ export const ko: Record<string, string> = {
|
||||
teamTaskInProgress: '진행 중',
|
||||
teamTaskCompleted: '완료됨',
|
||||
teamTaskBlocked: '차단됨',
|
||||
teamTaskClaim: 'Claim',
|
||||
teamTaskComplete: '완료',
|
||||
teamTaskReopen: '다시 열기',
|
||||
teamTaskDelete: '삭제',
|
||||
|
||||
@@ -385,11 +385,14 @@ export const nl: Record<string, string> = {
|
||||
tasksNodeActivity: 'Laatste activiteit',
|
||||
tasksStateRunning: 'Bezig',
|
||||
tasksStateIdle: 'Inactief',
|
||||
tasksStateProvisioning: 'Provisioning',
|
||||
tasksStateDone: 'Voltooid',
|
||||
tasksStateError: 'Fout',
|
||||
tasksKindSubagent: 'Subagent',
|
||||
tasksKindTeammate: 'Teamlid',
|
||||
tasksKindWorkflow: 'Workflow',
|
||||
taskWindowCreate: 'New task',
|
||||
taskWindowDetail: 'Task detail',
|
||||
teamBoard: 'Teamtakenbord',
|
||||
teamChip: 'Team {members} leden · {tasks} taken',
|
||||
teamTasksEmpty: 'Nog geen gedeelde taken',
|
||||
@@ -402,6 +405,7 @@ export const nl: Record<string, string> = {
|
||||
teamTaskInProgress: 'Bezig',
|
||||
teamTaskCompleted: 'Voltooid',
|
||||
teamTaskBlocked: 'Geblokkeerd',
|
||||
teamTaskClaim: 'Claim',
|
||||
teamTaskComplete: 'Voltooien',
|
||||
teamTaskReopen: 'Heropenen',
|
||||
teamTaskDelete: 'Verwijderen',
|
||||
|
||||
@@ -389,11 +389,14 @@ export const pl: Record<string, string> = {
|
||||
tasksNodeActivity: 'Ostatnia aktywność',
|
||||
tasksStateRunning: 'Działa',
|
||||
tasksStateIdle: 'Gotowe',
|
||||
tasksStateProvisioning: 'Provisioning',
|
||||
tasksStateDone: 'Ukończone',
|
||||
tasksStateError: 'Błąd',
|
||||
tasksKindSubagent: 'Podagent',
|
||||
tasksKindTeammate: 'Członek zespołu',
|
||||
tasksKindWorkflow: 'Przepływ pracy',
|
||||
taskWindowCreate: 'New task',
|
||||
taskWindowDetail: 'Task detail',
|
||||
teamBoard: 'Tablica zadań zespołu',
|
||||
teamChip: 'Zespół {members} członków · {tasks} zadań',
|
||||
teamTasksEmpty: 'Brak wspólnych zadań',
|
||||
@@ -406,6 +409,7 @@ export const pl: Record<string, string> = {
|
||||
teamTaskInProgress: 'W toku',
|
||||
teamTaskCompleted: 'Ukończone',
|
||||
teamTaskBlocked: 'Zablokowane',
|
||||
teamTaskClaim: 'Claim',
|
||||
teamTaskComplete: 'Ukończ',
|
||||
teamTaskReopen: 'Otwórz ponownie',
|
||||
teamTaskDelete: 'Usuń',
|
||||
|
||||
@@ -370,11 +370,14 @@ export const pt: Record<string, string> = {
|
||||
tasksNodeActivity: 'Atividade recente',
|
||||
tasksStateRunning: 'Em execução',
|
||||
tasksStateIdle: 'Em espera',
|
||||
tasksStateProvisioning: 'Provisioning',
|
||||
tasksStateDone: 'Concluído',
|
||||
tasksStateError: 'Erro',
|
||||
tasksKindSubagent: 'Subagente',
|
||||
tasksKindTeammate: 'Membro da equipe',
|
||||
tasksKindWorkflow: 'Fluxo de trabalho',
|
||||
taskWindowCreate: 'New task',
|
||||
taskWindowDetail: 'Task detail',
|
||||
teamBoard: 'Quadro de tarefas da equipe',
|
||||
teamChip: 'Equipe {members} membros · {tasks} tarefas',
|
||||
teamTasksEmpty: 'Ainda não há tarefas compartilhadas',
|
||||
@@ -387,6 +390,7 @@ export const pt: Record<string, string> = {
|
||||
teamTaskInProgress: 'Em andamento',
|
||||
teamTaskCompleted: 'Concluída',
|
||||
teamTaskBlocked: 'Bloqueada',
|
||||
teamTaskClaim: 'Claim',
|
||||
teamTaskComplete: 'Concluir',
|
||||
teamTaskReopen: 'Reabrir',
|
||||
teamTaskDelete: 'Excluir',
|
||||
|
||||
@@ -385,11 +385,14 @@ export const ru: Record<string, string> = {
|
||||
tasksNodeActivity: 'Последняя активность',
|
||||
tasksStateRunning: 'Выполняется',
|
||||
tasksStateIdle: 'Ожидает',
|
||||
tasksStateProvisioning: 'Provisioning',
|
||||
tasksStateDone: 'Завершено',
|
||||
tasksStateError: 'Ошибка',
|
||||
tasksKindSubagent: 'Субагент',
|
||||
tasksKindTeammate: 'Участник команды',
|
||||
tasksKindWorkflow: 'Рабочий процесс',
|
||||
taskWindowCreate: 'New task',
|
||||
taskWindowDetail: 'Task detail',
|
||||
teamBoard: 'Доска задач команды',
|
||||
teamChip: 'Команда: {members} участников · {tasks} задач',
|
||||
teamTasksEmpty: 'Общих задач пока нет',
|
||||
@@ -402,6 +405,7 @@ export const ru: Record<string, string> = {
|
||||
teamTaskInProgress: 'В работе',
|
||||
teamTaskCompleted: 'Завершена',
|
||||
teamTaskBlocked: 'Заблокирована',
|
||||
teamTaskClaim: 'Claim',
|
||||
teamTaskComplete: 'Завершить',
|
||||
teamTaskReopen: 'Открыть снова',
|
||||
teamTaskDelete: 'Удалить',
|
||||
|
||||
@@ -370,11 +370,14 @@ export const sv: Record<string, string> = {
|
||||
tasksNodeActivity: 'Senaste aktivitet',
|
||||
tasksStateRunning: 'Körs',
|
||||
tasksStateIdle: 'Vilar',
|
||||
tasksStateProvisioning: 'Provisioning',
|
||||
tasksStateDone: 'Klar',
|
||||
tasksStateError: 'Fel',
|
||||
tasksKindSubagent: 'Subagent',
|
||||
tasksKindTeammate: 'Teammedlem',
|
||||
tasksKindWorkflow: 'Arbetsflöde',
|
||||
taskWindowCreate: 'New task',
|
||||
taskWindowDetail: 'Task detail',
|
||||
teamBoard: 'Teamuppgiftstavla',
|
||||
teamChip: 'Team {members} medlemmar · {tasks} uppgifter',
|
||||
teamTasksEmpty: 'Inga delade uppgifter än',
|
||||
@@ -387,6 +390,7 @@ export const sv: Record<string, string> = {
|
||||
teamTaskInProgress: 'Pågår',
|
||||
teamTaskCompleted: 'Klar',
|
||||
teamTaskBlocked: 'Blockerad',
|
||||
teamTaskClaim: 'Claim',
|
||||
teamTaskComplete: 'Färdigställ',
|
||||
teamTaskReopen: 'Öppna igen',
|
||||
teamTaskDelete: 'Ta bort',
|
||||
|
||||
@@ -387,11 +387,14 @@ export const th: Record<string, string> = {
|
||||
tasksNodeActivity: 'กิจกรรมล่าสุด',
|
||||
tasksStateRunning: 'กำลังทำงาน',
|
||||
tasksStateIdle: 'พร้อม',
|
||||
tasksStateProvisioning: 'Provisioning',
|
||||
tasksStateDone: 'เสร็จแล้ว',
|
||||
tasksStateError: 'ข้อผิดพลาด',
|
||||
tasksKindSubagent: 'เอเจนต์ย่อย',
|
||||
tasksKindTeammate: 'สมาชิกทีม',
|
||||
tasksKindWorkflow: 'เวิร์กโฟลว์',
|
||||
taskWindowCreate: 'New task',
|
||||
taskWindowDetail: 'Task detail',
|
||||
teamBoard: 'บอร์ดงานของทีม',
|
||||
teamChip: 'ทีม {members} สมาชิก · {tasks} งาน',
|
||||
teamTasksEmpty: 'ยังไม่มีงานที่แชร์',
|
||||
@@ -404,6 +407,7 @@ export const th: Record<string, string> = {
|
||||
teamTaskInProgress: 'กำลังดำเนินการ',
|
||||
teamTaskCompleted: 'เสร็จแล้ว',
|
||||
teamTaskBlocked: 'ถูกบล็อก',
|
||||
teamTaskClaim: 'Claim',
|
||||
teamTaskComplete: 'ทำให้เสร็จ',
|
||||
teamTaskReopen: 'เปิดใหม่',
|
||||
teamTaskDelete: 'ลบ',
|
||||
|
||||
@@ -387,11 +387,14 @@ export const tr: Record<string, string> = {
|
||||
tasksNodeActivity: 'Son etkinlik',
|
||||
tasksStateRunning: 'Çalışıyor',
|
||||
tasksStateIdle: 'Beklemede',
|
||||
tasksStateProvisioning: 'Provisioning',
|
||||
tasksStateDone: 'Tamamlandı',
|
||||
tasksStateError: 'Hata',
|
||||
tasksKindSubagent: 'Alt ajan',
|
||||
tasksKindTeammate: 'Ekip üyesi',
|
||||
tasksKindWorkflow: 'İş akışı',
|
||||
taskWindowCreate: 'New task',
|
||||
taskWindowDetail: 'Task detail',
|
||||
teamBoard: 'Takım görev panosu',
|
||||
teamChip: 'Takım {members} üye · {tasks} görev',
|
||||
teamTasksEmpty: 'Henüz paylaşılan görev yok',
|
||||
@@ -404,6 +407,7 @@ export const tr: Record<string, string> = {
|
||||
teamTaskInProgress: 'Devam ediyor',
|
||||
teamTaskCompleted: 'Tamamlandı',
|
||||
teamTaskBlocked: 'Engelli',
|
||||
teamTaskClaim: 'Claim',
|
||||
teamTaskComplete: 'Tamamla',
|
||||
teamTaskReopen: 'Yeniden aç',
|
||||
teamTaskDelete: 'Sil',
|
||||
|
||||
@@ -387,11 +387,14 @@ export const vi: Record<string, string> = {
|
||||
tasksNodeActivity: 'Hoạt động gần đây',
|
||||
tasksStateRunning: 'Đang chạy',
|
||||
tasksStateIdle: 'Nhàn rỗi',
|
||||
tasksStateProvisioning: 'Provisioning',
|
||||
tasksStateDone: 'Hoàn thành',
|
||||
tasksStateError: 'Lỗi',
|
||||
tasksKindSubagent: 'Tác nhân con',
|
||||
tasksKindTeammate: 'Thành viên nhóm',
|
||||
tasksKindWorkflow: 'Quy trình',
|
||||
taskWindowCreate: 'New task',
|
||||
taskWindowDetail: 'Task detail',
|
||||
teamBoard: 'Bảng tác vụ nhóm',
|
||||
teamChip: 'Nhóm {members} thành viên · {tasks} tác vụ',
|
||||
teamTasksEmpty: 'Chưa có tác vụ dùng chung',
|
||||
@@ -404,6 +407,7 @@ export const vi: Record<string, string> = {
|
||||
teamTaskInProgress: 'Đang tiến hành',
|
||||
teamTaskCompleted: 'Hoàn thành',
|
||||
teamTaskBlocked: 'Bị chặn',
|
||||
teamTaskClaim: 'Claim',
|
||||
teamTaskComplete: 'Hoàn tất',
|
||||
teamTaskReopen: 'Mở lại',
|
||||
teamTaskDelete: 'Xóa',
|
||||
|
||||
@@ -402,11 +402,14 @@ export const zhHK: Record<string, string> = {
|
||||
tasksNodeActivity: '最近活動',
|
||||
tasksStateRunning: '執行中',
|
||||
tasksStateIdle: '閒置',
|
||||
tasksStateProvisioning: '部署中',
|
||||
tasksStateDone: '已完成',
|
||||
tasksStateError: '出錯',
|
||||
tasksKindSubagent: '子代理',
|
||||
tasksKindTeammate: '成員',
|
||||
tasksKindWorkflow: '工作流',
|
||||
taskWindowCreate: '新增任務',
|
||||
taskWindowDetail: '任務詳情',
|
||||
teamBoard: '團隊任務板',
|
||||
teamChip: '團隊 {members} 成員 · {tasks} 任務',
|
||||
teamTasksEmpty: '暫無共用任務',
|
||||
@@ -419,6 +422,7 @@ export const zhHK: Record<string, string> = {
|
||||
teamTaskInProgress: '進行中',
|
||||
teamTaskCompleted: '已完成',
|
||||
teamTaskBlocked: '受阻',
|
||||
teamTaskClaim: '认领',
|
||||
teamTaskComplete: '完成',
|
||||
teamTaskReopen: '重開',
|
||||
teamTaskDelete: '刪除',
|
||||
|
||||
@@ -402,11 +402,14 @@ export const zhMO: Record<string, string> = {
|
||||
tasksNodeActivity: '最近活動',
|
||||
tasksStateRunning: '執行中',
|
||||
tasksStateIdle: '閒置',
|
||||
tasksStateProvisioning: '部署中',
|
||||
tasksStateDone: '已完成',
|
||||
tasksStateError: '出錯',
|
||||
tasksKindSubagent: '子代理',
|
||||
tasksKindTeammate: '成員',
|
||||
tasksKindWorkflow: '工作流',
|
||||
taskWindowCreate: '新增任務',
|
||||
taskWindowDetail: '任務詳情',
|
||||
teamBoard: '團隊任務板',
|
||||
teamChip: '團隊 {members} 成員 · {tasks} 任務',
|
||||
teamTasksEmpty: '暫無共用任務',
|
||||
@@ -419,6 +422,7 @@ export const zhMO: Record<string, string> = {
|
||||
teamTaskInProgress: '進行中',
|
||||
teamTaskCompleted: '已完成',
|
||||
teamTaskBlocked: '受阻',
|
||||
teamTaskClaim: '认领',
|
||||
teamTaskComplete: '完成',
|
||||
teamTaskReopen: '重開',
|
||||
teamTaskDelete: '刪除',
|
||||
|
||||
@@ -402,11 +402,14 @@ export const zhTW: Record<string, string> = {
|
||||
tasksNodeActivity: '最近活動',
|
||||
tasksStateRunning: '執行中',
|
||||
tasksStateIdle: '閒置',
|
||||
tasksStateProvisioning: '部署中',
|
||||
tasksStateDone: '已完成',
|
||||
tasksStateError: '出錯',
|
||||
tasksKindSubagent: '子代理',
|
||||
tasksKindTeammate: '成員',
|
||||
tasksKindWorkflow: '工作流',
|
||||
taskWindowCreate: '新增任務',
|
||||
taskWindowDetail: '任務詳情',
|
||||
teamBoard: '團隊任務板',
|
||||
teamChip: '團隊 {members} 成員 · {tasks} 任務',
|
||||
teamTasksEmpty: '暫無共用任務',
|
||||
@@ -419,6 +422,7 @@ export const zhTW: Record<string, string> = {
|
||||
teamTaskInProgress: '進行中',
|
||||
teamTaskCompleted: '已完成',
|
||||
teamTaskBlocked: '受阻',
|
||||
teamTaskClaim: '认领',
|
||||
teamTaskComplete: '完成',
|
||||
teamTaskReopen: '重開',
|
||||
teamTaskDelete: '刪除',
|
||||
|
||||
@@ -399,11 +399,14 @@ export const zh = {
|
||||
tasksNodeActivity: '最近活动',
|
||||
tasksStateRunning: '运行中',
|
||||
tasksStateIdle: '待命',
|
||||
tasksStateProvisioning: '部署中',
|
||||
tasksStateDone: '已完成',
|
||||
tasksStateError: '出错',
|
||||
tasksKindSubagent: '子代理',
|
||||
tasksKindTeammate: '成员',
|
||||
tasksKindWorkflow: '工作流',
|
||||
taskWindowCreate: '新建任务',
|
||||
taskWindowDetail: '任务详情',
|
||||
teamBoard: '团队任务板',
|
||||
teamChip: '团队 {members} 成员 · {tasks} 任务',
|
||||
teamTasksEmpty: '暂无共享任务',
|
||||
@@ -416,6 +419,7 @@ export const zh = {
|
||||
teamTaskInProgress: '进行中',
|
||||
teamTaskCompleted: '已完成',
|
||||
teamTaskBlocked: '阻塞',
|
||||
teamTaskClaim: '认领',
|
||||
teamTaskComplete: '完成',
|
||||
teamTaskReopen: '重开',
|
||||
teamTaskDelete: '删除',
|
||||
@@ -878,11 +882,14 @@ export const en: Record<keyof typeof zh, string> = {
|
||||
tasksNodeActivity: 'Latest activity',
|
||||
tasksStateRunning: 'Running',
|
||||
tasksStateIdle: 'Idle',
|
||||
tasksStateProvisioning: 'Provisioning',
|
||||
tasksStateDone: 'Done',
|
||||
tasksStateError: 'Error',
|
||||
tasksKindSubagent: 'Subagent',
|
||||
tasksKindTeammate: 'Teammate',
|
||||
tasksKindWorkflow: 'Workflow',
|
||||
taskWindowCreate: 'New task',
|
||||
taskWindowDetail: 'Task detail',
|
||||
teamBoard: 'Team task board',
|
||||
teamChip: 'Team {members} members · {tasks} tasks',
|
||||
teamTasksEmpty: 'No shared tasks yet',
|
||||
@@ -895,6 +902,7 @@ export const en: Record<keyof typeof zh, string> = {
|
||||
teamTaskInProgress: 'In progress',
|
||||
teamTaskCompleted: 'Completed',
|
||||
teamTaskBlocked: 'Blocked',
|
||||
teamTaskClaim: 'Claim',
|
||||
teamTaskComplete: 'Complete',
|
||||
teamTaskReopen: 'Reopen',
|
||||
teamTaskDelete: 'Delete',
|
||||
|
||||
@@ -788,17 +788,6 @@
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
}
|
||||
|
||||
/* The chip's own 10px padding would inset its glyph from the card's right
|
||||
content edge; pull the box back by 8px so the × lands on the text axis. */
|
||||
.popHeadActions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
margin-right: -8px;
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
/* The action bar closes EVERY card the same way: one l1 rule above, one 8px
|
||||
beat, and the primary action left to fill whatever the row gives it. */
|
||||
.popFoot {
|
||||
@@ -820,6 +809,24 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* The task window's own head line (inside the floating window's scrolling
|
||||
body): state dot + subject + status Tag. Sentence case, unlike the node
|
||||
detail's letter-spaced uppercase `.popHead` band — this one labels a real
|
||||
subject rather than a card's section. */
|
||||
.taskHeadRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.taskHeadRow .popTitle {
|
||||
flex: 1;
|
||||
font: var(--dsw-font-xs-13);
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
.jobPopMeta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -838,14 +845,13 @@
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
}
|
||||
|
||||
/* The action row of a floating window. It is the shell's FOOTER, which already
|
||||
draws the body/footer boundary, so it carries no rule of its own. */
|
||||
.jobPopActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--dsw-alias-border-l1);
|
||||
}
|
||||
|
||||
/* The follow-tail switch is the action bar's left cell; the host Switch draws
|
||||
@@ -940,7 +946,7 @@
|
||||
|
||||
/* A section inside a card opens with the family's one hairline: 8px of air on
|
||||
both sides of the rule (the card's own 4px gap plus 4px here, and the 8px
|
||||
padding below), exactly the beat `.jobPopActions` closes a card with. */
|
||||
padding below), the same beat the window footers' own rule uses. */
|
||||
.popSection {
|
||||
margin-top: 4px;
|
||||
padding-top: 8px;
|
||||
|
||||
+18
-12
@@ -13,8 +13,8 @@
|
||||
* exist as the origin's catalog children are RE-PARENTED under the run
|
||||
* node, members without a catalog row are synthesized from the run's own
|
||||
* member data (so a finished run still shows its members);
|
||||
* - team members enrich matching agent nodes in place (role/model/status);
|
||||
* the lead's membership lands on the root node;
|
||||
* - team members enrich matching agent nodes in place (role/phase/status);
|
||||
* the lead's roster row lands on the root node;
|
||||
* - fold: per parent, settled agent LEAVES (state done/error, never the
|
||||
* current session, never a workflow member's run node) collapse into one
|
||||
* trailing `fold` node carrying the member ids and label previews.
|
||||
@@ -23,13 +23,14 @@ import type {
|
||||
SidebarSessionSummary,
|
||||
SidebarSubagentAddress,
|
||||
SidebarSubagentCatalogEntry,
|
||||
SidebarTeamMemberView,
|
||||
SidebarTeamMemberProjection,
|
||||
SidebarTeamTaskView,
|
||||
} from '../context-types.ts'
|
||||
import type { SidebarChildLiveView } from '../context-types.ts'
|
||||
import type { WorkflowRunView } from '../workflow-runs.ts'
|
||||
import { isSideThreadSummary } from './subagent-detect.ts'
|
||||
import { childLive, isKnownLeaf, type SubagentCatalogView } from './subagent-catalog.ts'
|
||||
import type { TeamMemberRow } from './team-projection.ts'
|
||||
|
||||
/** Display state of one agent node (drives the dot + fold candidacy). */
|
||||
export type TasksNodeState = 'running' | 'idle' | 'done' | 'error'
|
||||
@@ -61,12 +62,14 @@ export interface TasksAgentNode {
|
||||
current: boolean
|
||||
/** Live tail of a running child (the merged activity + newest text). */
|
||||
live?: SidebarChildLiveView
|
||||
/** Team enrichment (membership view matched by session id). */
|
||||
/** Team enrichment (roster row matched by session id). */
|
||||
team?: {
|
||||
role: 'lead' | 'teammate'
|
||||
name: string
|
||||
model?: string
|
||||
status: SidebarTeamMemberView['status']
|
||||
/** Durable lifecycle of the roster row (`provisioning | active | failed`). */
|
||||
phase: SidebarTeamMemberProjection['phase']
|
||||
/** The member's derived display status (see ./team-projection.ts). */
|
||||
status: TeamMemberRow['status']
|
||||
diagnostics: string[]
|
||||
}
|
||||
/**
|
||||
@@ -121,7 +124,7 @@ export interface TasksModelInput {
|
||||
currentSessionId: string
|
||||
live: Readonly<Record<string, SidebarChildLiveView | undefined>>
|
||||
runs: readonly WorkflowRunView[]
|
||||
teamMembers: readonly SidebarTeamMemberView[]
|
||||
teamMembers: readonly TeamMemberRow[]
|
||||
/** The team's shared tasks; each lands on its OWNER's node. */
|
||||
teamTasks?: readonly SidebarTeamTaskView[]
|
||||
/** Whether settled leaves collapse into fold nodes. */
|
||||
@@ -142,14 +145,17 @@ function childLabel(
|
||||
return entry.label ?? summary?.displayTitle ?? entry.id
|
||||
}
|
||||
|
||||
/** Map a team member's runtime status onto the node display state. */
|
||||
function teamState(status: SidebarTeamMemberView['status']): TasksNodeState {
|
||||
/**
|
||||
* Map a team member's derived status onto the node display state. The status
|
||||
* already folds in the durable phase (./team-projection.ts), so this is a
|
||||
* vocabulary translation and nothing more.
|
||||
*/
|
||||
function teamState(status: TeamMemberRow['status']): TasksNodeState {
|
||||
switch (status) {
|
||||
case 'running': return 'running'
|
||||
case 'provisioning': return 'running'
|
||||
case 'failed': return 'error'
|
||||
case 'idle':
|
||||
case 'inactive': return 'idle'
|
||||
case 'idle': return 'idle'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,7 +207,7 @@ export function buildTasksModel(input: TasksModelInput): TasksNode[] {
|
||||
const member = teamById.get(id)
|
||||
return member === undefined
|
||||
? undefined
|
||||
: { role: member.role, name: member.name, model: member.model, status: member.status, diagnostics: member.diagnostics }
|
||||
: { role: member.role, name: member.name, phase: member.phase, status: member.status, diagnostics: member.diagnostics }
|
||||
}
|
||||
|
||||
const out: TasksNode[] = []
|
||||
|
||||
+59
-13
@@ -48,19 +48,58 @@ export function stateLabel(state: TasksNodeState): string {
|
||||
return t(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* The two facts every task display rule reads. Deliberately NARROWER than
|
||||
* either carrier (`TasksNodeTask` on the graph, `SidebarTeamTaskView` in the
|
||||
* board and the task window) so one rule serves all of them — the window sees
|
||||
* a `deleted` status the graph never draws, and only the shared rule needs to
|
||||
* know that a deleted task is not "blocked".
|
||||
*/
|
||||
export interface TaskStatusFacts {
|
||||
status: 'pending' | 'in_progress' | 'completed' | 'deleted'
|
||||
ready: boolean
|
||||
}
|
||||
|
||||
/** The task status label key. */
|
||||
export function taskStatusKey(status: TasksNodeTask['status']): CopyKey {
|
||||
export function taskStatusKey(status: TaskStatusFacts['status']): CopyKey {
|
||||
switch (status) {
|
||||
case 'pending': return 'teamTaskPending'
|
||||
case 'in_progress': return 'teamTaskInProgress'
|
||||
case 'completed': return 'teamTaskCompleted'
|
||||
case 'deleted': return 'teamTaskDeleted'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a shared task is HELD UP by its blockers.
|
||||
*
|
||||
* The service's `ready` flag means exactly one thing: "a pending task whose
|
||||
* every blocker completed, so it can be CLAIMED". It is therefore false for
|
||||
* every task that is not queued — which makes `!ready` the wrong test for
|
||||
* "blocked": a claimed task (in_progress) and a finished one both report
|
||||
* `ready: false`, and reading the flag alone labelled a task that is actively
|
||||
* being worked on as 阻塞 (reproduced on the real board: claiming a task made
|
||||
* its Tag flip from 待办 to 阻塞).
|
||||
*/
|
||||
export function taskBlocked(task: TaskStatusFacts): boolean {
|
||||
return task.status === 'pending' && !task.ready
|
||||
}
|
||||
|
||||
/** The status word of a task row: its own status, or 阻塞 when held up. */
|
||||
export function taskStatusLabel(task: TaskStatusFacts): string {
|
||||
return t(taskBlocked(task) ? 'teamTaskBlocked' : taskStatusKey(task.status))
|
||||
}
|
||||
|
||||
/** The tone of a task's status Tag (or dot) — the same three-way rule. */
|
||||
export function taskTone(task: TaskStatusFacts): 'success' | 'info' | 'warning' {
|
||||
if (task.status === 'completed') return 'success'
|
||||
return taskBlocked(task) ? 'warning' : 'info'
|
||||
}
|
||||
|
||||
/** The host StateDot semantic of a task row/line. */
|
||||
export function taskDotState(task: TasksNodeTask): StateDotState {
|
||||
export function taskDotState(task: TaskStatusFacts): StateDotState {
|
||||
if (task.status === 'completed') return 'done'
|
||||
return task.ready ? 'ongoing' : 'warning'
|
||||
return taskBlocked(task) ? 'warning' : 'ongoing'
|
||||
}
|
||||
|
||||
/** First `limit` characters with an ellipsis when truncated. */
|
||||
@@ -108,17 +147,17 @@ export function modeLabel(mode: TasksAgentNode['mode']): string | undefined {
|
||||
/**
|
||||
* The mono meta line of an agent card — at most two short tokens:
|
||||
* - the topology root: 主代理 · 状态
|
||||
* - a teammate with a known model: 模型 · 状态
|
||||
* - any other agent: 模式 · 状态
|
||||
* - any other agent: 模式 · 状态 (a teammate without a catalog mode falls back
|
||||
* to its team role, the one durable identity the roster still carries — the
|
||||
* `agentTeam` projection has no model field to print, see team-projection.ts)
|
||||
*/
|
||||
export function agentMeta(node: TasksAgentNode): string {
|
||||
const state = stateLabel(node.state)
|
||||
if (node.parentId === undefined) return `${t('subagentMainAgent')} · ${state}`
|
||||
if (node.team?.role === 'teammate' && node.team.model !== undefined && node.team.model !== '') {
|
||||
return `${node.team.model} · ${state}`
|
||||
}
|
||||
const mode = modeLabel(node.mode)
|
||||
return mode === undefined ? state : `${mode} · ${state}`
|
||||
if (mode !== undefined) return `${mode} · ${state}`
|
||||
const fallback = node.team === undefined ? undefined : teamRoleLabel(node.team)
|
||||
return fallback === undefined ? state : `${fallback} · ${state}`
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,10 +172,17 @@ export function agentMeta(node: TasksAgentNode): string {
|
||||
export function agentIdentity(node: TasksAgentNode): string {
|
||||
// The root's badge already reads 主代理: an identity line would repeat it.
|
||||
if (node.parentId === undefined) return ''
|
||||
if (node.team?.role === 'teammate' && node.team.model !== undefined && node.team.model !== '') {
|
||||
return node.team.model
|
||||
}
|
||||
return modeLabel(node.mode) ?? ''
|
||||
const mode = modeLabel(node.mode)
|
||||
if (mode !== undefined) return mode
|
||||
return node.team === undefined ? '' : teamRoleLabel(node.team) ?? ''
|
||||
}
|
||||
|
||||
/** The roster row's own identity word (the fallback for a modelless teammate). */
|
||||
function teamRoleLabel(team: NonNullable<TasksAgentNode['team']>): string | undefined {
|
||||
if (team.role === 'lead') return undefined
|
||||
if (team.phase === 'provisioning') return t('tasksStateProvisioning')
|
||||
if (team.phase === 'failed') return t('tasksStateError')
|
||||
return t('tasksKindTeammate')
|
||||
}
|
||||
|
||||
/** The mono meta line of a workflow run card: status · member tally. */
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* 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'
|
||||
}
|
||||
+61
-14
@@ -352,22 +352,37 @@ export interface SidebarAgentPresetsService {
|
||||
/**
|
||||
* The experimental Agent Teams service face (`ctx.agentTeams`, mounted only
|
||||
* when the deployment loads `dsh-experimental-agent-team-profile`; absent →
|
||||
* `ctx.get` returns undefined and the Teams block hides). Only the
|
||||
* browser-facing Remote vocabulary the Tasks page needs is mirrored —
|
||||
* structurally, so the plugin never imports the experimental package.
|
||||
* `ctx.get` returns undefined and the Teams block hides).
|
||||
*
|
||||
* Only the WRITE half is mirrored here. DSH 0.1.7 deleted the 0.1.6
|
||||
* `remoteView` / `remoteCreateTask` / `remoteUpdateTask` trio (the browser UI
|
||||
* that consumed it went away with `ctx.remote`) and moved the board's READ
|
||||
* path onto the Lead Session's `agentTeam` projection — which the client
|
||||
* already receives in `SessionListState.projectionsBySession`. So the reads
|
||||
* never touch this service any more; the two remaining calls each need the
|
||||
* exact live Lead Agent as their authority credential, hence the host routes.
|
||||
*
|
||||
* Rejections are THROWN now, not returned: `createTask` / `updateTask` hand
|
||||
* back the committed view, and a stale revision throws a `TeamError`
|
||||
* (`HarnessError` subclass, `code === 'TEAM_TASK_STALE_REVISION'`) instead of
|
||||
* resolving the 0.1.6 `TeamTaskMutationResult` union.
|
||||
*/
|
||||
export interface SidebarAgentTeamsService {
|
||||
/** The agent's team membership, or undefined for a non-team/stale agent. */
|
||||
tryMembership(agent: unknown): unknown
|
||||
/** The point-in-time team snapshot the official panel renders. */
|
||||
remoteView(agent: unknown): Promise<{ members: SidebarTeamMemberView[]; tasks: SidebarTeamTaskView[] }>
|
||||
/** Create one shared task (CAS-free; ids are server-issued). */
|
||||
remoteCreateTask(agent: unknown, req: SidebarCreateTeamTaskRequest): Promise<SidebarTeamTaskMutationResult>
|
||||
/** Compare-and-set mutation of one shared task (stale revision → conflict). */
|
||||
remoteUpdateTask(agent: unknown, req: SidebarUpdateTeamTaskRequest): Promise<SidebarTeamTaskMutationResult>
|
||||
createTask(agent: unknown, req: SidebarCreateTeamTaskRequest): Promise<SidebarTeamTaskView>
|
||||
/** Compare-and-set mutation of one shared task (stale revision → throws). */
|
||||
updateTask(agent: unknown, req: SidebarUpdateTeamTaskRequest): Promise<SidebarTeamTaskView>
|
||||
}
|
||||
|
||||
/** One team member as the runtime-enriched view reports it. */
|
||||
/**
|
||||
* One team member as the runtime-enriched `listMembers` view reports it.
|
||||
* NOT consumed by this plugin any more (the projection below carries the
|
||||
* durable half and the live channel carries activity, see
|
||||
* ./client/team-projection.ts); kept as the mirror of the service's own
|
||||
* vocabulary so a future reader does not re-derive it.
|
||||
*/
|
||||
export interface SidebarTeamMemberView {
|
||||
/** The member's session id (the teammate's child session under the lead). */
|
||||
id: string
|
||||
@@ -381,6 +396,33 @@ export interface SidebarTeamMemberView {
|
||||
diagnostics: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* One durable roster row of the Lead Session's `agentTeam` projection. Phase
|
||||
* is the DURABLE lifecycle (the Lead row is always `active`); turn activity is
|
||||
* overlaid from the session's own status (see ./client/team-projection.ts).
|
||||
*/
|
||||
export interface SidebarTeamMemberProjection {
|
||||
/** The member's session id (the teammate's child session under the lead). */
|
||||
id: string
|
||||
name: string
|
||||
role: 'lead' | 'teammate'
|
||||
phase: 'provisioning' | 'active' | 'failed'
|
||||
/** The provisioning failure, when the durable row records one. */
|
||||
error?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* The Lead Session's published team state: durable roster identities and
|
||||
* phases, member errors, the non-deleted task views (0.1.6's `TeamView` shape,
|
||||
* enriched per task exactly like the service's own views), and the first
|
||||
* rejected Team record when the board had to stop at its last valid state.
|
||||
*/
|
||||
export interface SidebarTeamProjection {
|
||||
members: readonly SidebarTeamMemberProjection[]
|
||||
tasks: readonly SidebarTeamTaskView[]
|
||||
failure?: string
|
||||
}
|
||||
|
||||
/** One shared task-board row (durable fields plus derived readiness). */
|
||||
export interface SidebarTeamTaskView {
|
||||
id: string
|
||||
@@ -415,10 +457,11 @@ export interface SidebarUpdateTeamTaskRequest {
|
||||
owner?: string
|
||||
}
|
||||
|
||||
/** Browser task-mutation result (stale revisions kept distinct, passthrough). */
|
||||
export type SidebarTeamTaskMutationResult =
|
||||
| { ok: true; value: SidebarTeamTaskView }
|
||||
| { ok: false; error: { code: 'team-task-conflict' | 'team-rejected'; message: string } }
|
||||
/** What a team-task write hands back: the committed view (0.1.7 shape). */
|
||||
export interface SidebarTeamTaskMutationResult {
|
||||
ok: true
|
||||
value: SidebarTeamTaskView
|
||||
}
|
||||
|
||||
/** The host session-title service face (mirror of the sessionTitle service). */
|
||||
export interface SidebarSessionTitleService {
|
||||
@@ -478,7 +521,11 @@ export interface SidebarSessionList {
|
||||
|
||||
/** One session's projection values, as the client snapshot publishes them. */
|
||||
export interface SidebarProjectionSnapshot {
|
||||
values: { subagentCatalog?: readonly SidebarSubagentCatalogEntry[] }
|
||||
values: {
|
||||
subagentCatalog?: readonly SidebarSubagentCatalogEntry[]
|
||||
/** The Lead Session's team board (only the team's Lead carries one). */
|
||||
agentTeam?: SidebarTeamProjection
|
||||
}
|
||||
state: 'idle' | 'loading' | 'ready' | 'error'
|
||||
error: { code?: string; message?: string } | null
|
||||
}
|
||||
|
||||
+3
-3
@@ -483,9 +483,9 @@ function buildApi(
|
||||
// Workflow runs of the whole tree (folded from `tool-workflow/*`
|
||||
// session events; empty list when the tree never ran one).
|
||||
'workflows.list': (payload) => workflowsApi.list(payload),
|
||||
// Agent Teams (experimental): the root-led team's roster + task board,
|
||||
// and the task board's create/CAS-update mutations.
|
||||
'teams.view': (payload) => teamsApi.view(payload),
|
||||
// Agent Teams (experimental): the task board's create/CAS-update
|
||||
// mutations. Reads ride the Lead Session's `agentTeam` projection, so
|
||||
// there is no `teams.view` route (see src/team-routes.ts).
|
||||
'teams.taskCreate': (payload) => teamsApi.taskCreate(payload),
|
||||
'teams.taskUpdate': (payload) => teamsApi.taskUpdate(payload),
|
||||
// The side card preferences. The settings service is optional in the
|
||||
|
||||
+81
-68
@@ -1,51 +1,53 @@
|
||||
/**
|
||||
* The Agent Teams routes of the /sidebar JSON API ('teams.view' /
|
||||
* 'teams.taskCreate' / 'teams.taskUpdate'). The experimental
|
||||
* `dsh-experimental-agent-team` service (`ctx.agentTeams`) exists only when
|
||||
* the deployment opts in, so the routes degrade structurally:
|
||||
* The Agent Teams WRITE routes of the /sidebar JSON API ('teams.taskCreate' /
|
||||
* 'teams.taskUpdate').
|
||||
*
|
||||
* - service absent (`ctx.get('agentTeams')` undefined) → `{available:false}`
|
||||
* — the client hides the whole Teams block silently (absence is a
|
||||
* deployment fact, never an error banner);
|
||||
* - the tree's root agent is not a team member (`tryMembership` miss) →
|
||||
* `{available:true, team:null}` — the page shows the plain agent graph;
|
||||
* - hit → the service's own Remote vocabulary (`remoteView` /
|
||||
* `remoteCreateTask` / `remoteUpdateTask`), whose mutation result union
|
||||
* already keeps CAS conflicts (`team-task-conflict`) distinct from other
|
||||
* rejections — the route passes it through untouched so the client's
|
||||
* edit popover can render "someone else changed this task, refreshing".
|
||||
* The READ path is deliberately absent. DSH 0.1.7 replaced the 0.1.6 Remote
|
||||
* trio (`remoteView` / `remoteCreateTask` / `remoteUpdateTask`, which this
|
||||
* plugin's `teams.view` route mirrored) with the Lead Session's `agentTeam`
|
||||
* Session projection: the browser already receives it in
|
||||
* `SessionListState.projectionsBySession`, so the Tasks page reads the board
|
||||
* straight off the shared snapshot (see ./client/team-projection.ts) and no
|
||||
* host route, poller or observation handshake is involved. That also removed
|
||||
* the only reason this file needed `tryMembership` for reads.
|
||||
*
|
||||
* What remains cannot be done in the browser: `createTask` / `updateTask`
|
||||
* demand the exact live Lead Agent as their authority credential, which only
|
||||
* the host holds. The experimental `dsh-experimental-agent-team` service
|
||||
* (`ctx.agentTeams`) exists only when the deployment opts in, so the routes
|
||||
* degrade structurally:
|
||||
*
|
||||
* - service absent (`ctx.get('agentTeams')` undefined) → 503 `team-error`
|
||||
* (a mutation can never be accepted there; the page silently hides the
|
||||
* whole Teams block because the projection is absent too, so this is the
|
||||
* belt to that pair of braces);
|
||||
* - the tree's root agent is not live in this process (cold session / another
|
||||
* harness) → 404 `team-error`; teams are live-led by definition;
|
||||
* - the root leads no team (`tryMembership` miss) → 404 `team-error`.
|
||||
*
|
||||
* Rejections are THROWN by 0.1.7 (0.1.6 returned a `TeamTaskMutationResult`
|
||||
* union), so the CAS conflict has to be translated back into a wire code the
|
||||
* client can route on: `TEAM_TASK_STALE_REVISION` → `team-conflict` (409),
|
||||
* every other `TeamError` → `team-error` (400). The message is never parsed.
|
||||
*
|
||||
* Teams are implicit (TeamId ≡ the lead's session id) and every service
|
||||
* method demands the live member Agent as its authority token, so the
|
||||
* routes re-derive the caller per request (`ctx.agents.get(rootSessionId)`)
|
||||
* exactly like the jobs.kill fence. Zero DSH source changes; the plugin
|
||||
* never imports the experimental package (structural mirrors only).
|
||||
* method demands the live member Agent as its authority token, so the routes
|
||||
* re-derive the caller per request (`ctx.agents.get(rootSessionId)`) exactly
|
||||
* like the jobs.kill fence. Zero DSH source changes; the plugin never imports
|
||||
* the experimental package (structural mirrors only).
|
||||
*/
|
||||
import type {
|
||||
Context,
|
||||
SidebarAgentTeamsService,
|
||||
SidebarAgentsService,
|
||||
SidebarCreateTeamTaskRequest,
|
||||
SidebarTeamMemberView,
|
||||
SidebarTeamTaskMutationResult,
|
||||
SidebarTeamTaskView,
|
||||
SidebarUpdateTeamTaskRequest,
|
||||
} from './context-types.ts'
|
||||
import { requireString, SidebarError } from './wire.ts'
|
||||
|
||||
/** The Agent Teams routes of the sidebar API. */
|
||||
/** The Agent Teams write routes of the sidebar API. */
|
||||
export interface SidebarTeamsRoutes {
|
||||
/**
|
||||
* The team led by the tree's root agent, if any.
|
||||
* @param payload - `{ rootSessionId }`.
|
||||
* @returns `{ available: false }` when the deployment lacks the
|
||||
* experimental layer; `{ available: true, team: null }` when the root
|
||||
* leads no team; otherwise the members + task board.
|
||||
*/
|
||||
view(payload: unknown): Promise<
|
||||
| { available: false }
|
||||
| { available: true; team: { members: SidebarTeamMemberView[]; tasks: SidebarTeamTaskView[] } | null }
|
||||
>
|
||||
/** Create one shared task (payload = `{ rootSessionId, ...request }`). */
|
||||
taskCreate(payload: unknown): Promise<SidebarTeamTaskMutationResult>
|
||||
/** CAS-mutate one shared task (payload = `{ rootSessionId, ...request }`). */
|
||||
@@ -57,6 +59,9 @@ const TASK_ACTIONS = new Set([
|
||||
'claim', 'release', 'edit', 'set_dependencies', 'complete', 'reopen', 'reassign', 'delete',
|
||||
])
|
||||
|
||||
/** The service's stale-revision code (0.1.6 returned it as a result union). */
|
||||
const STALE_REVISION_CODE = 'TEAM_TASK_STALE_REVISION'
|
||||
|
||||
/** Narrow an unknown payload value to a string array (else undefined). */
|
||||
function stringArrayOf(value: unknown): string[] | undefined {
|
||||
if (!Array.isArray(value)) return undefined
|
||||
@@ -65,7 +70,18 @@ function stringArrayOf(value: unknown): string[] | undefined {
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the Agent Teams routes bound to the plugin context.
|
||||
* The thrown value's `HarnessError` code, when it carries one. Read
|
||||
* structurally: the plugin does not import `@deepseek-ai/dsh-llm` for a
|
||||
* string, and a non-TeamError rejection simply has no code to route on.
|
||||
*/
|
||||
function errorCodeOf(error: unknown): string | undefined {
|
||||
if (typeof error !== 'object' || error === null) return undefined
|
||||
const code = (error as { code?: unknown }).code
|
||||
return typeof code === 'string' ? code : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the Agent Teams write routes bound to the plugin context.
|
||||
* @param ctx - host plugin context.
|
||||
*/
|
||||
export function buildTeamsApi(ctx: Context): SidebarTeamsRoutes {
|
||||
@@ -74,13 +90,12 @@ export function buildTeamsApi(ctx: Context): SidebarTeamsRoutes {
|
||||
|
||||
/**
|
||||
* The live lead agent of the requested tree plus its membership check.
|
||||
* Throws 503 team-error when the experimental layer is absent (a mutation
|
||||
* can never be accepted there); returns undefined when the root simply
|
||||
* leads no team (a normal page state).
|
||||
* Throws 503 when the experimental layer is absent and 404 when the root
|
||||
* simply leads no team (a normal page state, but never for a mutation).
|
||||
*/
|
||||
const membershipOf = async (
|
||||
payload: unknown,
|
||||
): Promise<{ svc: SidebarAgentTeamsService; agent: unknown } | undefined> => {
|
||||
): Promise<{ svc: SidebarAgentTeamsService; agent: unknown }> => {
|
||||
if (teams === undefined) {
|
||||
throw new SidebarError('team-error', 'the agent-teams layer is not mounted in this deployment', 503)
|
||||
}
|
||||
@@ -88,56 +103,52 @@ export function buildTeamsApi(ctx: Context): SidebarTeamsRoutes {
|
||||
const agent = agents?.get(rootSessionId)
|
||||
if (agent === undefined) {
|
||||
// The root is not live in this process (cold session / other harness):
|
||||
// teams are live-led by definition, so there is nothing to show.
|
||||
return undefined
|
||||
// teams are live-led by definition, so there is nothing to mutate.
|
||||
throw new SidebarError('team-error', 'the tree root is not live in this process', 404)
|
||||
}
|
||||
try {
|
||||
if (await teams.tryMembership(agent) === undefined) return undefined
|
||||
} catch {
|
||||
return undefined
|
||||
if (teams.tryMembership(agent) === undefined) {
|
||||
throw new SidebarError('team-error', 'the tree root leads no team', 404)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof SidebarError) throw error
|
||||
throw new SidebarError('team-error', error instanceof Error ? error.message : String(error), 404)
|
||||
}
|
||||
return { svc: teams, agent }
|
||||
}
|
||||
|
||||
/** Service rejections become a 400 team-error; CAS conflicts ride the union. */
|
||||
const serviceError = (error: unknown): SidebarError =>
|
||||
new SidebarError('team-error', error instanceof Error ? error.message : String(error), 400)
|
||||
/**
|
||||
* Translate one service rejection: a stale revision keeps its own wire code
|
||||
* so the client's task window can say "someone else changed this task"
|
||||
* instead of showing a generic failure.
|
||||
*/
|
||||
const serviceError = (error: unknown): SidebarError => {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
return errorCodeOf(error) === STALE_REVISION_CODE
|
||||
? new SidebarError('team-conflict', message, 409)
|
||||
: new SidebarError('team-error', message, 400)
|
||||
}
|
||||
|
||||
return {
|
||||
async view(payload) {
|
||||
if (teams === undefined) return { available: false }
|
||||
const membership = await membershipOf(payload)
|
||||
if (membership === undefined) return { available: true, team: null }
|
||||
try {
|
||||
const team = await membership.svc.remoteView(membership.agent)
|
||||
return { available: true, team }
|
||||
} catch (error) {
|
||||
throw serviceError(error)
|
||||
}
|
||||
},
|
||||
async taskCreate(payload) {
|
||||
const membership = await membershipOf(payload)
|
||||
if (membership === undefined) {
|
||||
throw new SidebarError('team-error', 'the tree root leads no team', 404)
|
||||
}
|
||||
const record = payload as Record<string, unknown>
|
||||
const blockedBy = stringArrayOf(record.blockedBy)
|
||||
const writeScopes = stringArrayOf(record.writeScopes)
|
||||
const req: SidebarCreateTeamTaskRequest = {
|
||||
subject: requireString(payload, 'subject'),
|
||||
description: typeof record.description === 'string' ? record.description : '',
|
||||
...(stringArrayOf(record.blockedBy) !== undefined ? { blockedBy: stringArrayOf(record.blockedBy) } : {}),
|
||||
...(stringArrayOf(record.writeScopes) !== undefined ? { writeScopes: stringArrayOf(record.writeScopes) } : {}),
|
||||
...(blockedBy !== undefined ? { blockedBy } : {}),
|
||||
...(writeScopes !== undefined ? { writeScopes } : {}),
|
||||
}
|
||||
try {
|
||||
return await membership.svc.remoteCreateTask(membership.agent, req)
|
||||
return { ok: true, value: await membership.svc.createTask(membership.agent, req) }
|
||||
} catch (error) {
|
||||
throw serviceError(error)
|
||||
}
|
||||
},
|
||||
async taskUpdate(payload) {
|
||||
const membership = await membershipOf(payload)
|
||||
if (membership === undefined) {
|
||||
throw new SidebarError('team-error', 'the tree root leads no team', 404)
|
||||
}
|
||||
const record = payload as Record<string, unknown>
|
||||
const expectedRevision = record.expectedRevision
|
||||
if (typeof expectedRevision !== 'number' || !Number.isSafeInteger(expectedRevision) || expectedRevision < 0) {
|
||||
@@ -147,18 +158,20 @@ export function buildTeamsApi(ctx: Context): SidebarTeamsRoutes {
|
||||
if (typeof action !== 'string' || !TASK_ACTIONS.has(action)) {
|
||||
throw new SidebarError('bad-request', 'missing or invalid "action"')
|
||||
}
|
||||
const blockedBy = stringArrayOf(record.blockedBy)
|
||||
const writeScopes = stringArrayOf(record.writeScopes)
|
||||
const req: SidebarUpdateTeamTaskRequest = {
|
||||
taskId: requireString(payload, 'taskId'),
|
||||
expectedRevision,
|
||||
action: action as SidebarUpdateTeamTaskRequest['action'],
|
||||
...(typeof record.subject === 'string' ? { subject: record.subject } : {}),
|
||||
...(typeof record.description === 'string' ? { description: record.description } : {}),
|
||||
...(stringArrayOf(record.blockedBy) !== undefined ? { blockedBy: stringArrayOf(record.blockedBy) } : {}),
|
||||
...(stringArrayOf(record.writeScopes) !== undefined ? { writeScopes: stringArrayOf(record.writeScopes) } : {}),
|
||||
...(blockedBy !== undefined ? { blockedBy } : {}),
|
||||
...(writeScopes !== undefined ? { writeScopes } : {}),
|
||||
...(typeof record.owner === 'string' ? { owner: record.owner } : {}),
|
||||
}
|
||||
try {
|
||||
return await membership.svc.remoteUpdateTask(membership.agent, req)
|
||||
return { ok: true, value: await membership.svc.updateTask(membership.agent, req) }
|
||||
} catch (error) {
|
||||
throw serviceError(error)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ export type SidebarErrorCode =
|
||||
| 'git-error'
|
||||
| 'job-error'
|
||||
| 'team-error'
|
||||
| 'team-conflict'
|
||||
| 'sidechat-error'
|
||||
| 'subagents-unavailable'
|
||||
| 'settings-rejected'
|
||||
|
||||
Reference in New Issue
Block a user