Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ driga inbox --since yesterday

Service installation starts a per-user collector. The first import marks older
events as already seen. Reading an inbox item never acknowledges it; use
`driga inbox ack <item-id>` or the Inbox controls in the TUI.
`driga inbox ack <item-id>` or the Flow Activity controls in the TUI.

Use `driga web` for the local browser dashboard, `driga top --project .` to focus
on the current project, and `driga <command> --help` for command-specific flags.
Expand Down
107 changes: 107 additions & 0 deletions docs/specs/2026-10-23-flow-board-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Flow — Final v2

Flow monitors stored agent work and resolves interruptions. The approved Final v2
Map and Timeline replace the previous timeline board and family signal map.

## Navigation and ownership

Both interfaces show three destinations: **0 Flow**, **1 Projects**, **2 Machines**.
Flow opens on Map. The web segmented control and TUI `m` / `t` / `s` select Map,
Timeline and Sessions. In the terminal, Map uses the same project tree as Timeline.
`/` finds, arrows or `j`/`k` select, Enter opens context, and Esc returns. `M` changes
TUI machine scope. Sessions retains the existing sortable, searchable directory;
`S` sorts in the TUI. Projects owns repository/check-out evidence. Machines owns
health, process review and disk reclamation (`h` / `p` / `d` in the TUI).

Chat is a contextual panel on `c`, with the current session, project and machine.
Esc closes it and retains the draft and origin. Wide terminals keep the destination
beside Chat; narrow terminals use the available reading width for Chat. CLI command
names and schema-2 session/family APIs are unchanged.

Notification and retained event history live in Flow's expandable Activity section;
TUI `h` opens history. A completed turn is history, not a live wait or a session end.

## Shared model and honest evidence

`LoadBoardSnapshot` loads sessions, events, observation minutes, inbox spans and
session files in bulk for each scoped machine. `Board` projects Map and Timeline in
one pass, using a fixed last-hour display window. There is no per-session query loop.
Live waits, pending permissions and the state verdict produce one Needs you list,
oldest first; its length equals the waiting state count. Session identity includes
machine identity. Ended sessions cannot be revived by an old open inbox span.

Only stored `ParentID` establishes a spawn edge. Same-file overlap requires the
same full stored path on the same machine, with both sessions still live. Equal
basenames and equal paths on different hosts do not establish overlap. A shared
file uses a deterministic star, avoiding quadratic all-pairs edges.

Historical working slices require measured transcript/CPU activity or real tool
telemetry. A current verdict applies only at the right edge. Unknown history remains
neutral; recorded waits are amber and stalled state caps are hatched. Tool ticks
and counts require Claude `otlp:tool_result` or Copilot `postToolUse` events and are
labelled **telemetry**. Codex/Gemini observation growth never becomes a tool tick.
Cost appears only with stored cost metadata and is labelled **retained window**.
Missing usage, tools, files and agent badges are omitted. No handoff edges,
particles, streams, Sankey, replay or inferred outcomes are rendered.

## Map

The Go model produces SVG coordinates. Project/machine regions occupy a two-column
grid; height follows family/session count. Paired-machine regions are dashed. Leads
are ordered by start time and identity down the left column. Workers occupy the
adjacent column, with purple elbow spawn edges. Four workers are visible per family;
remaining workers fold into a `+N` control that opens Timeline. Timeline retains the
complete forest. Missing parents and cycles cannot hide sessions.

Nodes have radius 18, a state glyph, a small agent-letter badge and a title below.
Working is green `▶`, waiting amber `◆`, stalled red `‖`, idle blue `○`, and ended
muted `✓`. Stalled strokes are dashed; ended strokes are dotted and faded. Recent
stored activity controls fill opacity. Only waiting/stalled nodes show an age.
Waiting has a static amber ring; selection has a purple ring. Regions and coordinates
never depend on current activity or verdict, so refreshes do not move sessions.

