Skip to content
Registry

kanban

v0.1.21

File-backed kanban board — tickets, threaded comments, agent assignment, the kanban:change and kanban:comment trigger types, and an injectable console board with agent profiles for planning and review.

license: Apache-2.0Used in 1 kitagentsboardconsole-uikanbantickets

Install

bash
iii trigger compose::add worker=kanban@0.1.21

Run in your project with the iii engine running. Exact versions are immutable and digest-pinned.

skill doc

skills/ade-worker-design/patterns.md

Console UI patterns for record-shaped workers

Most worker UIs manage a collection of records (tickets, jobs, documents, rows). These recipes encode the decisions that make such a UI read as native to the Console. Every snippet uses for the worker name; scope every selector under [data-iii-ui=""] and use only tokens.

Choose the pattern before writing JSX:

Need Pattern
Many records grouped by a status/stage, moved between groups Board (§1)
One record with a body, properties, and history Record screen (§2), opened as its own pane (§3)
Comments, replies, and system events on a record Activity timeline (§4)
Creating a record Creation modal (§5) — the only modal in the flow
Showing a record the agent touched in chat Chat card (§6)
Worker settings Settings form (§7)
Multi-user consistency Live updates (§8)

A detail view is never a modal and never a drawer: it is a screen.

1. Board (lanes)

Structure: PageShell → PageHeader (title, N records · description, one primary action such as New ticket) → PageMain → a slim toolbar (filter Input, optional count hint) → a horizontally scrolling row of lanes.

  • Lane = one surface step: background: var(--color-surface), radius var(--radius-md), flex: 1 0 220px; min-width: 220px; max-width: 360px, column flex with min-height: 0 and the body overflow-y: auto. Never frame a lane with a border; the fill is the region.
  • Lane header (36 px): one 16 px status glyph, 12.5 px/600 sans label, tabular mono count in --color-ink-ghost, and an IconButton “New in ” that is opacity: 0 until :hover/:focus-within (always visible under @media (hover: none)).
  • Card = one step above the lane: background: var(--color-panel-raised) with a hairline ring box-shadow: 0 0 0 1px var(--color-edge); hover swaps the ring for var(--shadow-raised); :focus-visible uses 0 0 0 2px var(--color-rule-focus). Padding 9–11 px, gap 8 px. Line one: 13 px/500 title. Line two: mono key, a Chip only for notable levels (e.g. high / urgent), a spacer, and a 20 px initials avatar. Show every fact on the record screen instead of turning the card into badge soup.
  • Card semantics:
    ; Enter and Space open it.
  • Empty lane: a quiet full-width button “No tickets — add one” in --color-ink-ghost (hover --color-surface-hover). Empty board: EmptyState with the primary action.
  • Loading: skeleton lanes with Skeleton cards so the silhouette is stable. Error: StatusPanel variant="alert" plus a Button variant="pill" retry.

Drag and drop

Use native HTML5 drag and drop; no library.

  • onDragStart on the card: effectAllowed = 'move', setData('text/plain', key), measure the card height, and set the drag state in a timeout so the browser captures the drag image before the card dims.
  • The lane handles onDragOver (preventDefault, compute the insertion index from card midpoints, skipping the dragged card), onDragLeave (ignore when relatedTarget is inside the lane), and onDrop.
  • Never remove the dragged card from the DOM. Keep it in place with data-dragging (opacity: 0.35); a card removed from the list disappears for good if dragend never arrives. Also clear the drag state from document-level dragend/drop listeners.
  • Show the landing position as a placeholder slot, not a line: an empty block the height of the dragged card with background: var(--color-surface-selected) and the card radius. Tint the target lane --color-surface-hover.
  • Apply the move optimistically with the same ordering rule as the server (midpoint between neighbours; append when dropped last), call the move function with { id, status, index }, and refetch only on error. Reordering inside a lane should not create timeline noise; a lane change should.
  • Headless test harnesses do not fire HTML5 drag events from synthetic pointer moves: verify by dispatching DragEvents (dragstart, dragover, drop, dragend) with a DataTransfer in browser::evaluate.

Narrow panes

