The task registry has run every background bash, pwsh, pty-send, and one-shot subagent since it landed, but only the model could read it: a human at the Web client could not see that a build was running, tell a finished task from a stuck one, or find its outcome anywhere but the `run_in_background` tool card that printed an id and never updated. Task state now reaches the browser as one whole-snapshot `session/tasks` mux frame per session, pushed at every registry commit that changes what that session can see. `TaskService` gains `onTasksChanged`, which is owner-granular because owner-disposal removal is a change no per-task record can express. The carrier reads the exact owner the listener hands it, so a push stays correct while that scope tears down, and reads the baseline through the non-resuming `ctx.agents.get` so listing never revives a cold session. The client keeps a last-wins mirror on `SessionListState`, and a new `dsh-client-ui-task` package renders it beside the subagent catalog — rendering nothing at all until the session has a task, so an ordinary conversation grows no new chrome. Streamed per-task output and human-initiated cancellation are separate phases; the note records why neither has to undo this channel, and why no Web path may call the consuming `ctx.tasks.read()`.
@deepseek-ai/dsh-client-ui-task
English | 中文
Web background-task feature owner: contributes one entry to conversation.session.header.actions listing the ctx.tasks records this session can see. The data arrives entirely through the tasksBySession list mirror that dsh-client-runtime folds from session/tasks frames, so this package issues no RPC and holds no state beyond popover visibility.
The trigger renders only when the session has at least one task, so an ordinary conversation never grows a control for a capability it is not using. Its badge counts running plus stopping and is omitted at zero, leaving a session that holds only finished tasks a quiet entry point into its history rather than one advertising a count of nothing. The popover is a flat list: live rows first by startedAt ascending, then settled rows by finishedAt descending, with a same-millisecond tie broken on start order so the host's map iteration never decides it. A row shows the producer kind, the label, a status marker, the producer's detail in place of the generic status word once it has one, and an elapsed duration. That duration advances once per second while the row is live and freezes at finishedAt; the clock runs only while an open list holds something that moves. A settled row missing finishedAt reads as zero rather than as a negative figure, and a duration past an hour stays in hours rather than growing a day vocabulary no producer currently reaches.
Settled rows stay visible and de-emphasized until the registry drops them at owner disposal. They are in the snapshot, a failed task's detail is the only place its failure is legible, and filtering them out here is work the output and cancellation phases would undo. A running one-shot background subagent therefore appears both here and in the subagent catalog: the catalog navigates into the child's transcript, while this list is the only handle a future cancellation can attach to.
Escape closes the list and returns focus to the trigger, as does a pointer press outside it. The last task disappearing closes the list before the control unmounts, so focus never vanishes from a removed node. Styling uses tokens only; copy goes through the package's own task locale namespace. The behavior is specified by the Web background-task display Agent Note.
Model Experience
None, as this package renders host-computed registry state for a human and touches no prompt, message, schema, stream, or tool result. The model's own view of the same tasks stays with dsh-tool-tasks.
KV Cache effect
None; the package never assembles or sends provider requests.
Known Limitations and Deferred Work
- Rows are read-only — a task's streamed output and a human-initiated cancellation are separate phases. Cancellation additionally owes a model-facing decision the seam does not answer today:
kill()marks terminal delivery reported, so an interrupt written against the current contract would leave the model believing its task is still running. - The list is not the registry's own set — it shows what the owning session can see through the wire view, so an unowned task (one started without a live
Agent) is invisible here whiletask_liststill reports it to the model.