Same-file links are red dashed lines with a filename pill. Nodes are focusable SVG
buttons with title/state/age/agent labels. Click and Enter select the shared inspector.
A legend explains glyphs, spawn, overlap, brightness and telemetry. The header shows
state counts, a small SVG events/minute sparkline and elapsed time since observation.
The empty state explains where the next session will appear.

## Timeline and inspector

Timeline is a project-grouped ARIA tree with roving tabindex and arrow-key navigation.
Rows show branch, state, title, agent, sixty minute slices, real telemetry ticks,
file-overlap markers and age. The selected family repeats the same evidence in a
compact tree in the inspector. The inspector conditionally shows now, files,
shared-file annotations, tools and retained-window cost.

Needs you stays above the family. The oldest item has a full wrapping command;
compact following rows and overflow preserve access to other waits. Allow/Deny use
the existing authenticated, CSRF-protected prompt-answer endpoint. A new permission
never takes focus. Buttons arm after 600 ms; terminal answers require explicit shelf
focus and the same review delay. Enter can open the full, scrollable prompt inspector.
Terminal uses the existing jump resolver; Open session uses the session inspector;
Ask Chat opens the panel with selected context. Process termination lives in Machines.

## Refresh and libraries

The console's existing SSE notifications trigger htmx fragment refreshes. Vendored
**Idiomorph 0.7.4**, including its htmx extension, supplies `morph:innerHTML` swaps;
its BSD Zero Clause license is recorded in `static/vendor/LICENSES.txt`. Stable DOM
IDs and client state preserve session identity, view, search, caret, keyboard focus
and scroll. No secondary JSON delta stream or client-rendered Map exists.

Lip Gloss v2 `tree` supplies terminal connectors, bubbles supplies the session table,
filter, text composer and inspectors, and the existing event-count spark renderer
supplies compact terminal history. No force-layout library or JS framework is added.
Motion is static by default and the CSS also honours reduced-motion preferences.

## Verification

Model tests cover input-order/activity-independent slots, worker folding at forty
sessions, complete Timeline membership, parent links, same-path scope, paired-region
styling, attention parity/order, unknown history and telemetry honesty. A 200-session
benchmark exercises the combined Map and Timeline model. Handler/CSP/escaping and
Node DOM tests cover refresh state, keyboard movement and prompt arming. A synthetic
loopback Playwright harness exercises the actual htmx/Idiomorph swap and captures
light/dark Map/Timeline and empty/quiet layouts. TUI goldens cover 80, 100, 140 and
200 columns, colour-independent geometry, and Windows newline normalization.
90 changes: 49 additions & 41 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ driga agents --json --project .
driga agents show <id> # numeric ID from the agents table or --json
driga projects show <project>
driga inbox # current waits and recent completions; "inbox list" also works
driga inbox --all # complete history, including acks and ended waits
driga inbox list --all # complete history, including acks and ended waits
driga inbox ack <item-id> # explicit acknowledgment
driga conflicts
driga orphans # "orphans list" also works
Expand Down Expand Up @@ -50,35 +50,44 @@ Hosted Codex threads use heuristic labels prefixed with `~`; a shared host proce
does not establish individual thread liveness. Unknown agent versions are parsed
tolerantly and reported by `driga doctor`.

`driga top` opens on Home (`0`): what needs you, agent states, machine health, the last
hour's activity, projects and leftovers. On terminals 140 columns or wider the Agents tab
shows the selected agent beside the list. `driga activity` prints the same 60-minute
history (machine samples and events per minute; minutes without a sample are gaps).

