tasty.gallery
Surfaces · Task DAG
← Design
Specs
Theme

Task DAG

Follow an agent task graph in a tab or workspace popup. Both views use the same canvas to show progress. These views are for inspection; create and edit tasks through the CLI.

This page uses a saved copy of the design system, last changed on . It may differ from the current design source.

When to use it
Read the usage notes to choose a component for your task.
Design tokens
See which tokens each example uses and what they control.
Check dimensions
Turn on Specs at the top right to show the 4px grid and measurements.

Graph canvas

Task DAG canvas — read-only observation

The one canvas both surfaces embed. Nodes are laid out top-down by dependency layer, edges are orthogonal with 4px elbows. This is not a node editor: there are no ports, no sockets, no drag-to-move — the only gestures are pan (drag empty canvas or middle-drag), zoom (Ctrl + wheel; bare wheel pans, because a terminal app reads bare wheel as scroll), select (click a node), and Esc to clear.

Layout spec
node box
168 × 48 (fixed at every zoom)
layer gap
32 along flow · 24 across
zoom
20% – 150%, 10% steps
fit
auto on open, capped at 100%
background
dot grid · 16px
poll
0.5s — colour only, never re-layout
Tokens used
dag-canvas-bg— canvas beddag-canvas-dot— grid markdag-layer-gap— 32 flow depthdag-sibling-gap— 24 acrossdag-edge-corner-radius— elbow

Position stability is the contract that makes a 0.5s poll survivable: layout is computed from ids + dependency edges only — never from status, duration or counts — so a task turning running → succeeded repaints one card and moves nothing. Layering is longest-path and capped by node count, so a cyclic graph still terminates and still draws.

Never add a connect handle, a port dot, or a drag-to-move affordance. Anything that looks editable promises an edit the host cannot honour — DAGs are built by agents over CLI/IPC.

Node card · 8 states

Task node — every execution state

One card = one task. Three channels carry state so colour is never alone: the leading 3px bar (position), a mono glyph (◦ ❯ ◑ ✓ ✗ − ⊘ ?), and the spelled label. running is the loudest — accent border + accent wash — and it is static: the 0ms terminal-motion policy holds, so there is no spinner and no pulse on the card.

Layout spec
card
168 × 48 · radius 4
padding
4 / 8 · row gap 4
status bar
3px, full height
name
13 medium, 1 line, ellipsis
meta row
10 caps · duration mono, right
dimmed
skipped + cancelled at 0.4
Tokens used
dag-status-waiting— waitingdag-status-ready— readydag-status-running— runningdag-status-succeeded— succeededdag-status-failed— faileddag-status-cancelled— cancelleddag-status-skipped— skippeddag-status-unknown— unknown

Colour vs. text: the state colour is the recede channel — bar, border, glyph on a chip. The spelled label reads from its own --tasty-dag-status-<state>-label role: waiting takes --tasty-text-muted, and the two dimmed states take --tasty-text-secondary, which still measures 4.74:1 after the 0.75 card dim.

Truncation rule (i18n): the task name is a single line with ellipsis at the card edge — never wraps, never shrinks the font; the full name lives in the tooltip and the detail panel. On the meta row the status label yields first: it ellipses before the duration is dropped, so a long German or Japanese state string can't push the timing out of the card.

Task kinds

Five task kinds, told apart by the leading glyph: run (shell), custom (IPC call), reduce (result fan-in), wait_barrier (gate), agent (one Claude / Codex turn, 2026-10-07). Claude and Codex share the agent glyph; the provider is a detail-panel field, not a second shape.

Layout spec
glyph
14px, muted, leading
run
terminal
custom
plug
reduce
layers
wait_barrier
lock
agent
agent (new) · stroke = icon-stroke-width like the rest · detail kind label “Agent”
Tokens used
dag-node-meta-fg— kind glyph

New glyph: icons/agent.svg — a speech bubble carrying the prompt chevron (an agent turn is a conversation that runs commands). terminal, plug, layers and lock carry the other four. A dedicated barrier glyph would read better than lock if one is ever drawn.

Level of detail · selection · overflow

Zooming out never changes the box, only its content: full (≥70%) name + status + duration · compact (≥40%) glyph + name · block (<40%) status fill only. At 200 nodes the block tier keeps every state readable as colour while the graph shape stays legible.

Layout spec
full
zoom ≥ 0.7
compact
0.4 – 0.7
block
< 0.4
selection
2px accent outline, 1px offset
hover
derived overlay-hover on the body
Tokens used
dag-node-selected-ring— selectiondag-node-hover-bg— hover washdag-node-dim-opacity— skipped path

55 nodes, auto-fitted on open — the canvas lands in the compact or block tier and says so in the bottom-left hint chip, so a user who sees colour blocks knows why and knows the way back.

Running phase · unknown reason (2026-10-07)

