Review found the `onTasksChanged` teardown reasoning inverted. The comment claimed every registration is an effect on the registry's own fiber, so listeners would be gone before service disposal empties the store — but the traceable proxy rebinds `this.ctx` to the CALLER, which this package's own HMR-safety test already proves. The only shipped consumer registers from the api-proxy mux stream, so it was still listening and simply kept the rows it last received. Service disposal now announces the emptied set, and teardown announces its stopping transition immediately instead of leaving an observer on `running` for however long a slow producer takes to release. Two documentation claims were false in the opposite direction: the Agent Note and the ui-task README both said an unowned task is invisible in the header, while `list(caller)` returns unowned tasks to every caller, the carrier fans their changes out to every subscribed session, and this PR's own test asserts exactly that. The note even contradicted itself two sections earlier. Both sides now state the real asymmetries — another session's tasks, and the process-local registry emptying on restart. The "no Web path calls the consuming `ctx.tasks.read()`" invariant claimed a test that did not exist; the carrier suite's producer had no `readOutput` at all, so a stray read would have failed nothing. Its producer now counts cursor consumption and the lifecycle and baseline paths both assert zero. Also: a session created after the mux opened now receives the task baseline it missed, the popover samples its clock when it opens rather than at mount, and a failed task's unbounded producer detail elides instead of widening the row.
3.6 KiB
@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 one session can see through the wire view, so a task owned by another session never appears here, and a process restart empties the list while the transcript keeps the
run_in_backgroundcards that started those tasks. An unowned task (one started without a liveAgent) is the opposite case: it reaches every session's list, matching whatlist(caller)reports to every caller.