In the TUI, use `0`–`8` for tabs (Home, Agents, Projects, Inbox, Orphans, Machine, Cleanup, Flow, Chat), Enter for detail, Esc to return, `?` for help,
`m` to cycle machine scope, and `q` to quit. Viewing details and issuing JSON
queries never acknowledge inbox items. Use `x`/Space in Inbox or `driga inbox ack`. Without a running collector,
acknowledgments are queued for the next collection. Orphan kills recheck process
identity and the descendant set immediately before signaling; young candidates
are refused. `--force` escalates survivors to SIGKILL (TerminateProcess on Windows), with a second confirmation unless `--yes/-y` is supplied.
The Machine tab and web Machine page lead with agents' memory and CPU, session
costs, agent processes without a session, and likely work shared with an editor.
Sessions expand into processes with attribution proofs and their launch app;
Other apps starts collapsed around the existing top-ten memory and CPU lists.
`driga machines show --json` and MCP `get_machine` add the same buckets to schema 2.
Low-disk and sustained critical-memory-pressure warnings open Machine, with
pressure text naming agents' share and observed growth. Open it directly with
`driga top --tab machine`. See [machine health](machine-health.md) for rules,
measurements, JSON fields, and deferred editor integrations.

The default inbox puts current waits first, followed by up to 50 unacknowledged
completions from the last 24 hours. `--since` selects another completion window;
`driga inbox --all` removes the default window/count bound and includes acknowledged
and ended events. Waits for sessions that ended, moved on, or aged out of the
visible agents are labelled **historical wait** in history. Reading or hiding
history never acknowledges it. TUI waiting counts use the same visible session
states as Agents, including waiting sessions without a captured inbox event.
The web inbox has a **show all history** link and pages of at most 100 rows.
`driga top` opens on **Flow (`0`)**. The three tabs are `0 Flow`, `1 Projects`
and `2 Machines`. Flow offers Map (`m`, the default), Timeline (`t`) and Sessions
(`s`). Map and Timeline use a grouped project tree in the terminal; the web Map is
a deterministic SVG of projects, leads and workers. More than four Map workers
fold into a `+N` control. Timeline and Sessions retain access to every session.

Use arrows or `j`/`k` to select, `/` to find, Enter to open context, Esc to return,
`?` for help, `M` to change TUI machine scope, and `q` to quit. `S` sorts Sessions.
Flow's Needs you shelf is oldest first and has the same count as the waiting header.
Tab explicitly focuses it; `y`/`n` answer a pending permission after a short review
delay. Commands wrap in full; Enter opens a scrollable inspector. `J` opens the
selected session's terminal through its stored jump destination. `K` (web: Stop)
stops a local session after you confirm; Driga first rechecks that it is the same
idle process. Sessions on a paired machine must be stopped on that machine.

`c` opens contextual Chat as a panel. Esc closes it, retaining the draft and current
selection. Wide terminals show Chat beside the destination; narrow terminals give
it the available reading width. Web Ask Chat prefills the selected session context.
`Alt+0`/`Alt+1`/`Alt+2` switch TUI tabs while typing.

Flow's expandable Activity section keeps notification and event history; TUI `h`
opens history and returns to Flow. Reading never acknowledges events. Use `x`/Space
on an eligible history item or `driga inbox ack`; unresolved permissions require an
answer. `driga inbox list --all` includes acknowledged/ended history. The CLI commands,
JSON schemas and collection/capture policies are unchanged.

Projects owns repository and worktree context. Machines owns health and reclamation:
`h` health, `p` process review, `d` disk candidates. Process termination rechecks
identity and descendants; young candidates are refused. `--force` escalates
survivors with a second confirmation unless `--yes/-y` is supplied. Remote processes
must be handled on their machine. Machine session memory is labelled separately
from Flow's monetary cost.

Flow shows only stored evidence: purple spawn links use session ParentID; red dashed
same-file links require full paths on the same machine. Tool counts/ticks are labelled
**telemetry** and appear only for actual Claude/Copilot tool events. Cost is labelled
**retained window**. Unknown history remains neutral. `driga activity` still prints
machine samples and events per minute; missing samples are gaps.