Below the width where two lanes fit (~640 px of pane), show one lane at a time behind Tabs → TabsList variant="line" → TabsTrigger icon={false} (five tabs in a phone-sized pane is a documented space constraint) with a mono count after each label; drop the lane header, let the lane fill the width, and lift cards to 44 px. Remember the active lane per paneId.

2. Record screen

Structure: PageShell → PageHeader (title = record title, description = mono key, actions = optional Back when rendered inline, “Reference in chat” (host.chat?.compose), Delete as an IconButton that warms to --color-alert on hover) → PageMain with overflow: auto → a centered column (max-width: 1080px, padding 18–24 px).

Use a named-area grid so the narrow order is a reflow, not a rewrite:

[data-iii-ui="<worker>"] .<worker>-record__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 268px;
  grid-template-areas: 'head props' 'body props' 'activity props';
  column-gap: 32px; row-gap: 26px; align-items: start;
}
[data-iii-ui="<worker>"] .<worker>-record[data-narrow] .<worker>-record__grid {
  grid-template-columns: minmax(0, 1fr);
  grid-template-areas: 'head' 'props' 'body' 'activity';
}
  • Masthead (head): mono key, status Badge, priority Chip, then the 18 px/600 title (letter-spacing: -0.01em). Rename inline: a pencil IconButton revealed on hover swaps the heading for an Input; Enter or blur saves, Escape cancels.
  • Properties rail (props): a --color-surface block of label/value rows (grid-template-columns: 76px minmax(0, 1fr), 34 px tall, 11.5 px/500 faint labels). Controls are inline: Select appearance="inline" for finite choices, Selector with onCreate/allowEmpty for people, with relative text and the absolute value on title, the UUID in 11 px mono ghost. Sticky on wide panes; a two-column grid on narrow ones.
  • Body (body): a section with a 13 px/600 heading row and an Edit ghost button; read mode renders Markdown, edit mode renders CodeEditor language="markdown" with Save/Cancel in the heading row and ⌘↵ to save. Report a dirty draft through PageRenderProps.setDirty.
  • Sections separate by spacing (24–26 px) and heading weight, never by a rule.
  • Edits are per field and optimistic: call the update function on change, replace local state with the response, and on failure refetch and show a dismissible StatusPanel variant="alert" at the top of the screen. Do not ship a page-wide “Save changes” form for a record.
  • Delete goes through ConfirmDialog (cancel owns focus), never window.confirm. After a soft delete keep the screen open with a StatusPanel variant="warn" (“This ticket was deleted … its file stays on disk”) and a Restore pill; disable editing while deleted.
  • States: Skeleton blocks in the same grid while loading; EmptyState when no record is selected (“Open one from the board” with an action that opens the board page); StatusPanel + retry on load errors.

3. Opening a record as its own pane

Register two pages: the collection (-board) and the record (-ticket). From a card, a chat card, or a palette row:

host.panels?.open({ pageId: '<worker>-ticket', context: { type: 'ticket', id } })

The Console places the record page beside the caller in the same workspace tab, or reuses an open instance, and delivers PageRenderProps.panelContext ({ id, pageId, context }). In the record page:

  • derive the record id from panelContext.context and re-run the effect on panelContext.id so repeated clicks re-navigate;
  • persist the last id in localStorage under :ticket: so a reload keeps the pane meaningful;
  • render the “nothing selected” EmptyState when there is no id.

When host.panels is absent (older console), fall back to rendering the record screen inside the collection pane with a Back action. Keep the record screen a shared component so both routes look identical.

4. Activity timeline and comments

One chronological list (

    ) mixing system events and comments:

    • Event row: grid-template-columns: 16px minmax(0,1fr) auto; a 16 px glyph in --color-ink-ghost, then actor verb phrase in 12.5 px faint sans, then a mono 11 px relative time with the absolute time on title. Write the verb phrase from field changes with human labels (“moved this from To do to Done”, “assigned this to Ana”, “edited the description”) — never raw enum values.
    • Comment: a --color-surface card with a header row (20 px initials avatar, 12.5 px/500 author, mono relative time, a hover-revealed Reply ghost button) and a Markdown body in sans.
    • Replies nest under their parent in a --color-panel-raised card with a hairline ring, indented 14 px. Keep threads one level deep; replying to a reply attaches to the root.
    • One composer at the bottom of the timeline on a raised surface: a scoped