Running phase — only awaiting_input gets its own look

Typed (v2) tasks report a step while running. executing is the plain running card. awaiting_input stops the graph on a person, so it swaps the whole state look to the needs-input yellow — bar, border, wash, glyph ! and label — and stays readable at the compact tier (glyph + name). postprocessing and retry_wait keep the running tone and only swap the label, with the run number.

Layout spec
executing
no change — RUNNING
awaiting_input
! · NEEDS INPUT · yellow · duration = time waiting
postprocessing
◑ · POSTPROCESS · RUN n
retry_wait
◑ · RETRY WAIT · RUN n
v1 tasks
never have a phase → plain running
Tokens used
dag-phase-awaiting— bar · borderdag-phase-awaiting-bg— card washdag-phase-awaiting-label— glyph + label

Labels (i18n). needs input · 입력 대기 · 入力待ち — postprocess · 후처리 · 後処理 — retry wait · 재시도 대기 · 再試行待ち — run {n} · {n}회차 · {n}回目. The meta row is caps for Latin scripts only.

Click. Selecting an awaiting node opens the detail panel with a yellow notice and an Open session button (secondary sm) that focuses the agent session surface. It is a user action only; agents never trigger it. The DAG list row adds a mono ! 1 needs input before the rollup chip; the rollup itself stays running.

Hover — the why-line for skipped, unknown and awaiting

The node tooltip is name — label plus one why-line. Unknown also says what unblocks it. The same text sits in the detail panel under Why unknown.

Labels (i18n). Why: · 이유: · 理由: — Why unknown · 알 수 없는 이유 · 不明の理由 — “Retry or cancel it to let the graph continue.” · “다시 실행하거나 취소해야 그래프가 이어집니다.” · “再実行またはキャンセルするとグラフが続行します。” — “Waiting for a person in the {provider} session” · “{provider} 세션이 사람의 입력을 기다리는 중” · “{provider} セッションが入力を待っています”. The reason text is the host's free text, shown as is.

Edges · 5 relations

Dependency edges

Relation is carried by dash pattern and colour together: depends_on solid neutral, fallback long-dash attention, reduce fine-dash info, binding dash-dot data (teal, 2026-10-07), transition long-dash route (lavender, 2026-10-07). When a pair has both depends_on and binding, only the binding is drawn — a binding already implies the order. A one_of binding draws one binding edge per source. Every edge ends in an arrowhead at the dependent task, so the direction reads as "waits for".

Layout spec
routing
orthogonal, 4px elbow
width
1px
fallback dash
6 3
reduce dash
2 3
binding dash
8 2 2 2
transition dash
10 4
same pair
binding replaces depends_on
detail label
binding → “binds input” · transition → “transition”
arrow
8px triangle at target
selected node
its edges take the accent
Tokens used
dag-edge-depends— depends_ondag-edge-fallback— fallbackdag-edge-reduce— reducedag-edge-binding— binding → accent-datadag-edge-transition— transition → accent-routedag-edge-dim-opacity— dead path

Dim the dead path: an edge leaving a failed/cancelled task, or entering a skipped one, drops to 0.4 along with the node. The failure stays loud; everything it stranded recedes.

Transitions · not selected (2026-10-07)

Transition selection — four states on one edge style

A transition edge always keeps its lavender long dash; its selection changes weight and opacity only. pending (producer not finished) = 1px, full opacity. selected = 2px (--tasty-dag-edge-selected-width). not_selected = 1px at --tasty-dag-edge-dim-opacity. unavailable (producer failed) = the existing dead-path dim, like any edge leaving a failed task. Nothing is hidden: the unchosen route stays readable.

Layout spec
pending
1px · full opacity
selected
2px · dag-edge-selected-width
not_selected
1px · dim 0.4
unavailable
dead-path dim, as from a failed task
not-selected node
skipped look (dimmed card, ⊘ glyph — canonical for both skip reasons; − is cancelled) · label NOT SELECTED · tooltip = kit nodeTitle(): “{name} — not selected” + “Why: Not selected by the upstream result” (2026-10-09)
upstream-unavailable node
skipped look · label SKIPPED · tooltip = nodeTitle(): “{name} — skipped” + “Why: An upstream task did not succeed”
rollup
succeeded + not selected only → succeeded
skip count
DAG list row + DAG tab header, after done/total: “{done} / {total} · {n} skipped ({k} not selected)” — shown only when n > 0, parenthesis only when k > 0 · same mono caption, dag-row-count-fg · not clickable
filter
no new list filter
legend
none — the relation reads from the dependency rows in Node detail; the canvas carries no legend
Tokens used
dag-edge-transition— routedag-edge-selected-width— → focus-ring-width 2dag-edge-dim-opacity— not selected / unavailabledag-status-skipped— both skip reasons