Machine text and JSON default to ten rows per evidence list and three nested
process/command/install entries per row. MCP trends read only the last 24 hours;
Expand Down Expand Up @@ -573,7 +582,7 @@ driga cleanup remove --kind worktree --all # every ready worktree; skips anythin
Every check runs again just before acting; if anything changed, the action is
refused with the reason. Each attempt is appended to `cleanup.log` in the data
directory; if that write fails after the action succeeded, the action reports a
warning instead of an error. The TUI has a Cleanup tab, and `list_cleanup_candidates` is a
warning instead of an error. Machines → Reclaim → Disk is the TUI home for cleanup, and `list_cleanup_candidates` is a
read-only MCP tool.

## Summaries and Chat
Expand Down Expand Up @@ -611,19 +620,17 @@ and marked **answered by Claude Code**. A timed indicator shows thinking, writin
or the current read tool. Streaming follows the bottom until you scroll up; reduced
motion uses a static dot.

In `driga top`, press **8** or **c** for Chat with the current selection. Enter
sends; Shift+Enter adds a line, or Alt+Enter in terminals that cannot report
Shift+Enter. Escape stops an active reply; otherwise
it enters **navigate** mode and keeps the draft. In navigate mode, normal keys
work: **0–8** switch tabs, **q** quits, **?** opens help and **/** filters. Press
**c** or **Enter** to type again. **Alt+0–Alt+8** switch tabs from either mode;
returning to Chat restores the draft. The footer shows the current mode.
In `driga top`, press **c** for Chat with the current selection. Enter sends;
Shift+Enter adds a line, or Alt+Enter in terminals without Shift+Enter reporting.
Escape closes the panel and retains the draft. **Alt+0**, **Alt+1** and **Alt+2**
switch to Flow, Projects and Machines while typing. Blur the composer to use
Chat's navigation controls; **c** or **Enter** focuses the composer again.
Help scrolls with arrows, PgUp/PgDown or the mouse wheel. Applied list filters
stay visible in the status line; Escape clears the filter when no overlay is open.
Session details load when selected, rather than for every overview row.
PgUp/PgDown scroll the visible transcript with a one-line overlap and stop at its ends. Alt+Up recalls the last draft, Ctrl+H cycles history, Ctrl+N
starts a conversation, Ctrl+T toggles tool details, Ctrl+X clears context and
Ctrl+0 returns Home. Thinking/writing/tool status includes elapsed time and a
Alt+0 returns to Flow. Thinking/writing/tool status includes elapsed time and a
streaming cursor; NoColor uses a static indicator. Flow's copy-resume key is **C**.

Chat can acknowledge inbox items, jump to a session terminal, kill an agent or
Expand Down Expand Up @@ -706,8 +713,9 @@ driga web --open # also opens it
It listens on loopback only (`--listen` refuses other addresses), runs as its own
process so the collector's footprint is unchanged, and updates live as the collector
writes. The token in the printed URL sets a cookie for that browser; restarting
`driga web` issues a new token. Pages: Home, Agents, Projects, Inbox, Orphans,
Machines, Cleanup (listing only), Flow and Chat. Chat provides local actions through approval cards.
`driga web` issues a new token. The three tabs are Flow, Projects and Machines. Flow has Map, Timeline and Sessions views,
with Needs you and retained Activity history. Chat opens as a contextual panel on `c`;
Machines includes process and disk reclamation. Chat provides local actions through approval cards.

## MCP registration

Expand Down
76 changes: 76 additions & 0 deletions internal/boardtest/fixture.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package boardtest

import (
"fmt"
"github.com/labstack/driga/internal/model"
"github.com/labstack/driga/internal/view"
"time"
)

// BoardFixture is entirely synthetic and never launches or inspects an agent.
// Busy has seventy sessions across twelve projects, two machines and families.
func BoardFixture(kind string, now time.Time) view.BoardSnapshot {
count := 70
if kind == "quiet" {
count = 2
}
if kind == "empty" {
count = 0
}
b := view.BoardSnapshot{Remote: map[string]bool{"studio-mac": true}, Files: map[string][]model.FileTouch{}, Nodes: []view.FlowNode{}, Load: []int{1, 3, 2, 4, 7, 3, 2, 3, 6, 8, 4, 5}, Memory: "mem 82%"}
names := []string{"driga", "echo-pro", "monk", "inloop", "canon", "atlas", "studio", "orbit", "relay", "小さな道具", "northstar", "platform"}
titles := []string{"Add board view", "Fix retry after reconnect", "Permission boundary audit", "Polish onboarding copy", "流れを整える 🌊", "Investigate cache invalidation", "Ship the release pipeline safely", "An intentionally long title with CJK 東京 and emoji 🛰️ — verify alignment at every width"}
for i := 0; i < count; i++ {
state := []string{"working", "working", "waiting", "stalled", "working", "idle", "working", "finished", "working", "idle"}[i%10]
if kind == "quiet" {
state = "idle"
}
id := int64(i + 1)
adapter := []string{"claude", "codex", "copilot", "gemini"}[i%4]
machine := "local"
if i%5 == 4 {
machine = "studio-mac"
}
n := view.FlowNode{ID: id, MachineID: machine, ExternalID: fmt.Sprint("synthetic-", id), Adapter: adapter, Title: titles[i%len(titles)], Project: names[i%12], Branch: "main", Cwd: "/synthetic/" + names[i%12], StartedAt: now.Add(-time.Duration(45+i) * time.Minute), LastActivityAt: now.Add(-time.Duration(i%12) * time.Minute), Label: state, Mixer: model.StateMixer{Verdict: state, Evidence: "telemetry: tool_result", ObservedAt: now.Add(-2 * time.Second)}, Events: []model.Event{}, Waits: []view.FlowWait{}}
if i%8 == 0 {
n.StartedFrom = "Editor"
n.Branch = "feat/board"
n.Cwd += "-worktree"
}
if i >= 48 && i < 60 {
p := int64(i - 47)
n.ParentID = &p
n.Project = names[(i-48)%12]
n.MachineID = "local"
}
if state == "waiting" {
n.Waits = []view.FlowWait{{ID: fmt.Sprint("wait-", id), StartedAt: now.Add(-time.Duration(3+i%21) * time.Minute), Reason: "Permission · Bash", Detail: "go test ./..."}}
n.Mixer.Evidence = "open permission prompt"
}
if state == "stalled" {
n.Mixer.Evidence = "no transcript growth for 18m"
}
if state == "finished" {
end := now.Add(-time.Duration(4+i%25) * time.Minute)
n.EndedAt = &end
}
if state != "idle" {
o := &view.FlowObserved{FromAt: now.Truncate(time.Minute).Add(-59 * time.Minute), LogBytesPerMin: make([]int64, 60), CPUMSPerMin: make([]int64, 60)}
for c := range o.LogBytesPerMin {
if c%13 < 8 && c > i%20 {
o.LogBytesPerMin[c] = int64((c+i)%7+1) * 100
o.CPUMSPerMin[c] = 30
}
}
n.Observed = o
cost := float64(i%7+1) * .07
in, out := int64(120000+i*200), int64(8400+i*17)
n.Events = []model.Event{{Adapter: adapter, TS: now.Add(-6 * time.Minute), Kind: "otlp:api_request", Metadata: &model.EventMetadata{InputTokens: &in, OutputTokens: &out, CostUSD: &cost}}, {Adapter: adapter, TS: now.Add(-2 * time.Minute), Kind: "otlp:tool_result", Summary: "go test ./...", Metadata: &model.EventMetadata{Tool: "Bash"}}}
}
if i == 0 || i == 12 {
b.Files[view.BoardKey(n)] = []model.FileTouch{{Path: "/synthetic/driga/board.go", TS: now.Add(-time.Minute)}}
}
b.Nodes = append(b.Nodes, n)
}
return b
}
Loading
Loading