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
12 changes: 11 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
## How the CLI should look and sound

`cli-ux` records the conventions every command follows: one accent colour used
only for the tool's own chrome, a stdout a program consumes carrying nothing
else, decoration only where there is a terminal to draw it on, errors that name
the fix, help as a lowercase imperative phrase, British spelling, and what a
long operation must keep saying while it works. Read it before adding a
surface, rather than copying whichever neighbour is nearest.

- `cmd/spinloop/palette.go` — the colours and the spinner every surface draws from, in two groups that must not be swapped: the brand colours for the tool's own chrome, the state colours for what an engine is doing. (`cli-ux`)
# AGENTS.md

This file provides guidance to coding agents, such as Claude Code, when working with code in this repository.
Expand Down Expand Up @@ -49,7 +59,7 @@ The binary lives under `cmd/`; domain logic is split into `internal/` packages s
- `internal/pi` — Pi's `models.json` IO: deep-merge of one managed provider, preserving siblings and unknown fields. (`pi-integration`)
- `internal/lucinate` — lucinate's `connections.json` IO: one managed connection, no secret ever written to disk. (`lucinate-integration`)
- `cmd/spinloop/fleet.go`, `metrics_render.go`, `status_render.go`, `fleet_dashboard.go`, `dashboard_*.go` — the `fleet` command group and its Bubble Tea dashboard (the CLI's only TUI). (`fleet-client`, `fleet-config`)
- `internal/fleet` — the fleet client: the `Node` interface (`daemonNode`/`remoteNode`), concurrent fan-out, and routing/waking a node for a launch. (`fleet-client`, `fleet-config`, `fleet-routing`, `remote-node`)
- `internal/fleet` — the fleet client: the `Node` interface (`daemonNode`/`remoteNode`), concurrent fan-out (each reading stamped with the time its call returned), the `StartPhase` a start reports and the one function that renders it, and routing/waking a node for a launch. (`fleet-client`, `fleet-config`, `fleet-routing`, `remote-node`)
- `examples/fleet-docker/` — a runnable multi-node fleet that doubles as the fleet integration test, run per PR by CI. (`fleet-docker-example`)
- `cmd/spinloop/remote.go` + `internal/remote` — the `remote` command group and the scale-to-zero cloud GPU control plane (SigV4-signed Lambda Function URL calls — the repo's only AWS/network dependency). (`remote-environments`, `endpoint-lifecycle`, `endpoint-provisioning`, `remote-endpoint`, `remote-seed`, `weight-seeding`, `remote-keep`, `remote-start-probe`)
- `internal/daemon` — the engine supervisor and the HTTP control API. `Routes()` in `api.go` is checked against `docs/openapi.yaml` by `openapi_test.go` — keep them in sync when adding an endpoint. (`daemon-api`, `daemon-api-contract`, `engine-activity`, `engine-metrics`, `api-logging`, `serve-daemon`)
Expand Down
28 changes: 18 additions & 10 deletions cmd/spinloop/dashboard_detail.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// The full-screen node detail view: enter opens it on the node under the
// cursor, replacing the grid with that node's unclipped metrics, its tailed
// engine log, and a footer naming the keys the view answers to; escape
// closes it. The metrics section is dashNodeContentLines — the exact lines
// closes it. The metrics section is dashNodeView's lines — the exact lines
// the tile draws, unclipped — so the two surfaces cannot disagree; the log
// section follows fleet.LogsCall the same way `fleet logs -f` follows one
// node. Everything else about the board — the grid's own refresh, and any
Expand Down Expand Up @@ -166,12 +166,11 @@ func (m *dashModel) detailLogCapacity() int {
}

// detailSectionHeights splits the frame's rows between the metrics section
// (dashNodeContentLines' natural length for the node in view) and the log
// section (whatever remains after the header, footer, and the three dividers
// around the three sections), floored at one row each.
// (dashNodeView's natural length for the node in view) and the log section
// (whatever remains after the header, footer, and the three dividers around
// the three sections), floored at one row each.
func (m *dashModel) detailSectionHeights() (metrics, log int) {
e := m.entries[m.cursor]
metrics = len(dashNodeContentLines(e.name, m.results[m.cursor], m.actions[m.cursor]))
metrics = len(m.detailNodeLines())
if metrics < 1 {
metrics = 1
}
Expand All @@ -183,6 +182,15 @@ func (m *dashModel) detailSectionHeights() (metrics, log int) {
return metrics, log
}

// detailNodeLines is the metrics section: the same lines the node's tile draws,
// for the node the view is open on.
func (m *dashModel) detailNodeLines() []string {
e := m.entries[m.cursor]
lines, _ := dashNodeView(e.name, m.results[m.cursor], m.actions[m.cursor],
dashNow(), dashStaleAfter(e.kind))
return lines
}

// detailLogLines is the log section's content: the tailed lines, most recent
// last, or the standing note when there is nothing to show yet.
func detailLogLines(content, note string) []string {
Expand All @@ -203,20 +211,20 @@ func (m dashModel) detailView() string {
e := m.entries[m.cursor]
metricsLines, avail := m.detailSectionHeights()

title := fmt.Sprintf("fleet dashboard %s node: %s", m.fleetPath, e.name)
detail := m.fleetPath
if e.node != nil {
// A standing node never polls at all, follow flag or not, so its
// header says nothing about a log state that can never change.
logState := "following"
if !m.detailLogFollow {
logState = "paused"
}
title += " log: " + logState
detail += " log: " + logState
}
header := dashClip(title, w)
header := dashTitleBar("fleet dashboard · node: "+e.name, detail, w)
divider := strings.Repeat("─", w)

content := dashNodeContentLines(e.name, m.results[m.cursor], m.actions[m.cursor])
content := m.detailNodeLines()
for len(content) < metricsLines {
content = append(content, "")
}
Expand Down
Loading