A branch that was not taken is a normal outcome, so it gets no new colour: it shares the skipped card and differs only in its spelled label and tooltip. i18n: dag.status.not_selected “not selected” · dag.why.branch_not_selected · dag.why.upstream_unavailable (2026-10-09 — replaces the retired dag.skip.* keys) · dag.rel.binding “binds input” · dag.rel.transition “transition”.

Canvas chrome

Zoom cluster + minimap

Bottom-right overlay, 8px off the canvas edge: minimap above, zoom cluster below (− · % · + · fit · direction). Below 560px of surface width the minimap is dropped; below 400px the percentage readout goes too, leaving five 28px targets.

Layout spec
cluster
28px row · 1px border
inset
8px from canvas edge
minimap
160 × 112
viewport
1px accent rect
minimap cutoff
surface < 560px
compact cutoff
surface < 400px
Tokens used
dag-chrome-bg— cluster filldag-minimap-viewport— viewport rectdag-minimap-min-surface— 560 cutoff

The minimap paints each node in its status colour, so it doubles as a health strip: a red block anywhere in the graph is visible without zooming out.

Runner badge

Host runner state

Four values in one pill: on/off, crashed, ready_count, running_count. The case that must be noticed is runner stopped with work ready — nothing is advancing the graph — so it takes the warning tone and carries the resume hint beside it.

Layout spec
pill
22px · radius 2 · 1px border
dot
StatusDot, pulses only while work runs
counts
mono 11
hint
hidden on a narrow header
Tokens used
dag-runner-stalled-fg— stopped + readydag-runner-crashed-fg— crasheddag-runner-idle-fg— stopped, no work

"Stopped with no work" is not a warning — it is the resting state of a finished graph, so it stays muted. Only stopped-with-ready earns the yellow.

DAG list row

One row per DAG — across all workspaces

A ListCtrl row (36px, icon + label + description) plus the one new part: a trailing cluster of origin tag, rollup state (same 8-state vocabulary as a task) and a mono done/total. Progress is text, not a bar — at this density 7/12 is exact where a 60px bar is only a suggestion.

Layout spec
row
36px min · ListCtrl density
label
13 — DAG name
description
11 — workspace · updated
trailing
origin tag · rollup · n/total · skip count when > 0
scope
all workspaces, filterable
Tokens used
dag-row-height— 36 rowdag-row-count-fg— n/totallistctrl-row-bg-hover— hover

Rollup (2026-10-07). Seven values, in filter order: waiting · ready · running · succeeded · partially_failed · failed · skipped. partially_failed (◒, peach --tasty-dag-status-partially-failed) = no more progress, an unrecovered failure, and at least one branch succeeded to its end. Precedence is progress first: running → ready → waiting-that-can-run → partially_failed | failed → succeeded | skipped. dagRollup(DAG_PARTIAL.nodes) = partially_failed. Its own filter option, between succeeded and failed. done/total counts its failed tasks as done, same as failed. Label: Partially failed · 일부 실패 · 一部失敗.

The workspace name is part of the description line, not a separate column — the popup lists every workspace, and a column would waste the width the DAG name needs.

Node detail

Selected task — side panel · bottom sheet

Selecting a node opens a 288px right panel on a wide surface and a 220px bottom sheet when the surface is narrow (< 640px) or inside the popup. Failure output can run to dozens of lines, so the error tail is its own bounded, scrollable, copyable block — never an expanding wall that pushes the graph off screen.

Layout spec
panel
288 wide · 12 padding
sheet
220 tall (narrow)
error tail
max 160, scrolls, copy button
output tail
max 112, scrolls
deps
22px rows, click to jump
Tokens used
dag-detail-bg— panel filldag-detail-log-bg— log beddag-detail-log-max-height— 160 error tail

Dependency rows are the graph's keyboard-free navigation: each row shows the upstream task's glyph + relation and jumps the selection there, so a failure can be walked back to its cause without hunting on the canvas.

Empty · cycle

Empty states and the cycle warning

Three non-happy states: no DAGs in this workspace (surface), no filter match (popup list), and cycle detected — a warning strip pinned to the canvas top. The cyclic graph keeps rendering behind it: this surface observes, it never hides state.

Layout spec
banner
28px · pinned to canvas top
banner copy
names the cycle path
empty A
surface — how a DAG appears
empty B
search — echoes the query
Tokens used
dag-cycle-bg— banner washdag-cycle-fg— banner texttext-disabled— empty glyph

DAG list popup — empty and no match (2026-10-10 b12)

The popup list uses the same DagEmpty as the surface (one shared function): glyph, title, hint, stacked in the vertical centre of the list area (not the 1/3 point). No match is the search variant and echoes the query. No DAG at all is its own popup variant: the popup spans every workspace, so the surface hint with a workspace-scoped command does not fit; it keeps the app's two strings.

