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
3 changes: 2 additions & 1 deletion docs/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,4 +260,5 @@ replaces it.
| D283 | 2026-11-11 | The contr plugin row of Settings → Setup gets one action (#713, S3; owner decision D65). `Install` when the plugin is not installed, `Enable` when it is disabled. A click opens a confirm with the exact command, the same text as the copy line; only `Run` runs it, off the UI thread, with a 2 min limit, and only these two lines with the `claude` the checks find. A spinner shows while it runs; a failure shows the last stderr line under the row; a success runs a recheck. The copy line stays. No other row gets an action (meter D66, gh and hc copy only); contr starts no hc core here and edits no file in Claude Code's folders. | Before, the owner had to copy the line to a terminal. The confirm keeps the run explicit. |
| D284 | 2026-11-11 | A fenced `contr-flow` block in Claude's answer draws as a flow diagram (#696; ChatView.md, Diagrams). contr has its own syntax, one arrow per line (`A -> B: label`, `A [state]`), and draws it with MyGo's Painter (package `internal/app/diagram`), with tokens only and the chat text size. A web view with Mermaid's JS was ruled out (a web process, no theme tokens) and so was a Mermaid subset (most real Mermaid would fall back). A block that does not parse or is too large (more than 12 nodes, past the layer limits, wider than the column) shows as its code block. Sequence diagrams come in a second PR. |
| D285 | 2026-11-11 | In the chat, a worker shows the mark of its model family in place of its 6px dot (#714, slice A): the Claude Code mascot, the OpenAI mark (Codex) or the DeepSeek mark, 12px, in the note lines and the work group's fold row. The family comes from what contr already knows of the worker: its model label, else the model of its profile, else its program (`claude`, `codex`); no new read. A worker of another program keeps the dot. The state reads as the dot's did: the mark takes the dot's ink (blocked `ink-100`, working `ink-88`, else `ink-30`) and hops as the working Claude mascot does while the worker works (still with Reduce Motion). No hue: blocked stays grey in the chat (D259, D261), and the mascot is grey too, as `claude` is the orchestrator's. Only the chat changes; the sidebar and the tabs keep their glyphs. The marks are from Simple Icons (CC0). |
| D286 | 2026-11-11 | PR and issue chips show GitHub's Octicons (#714, slice B; docs/spec/ChatView.md, Chips and Blocks). A PR shows the pull request icon of its state: open, draft, merged or closed; an issue the issue icon, open or closed. The icons are in `assets/icons/github` (Octicons, MIT, in NOTICE) and `icons.PRIcon`, `icons.IssueIcon` map gh's states to them. They take an ink token (`ink-48`), never GitHub's colors (D119, D261). The dot of the checks (running, passed, failed) stays after the icon, so the chip still shows CI; the open and closed dots go, as the icon says it. The read of a number (`gh pr view`, D262) adds `isDraft`; no other gh read is added. The board's PR cell shows the same icon. The work rows of the board panel (hidden, D147) are not changed. | The owner asked for GitHub's icons (#714). |
| D286 | 2026-11-11 | PR and issue chips show GitHub's Octicons (#714, slice B; docs/spec/ChatView.md, Chips and Blocks). A PR shows the pull request icon of its state: open, draft, merged or closed; an issue the issue icon, open or closed. The icons are in `assets/icons/github` (Octicons, MIT, in NOTICE) and `icons.PRIcon`, `icons.IssueIcon` map gh's states to them. They take an ink token (`ink-48`), never GitHub's colors (D119, D261). The dot of the checks (running, passed, failed) stays after the icon, so the chip still shows CI (replaced by D288); the open and closed dots go, as the icon says it. The read of a number (`gh pr view`, D262) adds `isDraft`; no other gh read is added. The board's PR cell shows the same icon. The work rows of the board panel (hidden, D147) are not changed. | The owner asked for GitHub's icons (#714). |
| D288 | 2026-11-11 | PR and issue chips have no checks dot; the Octicon's color shows the state (#741; docs/spec/ChatView.md, Chips and Blocks). It replaces the ink-48 icon and the dot of D286. While a PR is open its checks win: failed `content-failed`, running `content-running`, passed `content-done`; then open `content-done`, draft `ink-48`. Merged `content-link` and closed `ink-48`, so a closed PR does not read as failed. An issue: open `content-done`, closed `content-link`. The palette has no purple, so merged and a closed issue take `content-link`, the nearest hue; no token is added. The number keeps `content-link`. The tooltip and the label still name the state. The board's PR cell does the same. | The owner asked to remove the dots and show the state by color (#741). |
10 changes: 5 additions & 5 deletions docs/spec/ChatView.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,14 +267,14 @@ does not know stays plain text.

| Chip | Recognized | Known when | Dot | Click |
|---|---|---|---|---|
| PR | "#667" | gh says it is a pull request of the session's repository (read by number), or it is the PR of a worker's branch (D104, D145) | GitHub's pull request Octicon of its state, 0.9 of the text size, `ink-48`: open, draft, merged, closed (D286). An open PR with checks adds a 6px dot after it: running `content-running`, passed `content-done`, failed `content-failed`. A failed check wins over a pending one, a pending one over a passed one. | opens the PR as the agent rows do: in the browser (the PR pane is hidden, D147) |
| Issue | "#649" | gh says it is an issue | GitHub's issue Octicon, open or closed, 0.9 of the text size, `ink-48` (D286) | opens the issue in the browser |
| PR | "#667" | gh says it is a pull request of the session's repository (read by number), or it is the PR of a worker's branch (D104, D145) | GitHub's pull request Octicon of its state, 0.9 of the text size: open, draft, merged, closed (D286). No dot: the icon's color shows the state (D288). While the PR is open its checks win: failed `content-failed`, running `content-running`, passed `content-done`; with no checks open `content-done`, draft `ink-48`. Merged `content-link` (no purple token), closed `ink-48`. A failed check wins over a pending one, a pending one over a passed one. The tooltip and the label name the state. | opens the PR as the agent rows do: in the browser (the PR pane is hidden, D147) |
| Issue | "#649" | gh says it is an issue | GitHub's issue Octicon, open or closed, 0.9 of the text size (D286): open `content-done`, closed `content-link` (D288) | opens the issue in the browser |
| Worker | a word that is the name of a worker contr lists ("b-649"); a live one wins | always | needs you `content-failed`, working `content-running`, done (ended) `content-done`, waiting (idle) `content-waiting` | selects the worker in the sidebar |
| File | a word with a slash or an extension, with an optional ":42" | it is a file in the session's folder | none: the `file` line icon, 0.9 of the text size, `ink-48` | opens the file in the Review view, as the files panel does (the line is not used) |

- The dot is 6px, round, `space-4` before the text, its center near the
middle of the lowercase letters. The Octicons take the ink of the
text around them, never GitHub's colors (D119, D261, D286). The tip of a
middle of the lowercase letters. The Octicons take the content token of their
state, never GitHub's color values (D119, D261, D288). The tip of a
chip says what it is: "PR #667 · checks passed", "Draft PR #661", "Worker b-649 · needs you", "Open
internal/app/x.go".
- A number is read only while its text is on screen and only once: the
Expand All @@ -295,7 +295,7 @@ A coordinator writes a status board or a merge queue as a fenced block (D267). T

| Block | Syntax | Shows |
|---|---|---|
| Status board | info string `contr-status`; a line `<worker> \| #<issue> \| <what it builds>` | six columns: Worker, Issue, Builds, State, PR, Checks; heads mono `meta`, `ink-48`; no box, a `hairline` above and below the grid, `space-16` between columns; the worker in `body`, `ink-88`, after its 6px dot (the chip's worker colors); the issue mono `meta` `ink-64`; what it builds `body` `ink-88`, one line with "…"; the state in mono `meta`, `ink-64` (grey: only the dots have content colors), "needs you" in `blocked-text`; the PR as its Octicon (12px, `ink-48`, D286), the dot of its checks when it has some, and a mono 12px number on `hover`, radius 5, a click opens it as a chip does; the checks "2 of 3" (passed of passed, failed and pending) mono `meta` `ink-64`; "—" for none |
| Status board | info string `contr-status`; a line `<worker> \| #<issue> \| <what it builds>` | six columns: Worker, Issue, Builds, State, PR, Checks; heads mono `meta`, `ink-48`; no box, a `hairline` above and below the grid, `space-16` between columns; the worker in `body`, `ink-88`, after its 6px dot (the chip's worker colors); the issue mono `meta` `ink-64`; what it builds `body` `ink-88`, one line with "…"; the state in mono `meta`, `ink-64` (grey: only the dots have content colors), "needs you" in `blocked-text`; the PR as its Octicon (12px, D286) in the chip's color of its state (D288), and a mono 12px number on `hover`, radius 5, a click opens it as a chip does; the checks "2 of 3" (passed of passed, failed and pending) mono `meta` `ink-64`; "—" for none |
| Merge queue | info string `contr-queue`; a line `#<n> <label>`, optional ` -- <note>` | the steps in order on one rail 14px wide: an 8px dot, a 1px `hairline` line down to the next step; the step's number mono `meta` `ink-48`, `#n label` in `prose` `ink-88`, the state in words in mono `meta` |

State of a board row: a worker that needs the owner says "needs you"; else the state of its PR: "PR open", "CI running", "CI passed", "CI failed", "merged", "closed"; else the worker's own state: "building", "waiting", "done". A worker contr does not list shows its name in `ink-48` and "not found" in `ink-48`, no dot.
Expand Down
7 changes: 2 additions & 5 deletions internal/app/chatview/blocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ func (ch *Chat) boardRow(c *ui.Context, key int, pal *theme.Palette, l BoardLine
})
}

// prChip is the PR of a board row: its Octicon in ink-48 (D286), the dot of its checks, and its
// prChip is the PR of a board row: its Octicon in the color of its state (D288), and its
// number in mono on hover; a click opens it as a chip does.
func (ch *Chat) prChip(c *ui.Context, pal *theme.Palette, n int, chip Chip) {
name := strconv.Itoa(n)
Expand All @@ -435,10 +435,7 @@ func (ch *Chat) prChip(c *ui.Context, pal *theme.Palette, n int, chip Chip) {
})
b.Cursor(ui.CursorPointer).Children(func() {
ui.Row(c).AlignItems(ui.Center).Gap(theme.Space4).Children(func() {
icons.GitHubIcon(c, ChipGitHub(chip), 12*ch.scale(), pal.Ink48)
if col, ok := ChipColor(pal, chip.State); ok && checksDot(chip.State) {
ui.Box(c).Size(chipDot, chipDot).Radius(chipDot / 2).Background(col)
}
icons.GitHubIcon(c, ChipGitHub(chip), 12*ch.scale(), GitHubColor(pal, chip))
ui.Text(c, "#"+name).Font(theme.FontFamily).FontSize(12 * ch.scale()).NoWrap().TextColor(pal.Ink88)
})
})
Expand Down
48 changes: 32 additions & 16 deletions internal/app/chatview/chips.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,27 +242,43 @@ func ChipGitHub(c Chip) icons.GitHub {
return icons.GitHubNone
}

// checksDot reports whether the chip's state is one of checks, which a dot shows after the
// Octicon: running, passed or failed.
func checksDot(s ChipState) bool { return s == StateRunning || s == StatePassed || s == StateFailed }
// GitHubColor is the color of the Octicon of a PR or an issue chip (D288). While a PR is open,
// its checks win: failed, running, passed. Then merged, closed, draft, open. An issue is open or
// closed. The palette has no purple, so merged and a closed issue take content-link, the nearest
// hue; a closed PR and a draft take ink-48, so a closed PR does not read as failed.
func GitHubColor(p *theme.Palette, c Chip) ui.Color {
if c.Kind == ChipIssue {
if c.State == StateClosed {
return p.ContentLink
}
return p.ContentDone
}
switch c.State {
case StateFailed:
return p.ContentFailed
case StateRunning:
return p.ContentRunning
case StatePassed:
return p.ContentDone
case StateMerged:
return p.ContentLink
case StateClosed:
return p.Ink48
}
if c.Draft {
return p.Ink48
}
return p.ContentDone
}

// githubIcon is the LinkIcon of a PR or an issue chip: its Octicon in ink-48, 0.9 of the text
// size as the file icon, then the dot of its checks when it has one.
// githubIcon is the LinkIcon of a PR or an issue chip: its Octicon in the color of its state,
// 0.9 of the text size as the file icon.
func githubIcon(c Chip, g icons.GitHub, size float32) markdown.InlineObject {
side := size * 0.9
w := side + chipDotGap
if checksDot(c.State) {
w += chipDot + chipDotGap
}
return markdown.InlineObject{Width: w, Height: side * 0.82, Depth: side * 0.18,
return markdown.InlineObject{Width: side + chipDotGap, Height: side * 0.82, Depth: side * 0.18,
Draw: func(p *ui.Painter, r ui.Rect) {
pal := theme.Current()
if s := icons.GitHubSVG(g); s != nil {
p.Icon(s, ui.Rect{X: r.X, Y: r.Y, W: side, H: side}, pal.Ink48)
}
if col, ok := ChipColor(pal, c.State); ok && checksDot(c.State) {
y := r.Y + side/2 - chipDot/2
p.Fill(ui.Rect{X: r.X + side + chipDotGap, Y: y, W: chipDot, H: chipDot}, col, chipDot/2)
p.Icon(s, ui.Rect{X: r.X, Y: r.Y, W: side, H: side}, GitHubColor(theme.Current(), c))
}
}}
}
Expand Down
29 changes: 26 additions & 3 deletions internal/app/chatview/chips_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ func TestChipIcon(t *testing.T) {
t.Error("a web link has an icon")
}
side := float32(14 * 0.9)
// A PR or an issue shows its Octicon; a PR with checks adds their dot after it (D286).
// A PR or an issue shows its Octicon; no dot follows it (D288).
for _, tc := range []struct {
c Chip
w float32
Expand All @@ -208,8 +208,8 @@ func TestChipIcon(t *testing.T) {
{Chip{Kind: ChipPR, State: StateOpen, Draft: true}, side + chipDotGap},
{Chip{Kind: ChipPR, State: StateMerged}, side + chipDotGap},
{Chip{Kind: ChipPR, State: StateClosed}, side + chipDotGap},
{Chip{Kind: ChipPR, State: StatePassed}, side + 2*chipDotGap + chipDot},
{Chip{Kind: ChipPR, State: StateFailed, Draft: true}, side + 2*chipDotGap + chipDot},
{Chip{Kind: ChipPR, State: StatePassed}, side + chipDotGap},
{Chip{Kind: ChipPR, State: StateFailed, Draft: true}, side + chipDotGap},
} {
o, ok := chipIcon(chipDest(tc.c, "4"), 14)
if !ok || o.Width != tc.w || o.Draw == nil {
Expand Down Expand Up @@ -358,3 +358,26 @@ func TestChipsAskForNoRedrawAtRest(t *testing.T) {
t.Error("a redraw is asked at rest")
}
}

func TestGitHubColor(t *testing.T) {
for _, p := range []*theme.Palette{theme.Dark, theme.Light} {
for _, tc := range []struct {
c Chip
want any
}{
{Chip{Kind: ChipPR, State: StateFailed, Draft: true}, p.ContentFailed},
{Chip{Kind: ChipPR, State: StateRunning}, p.ContentRunning},
{Chip{Kind: ChipPR, State: StatePassed}, p.ContentDone},
{Chip{Kind: ChipPR, State: StateOpen}, p.ContentDone},
{Chip{Kind: ChipPR, State: StateOpen, Draft: true}, p.Ink48},
{Chip{Kind: ChipPR, State: StateMerged}, p.ContentLink},
{Chip{Kind: ChipPR, State: StateClosed}, p.Ink48},
{Chip{Kind: ChipIssue}, p.ContentDone},
{Chip{Kind: ChipIssue, State: StateClosed}, p.ContentLink},
} {
if got := GitHubColor(p, tc.c); any(got) != tc.want {
t.Errorf("dark=%v %+v: %v, want %v", p.Dark, tc.c, got, tc.want)
}
}
}
}
Loading