Layout spec
shared
DagEmpty — surface · search · popup
position
centred in the list area, both axes
no match
search glyph · “No DAGs match “{q}”” · “Clear the filter or widen the scope to all workspaces.”
no DAGs
gitTree glyph · “No DAGs yet” · “Tasks grouped by dependency appear here.” (existing keys)
glyph
24 · text-disabled
Tokens used
text-disabled— glyphtext-secondary— titletext-muted— hint

i18n: the search title is new — dag.popup.no_match “No DAGs match “{q}”” / ““{q}”와 일치하는 DAG 없음” / ““{q}” に一致する DAG はありません” · hint dag.popup.no_match_hint “Clear the filter or widen the scope to all workspaces.” / “필터를 지우거나 범위를 모든 워크스페이스로 넓히세요.” / “フィルターを消すか、範囲をすべてのワークスペースに広げてください。”. The old “No matching DAGs” / “Try a different filter.” go.

DAG list popup — no match without a query (2026-10-12)

The list can empty with no search text: the status filter or This workspace only alone filters everything out. There is no query to echo, so this is its own filtered variant: filter glyph (the cause is a filter, not the search field), a title without quotes, and the same hint as the search variant. Variant choice: trimmed query non-empty → search (echo the query only, even when status/scope filters are also on — the hint already says “clear the filter”); trimmed query empty and filters active → filtered; no DAG at all → popup.

Layout spec
variant
DagEmpty filtered
when
query.trim() empty · status filter ≠ all OR workspace-only on · 0 rows
glyph
filter · 24 · text-disabled
title
“No DAGs match the current filters”
hint
same key as search: dag.popup.no_match_hint
query + filters
search variant, echoes the query only
Tokens used
text-disabled— glyphtext-secondary— titletext-muted— hint

i18n (new): dag.popup.no_match_filtered “No DAGs match the current filters” / “현재 필터와 일치하는 DAG 없음” / “現在のフィルターに一致する DAG はありません”. Hint reuses dag.popup.no_match_hint. The temporary dag_list.empty_filtered* (“No matching DAGs” / “Try a different filter.”) can now be removed.

Surface · popup

Full-tab surface — wide and narrow

A whole terminal tab. Header: DAG name + {done} / {total} done (spaces around the slash in every language, 2026-10-09) (mono caption, --tasty-dag-header-count-fg = the list row counter, skip suffix after it, 2026-10-07), the DAG Select, the runner badge, refresh. Under 640px the header wraps to two rows (identity above, controls below), the runner hint drops, the minimap goes, and the detail panel becomes a bottom sheet.

Layout spec
header
8/12 padding · wraps < 640
canvas
all remaining space
detail
288 side → 220 sheet
narrow floor
320px, nothing clipped
DAG switch
instant swap + auto-fit
Tokens used
bg-sidebar— header bandseparator— header hairlinedag-detail-sheet-height— 220 sheet

Switching DAG in the header Select replaces the canvas instantly (0ms) and re-fits — a new graph has no position to preserve, so fit is the honest default. Within one DAG the view never re-fits on its own.

Workspace popup — list ⇄ single DAG

A movable 560 × 460 popup. The list carries search, a this workspace only toggle and a status filter; picking a row swaps the whole area for that DAG's canvas via DrillDown — instant, with a back bar that keeps the runner badge in view. The bottom sheet holds node detail here, because 560px has no room for a side panel.

Layout spec
frame
560 × 460 · resizable
titlebar
28px drag strip
list rows
36px · ~8 visible
swap
DrillDown, 0ms
dismiss
outside click · Esc · ×
Tokens used
dag-popup-width— 560 framedag-popup-height— 460 framedrilldown-backbar-height— 36 back barshadow-modal— lift

Detail view — the header keeps the runner badge only

The popup's list → detail flow reuses the graph screen, which drags the graph header along: the runner badge, the DAG selector and the zoom cluster all arrive in a view that shows one node — and the back bar's actions slot sits empty beneath them. Settled: no second header. The back bar is the detail's chrome, and its actions slot carries the compact zoom cluster (beside the graph it scales) and the runner badge (it describes the run the node belongs to). The DAG selector is hidden: the back-bar title already names the DAG, and switching DAGs under an open node detail has no meaning. Back restores the list, its header, and the selection.

Layout spec
frame
560×460 — unchanged
detail header
none — the back bar is the chrome
back bar actions
compact zoom cluster + runner badge
DAG selector
hidden in detail
back bar
36 — --tasty-drilldown-backbar-height
back
restores the list, keeps the selection
close / reset
unchanged host contract
Tokens used
drilldown-backbar-height— back bardag-popup-width— 560 framedag-popup-height— 460 frame

The actions slot was empty in the current build; this fills it rather than inventing a second bar. If a future detail view needs no zoom, the slot simply goes back to empty.