Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
9d01cf9
Chat: @ in the composer completes file paths (#708, D279)
contursofran Nov 11, 2026
9804760
Chat: @ paths leave out deleted files and list a nested repository once
contursofran Nov 11, 2026
b4c40f3
Chat: see fixture and e2e test of the @ path popup
contursofran Nov 11, 2026
10bb4a0
Chat: the composer shows Claude Code's suggested next prompt (#727, D…
contursofran Nov 11, 2026
8748c70
Docs: Path popup in the ChatView spec, D279
contursofran Nov 11, 2026
7923f4c
Merge main
contursofran Nov 11, 2026
4f4fd25
Merge main into agent/708-path-autocomplete
contursofran Nov 11, 2026
9f27428
Chat: next prompt docs (D281), spec sheet part and chat-next fixture …
contursofran Nov 11, 2026
512dfa9
Merge main
contursofran Nov 11, 2026
76df9b1
Chat: brand marks of known servers on call lines (#709 S4, D274)
contursofran Nov 11, 2026
90d3932
Merge remote-tracking branch 'origin/main' into agent/709-brand-marks
contursofran Nov 11, 2026
6a5ea10
Merge main into agent/708-path-autocomplete
contursofran Nov 11, 2026
80e1c52
Merge agent/708-path-autocomplete and main into agent/727-prompt-sugg…
contursofran Nov 11, 2026
e7391c0
Merge remote-tracking branch 'origin/agent/727-prompt-suggestion' int…
contursofran Nov 11, 2026
c01ed19
Chat: a contr-flow block draws as a flow diagram (#696, D284)
contursofran Nov 11, 2026
ec7ffd4
Chat: see fixture chat-flow, D284 and the Diagrams spec (#696)
contursofran Nov 11, 2026
70be730
Merge main
contursofran Nov 11, 2026
eec430a
Chat: a worker shows the mark of its model family (#714, D285)
contursofran Nov 11, 2026
5fc85a7
Merge main into agent/714-model-icons
contursofran Nov 11, 2026
4c29504
Chat: Octicons for PR and issue chips and board PRs (#714, D286)
contursofran Nov 11, 2026
37e74ef
Merge main into agent/714-github-icons
contursofran Nov 11, 2026
25ab47d
Remove the doubled chat-paths fixture of the merge
contursofran Nov 11, 2026
83f8263
gofmt the chat-flow fixture
contursofran Nov 11, 2026
31df843
Tests: remove the doubled chat-paths fixture entry
contursofran Nov 11, 2026
99e8f31
Merge main into agent/714-model-icons
contursofran Nov 11, 2026
a65b382
Merge main into agent/714-model-icons
contursofran Nov 11, 2026
97bf831
Merge main into agent/714-github-icons
contursofran Nov 11, 2026
74e554e
Merge main into agent/696-diagrams-flow
contursofran Nov 11, 2026
c9aa249
Merge main into agent/714-github-icons
contursofran Nov 11, 2026
77493ef
Merge commit 'c9aa24930fe795b869b306ccc65b91af3105fd00' into agent/69…
contursofran Nov 11, 2026
c92e1c0
Merge remote-tracking branch 'origin/main' into agent/696-diagrams-flow
contursofran Nov 11, 2026
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
55 changes: 55 additions & 0 deletions cmd/contr/e2e_see_flow_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//go:build e2e

package main

import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"

"github.com/contursofran/contr/internal/agents"
"github.com/contursofran/contr/internal/session"
)

// seeChatFlow spawns lead and writes a transcript whose answer has a flow diagram (D284).
func seeChatFlow(t *testing.T, h *harness) {
repo := h.repo(t, "demo", "main")
lead, err := h.mgr.Spawn(h.ctx, agents.Spec{Name: "lead", Profile: "claude-idle", Role: agents.RoleOrchestrator, Project: "demo", Worktree: repo})
if err != nil {
t.Fatalf("spawn lead: %v", err)
}
path := filepath.Join(h.home, ".claude", "projects", "demo", "flow.jsonl")
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(flowTranscript()), 0o644); err != nil {
t.Fatal(err)
}
s := &session.Session{UpdatedAt: uint64(time.Now().Unix())}
s.SetTranscript("flow", path)
if err := session.Write(filepath.Join(h.dir, "state"), lead.Address, s); err != nil {
t.Fatal(err)
}
}

// flowTranscript is a prompt and an answer with a contr-flow block: a branch, a loop back and
// two states.
func flowTranscript() string {
start := time.Now().Add(-2 * time.Minute)
line := func(n int, typ string, content any) string {
b, _ := json.Marshal(map[string]any{"type": typ, "uuid": fmt.Sprintf("u%d", n), "sessionId": "flow",
"timestamp": start.Add(time.Duration(n*10) * time.Second).UTC().Format(time.RFC3339Nano),
"message": map[string]any{"role": typ, "content": content}})
return string(b) + "\n"
}
return strings.Join([]string{
line(1, "user", "how does a fix get merged?"),
line(2, "assistant", []any{map[string]any{"type": "text", "text": "It goes through review:\n\n```contr-flow\n" +
"issue -> plan: owner picks\nplan -> build\nbuild -> review: PR\nreview -> merge: approved\n" +
"review -> build: blockers\nmerge -> release\nmerge [done]\nreview [waiting]\n```\n\nA blocker sends it back to the builder."}}),
}, "")
}
6 changes: 6 additions & 0 deletions cmd/contr/e2e_see_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,12 @@ var seeFixtures = map[string]seeFixture{
ready: "lead, ",
press: []string{"Chat", "wait:image 2"},
},
"chat-flow": {
about: "the orchestrator lead in Chat mode with an answer that holds a contr-flow block, drawn as a flow diagram (D284): six boxes, edge labels, a loop back under the boxes, review waiting and merge done",
setup: seeChatFlow,
ready: "lead, ",
press: []string{"Chat", "wait:Flow diagram"},
},
"chat-tools": {
about: "the chat fixture with a turn of mixed tool calls, its work group open (D274): a call line of each kind with its icon (skill, Slack connector, contr plugin, Linear MCP and Notion connector with their brand marks, an unknown MCP server with the connector icon, agent, fetch, Bash, Read, Grep), a tool of no kind with an empty slot, the label and key argument of each, and the Bash call open on its detail",
setup: seeChatTools,
Expand Down
1 change: 1 addition & 0 deletions docs/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,5 +258,6 @@ replaces it.
| D281 | 2026-11-11 | The composer shows Claude Code's suggested next prompt (#727, tracker #634). Claude Code does not write the suggestion to the transcript (it is made with the transcript off) and `terminal.read` gives text without styles, so contr reads the selected terminal's screen as escape sequences (the MyGo terminal's `Snapshot`) and takes the text after the last `❯` only when all of it is faint (SGR 2), as a real session draws it; typed text, a sent prompt or a menu choice is not faint. The read runs 300 ms after output, off the UI thread, through a new `OnOutput` hook of `ghostty.Conn`. While the field is empty, the suggestion is the field's placeholder; Tab or Right takes it, after the slash popup's Tab. Only the selected terminal has a screen, so other sessions show none (docs/spec/ChatView.md, Composer). | Owner in #727. |
| D282 | 2026-11-11 | A Claude worker gets the PATH of the process that spawns it (#728). `hc update` restarts the core under launchd, whose PATH is short, and each terminal inherits the PATH of the core, so `claude` was not found and the worker ended at once. Spawn now sets `CONTR_PATH` in the terminal: the PATH of the spawning process, then `~/.local/bin`, `/opt/homebrew/bin` and `/usr/local/bin`, without doubles. The clean wrapper (D216) adds these folders after the PATH of the core and removes the variable, so the PATH of the core still comes first. A program that does not start through the clean wrapper (not Claude) gets no change. | Seen on 2026-11-11 after the first `Update hc` (D277): two spawns ended with "claude: not found". |
| 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). |
12 changes: 11 additions & 1 deletion docs/spec/ChatView.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ hasn't heard from you…" nudges, and a user line that is all inside one

## Content

How Claude's Markdown shows (D261). MyGo's theme options style what they can; the Block hook builds the rest (remarks, tables, numbered and task lists), and a Code builder the diff blocks. The Markdown inside a remark, a cell or an item shows in a `markdown.View` of its own that shares the selection.
How Claude's Markdown shows (D261). MyGo's theme options style what they can; the Block hook builds the rest (remarks, tables, numbered and task lists), a Code builder the diff blocks, and the `contr-flow` blocks draw as [diagrams](#diagrams). The Markdown inside a remark, a cell or an item shows in a `markdown.View` of its own that shares the selection.

| Block | Value |
|---|---|
Expand Down Expand Up @@ -304,6 +304,16 @@ State of a queue step: a merged step has a 12px check in `content-done` and "mer

Live and frozen: only the newest board of the thread is live. Under it, a 5px `content-running` dot and "live · updated 18:49" (mono `meta`, `ink-48`): the time its values last changed. An older board keeps the values it showed while it was live, dims to 60% and says "as of 18:45 · a newer board is below". When contr did not show it live (an older answer read after the app started), it shows only the three columns the agent wrote and the time of its answer, so it never shows a state newer than its answer. Nothing ticks (D55): the times are fixed texts.

## Diagrams

Claude writes a flow diagram as a fenced block `contr-flow` (D284). contr draws it with MyGo's Painter, in the column; the lines read well in a terminal too.

- Syntax: one line per arrow, `A -> B` or `A -> B: label`; a state as `A [done]`, with `done`, `running`, `waiting`, `failed` or `info`. A node is any text without `->`; empty lines are skipped.
- Layout: layers left to right from the longest path, one sweep orders each layer; an arrow back (a cycle) is an arc under the boxes, its label under the arc.
- Box: height at least 36px, text `body` (at the chat text size, D272) in `ink-88`, centered, 14px side padding, at least 88px wide, radius 8; `hover` fill and an `ink-30` line; a node with a state takes the tint of its content color and a line in that color.
- Arrow: a 1.2px `ink-30` line, an open `ink-48` head; its label in mono `meta` (at the chat text size), `ink-48`, on a patch of the stage, in the middle of the gap.
- Falls back to the code block: a block that does not parse (a chain `A -> B -> C`, a line without an arrow, no arrow at all), more than 12 nodes, a label longer than 28 characters, more than 6 layers, more than 4 nodes in a layer, wider than the column, or a block that is still being written.

## Behavior

- The view reads the transcript only while it shows: a read runs off the
Expand Down
18 changes: 17 additions & 1 deletion internal/app/chatview/content.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/egoist/mygo/ui/markdown"
"github.com/egoist/mygo/ui/markdown/parser"

"github.com/contursofran/contr/internal/app/diagram"
"github.com/contursofran/contr/internal/app/kit"
"github.com/contursofran/contr/internal/app/theme"
)
Expand Down Expand Up @@ -129,7 +130,7 @@ func quoteText(src string) string {

// mdHooks sets the hooks of the chat's Markdown options.
func (ch *Chat) mdHooks() {
code := map[string]func(*ui.Context, *markdown.CodeBlockView){"diff": ch.diffBlock, "patch": ch.diffBlock}
code := map[string]func(*ui.Context, *markdown.CodeBlockView){"diff": ch.diffBlock, "patch": ch.diffBlock, diagram.Lang: ch.flowBlock}
ch.md.Block, ch.md.Code = ch.block, code
ch.mdNest.Block, ch.mdNest.Code = ch.block, code
// The views inside a remark, a cell or an item show the chips of the pre-pass (chips.go) as the
Expand Down Expand Up @@ -490,6 +491,21 @@ func DiffKind(line string) DiffLineKind {
return DiffContext
}

// flowBlock is a contr-flow block drawn as a flow diagram (D284), at most the column wide. A
// block that does not parse, does not fit or is still being written shows as its code block.
func (ch *Chat) flowBlock(c *ui.Context, b *markdown.CodeBlockView) {
f, err := diagram.Parse(b.Code())
maxW := float32(columnMax)
if w := ch.contentWidth(c); w > 0 {
maxW = min(maxW, w)
}
st := diagram.Style{Node: ch.ts(theme.StyleBody), Label: ch.ts(theme.StyleMeta)}
if err == nil && !b.Open && diagram.View(c, f, maxW, st) {
return
}
markdown.DefaultCodeBlock(c, b)
}

// diffBlock is a diff or patch code block: the language and Copy above its lines, an added line
// in content-done and a removed one in content-failed, each on a tint of its color. It scrolls
// sideways as other code blocks do.
Expand Down
28 changes: 28 additions & 0 deletions internal/app/chatview/content_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/egoist/mygo/ui/markdown"
"github.com/egoist/mygo/ui/markdown/parser"

"github.com/contursofran/contr/internal/app/diagram"
"github.com/contursofran/contr/internal/app/theme"
"github.com/contursofran/contr/internal/app/theme/themetest"
"github.com/contursofran/contr/internal/app/view"
Expand Down Expand Up @@ -291,3 +292,30 @@ func TestLinksHaveTheLinkColor(t *testing.T) {
}
}
}

// wideFlow is six long boxes in a row: wider than the column.
const wideFlow = "the first long box name -> the second long box name\nthe second long box name -> the third long box name\n" +
"the third long box name -> the fourth long box name\nthe fourth long box name -> the fifth long box name\n"

const bigFlow = "a -> b\nb -> c\nc -> d\nd -> e\ne -> f\nf -> g\ng -> h\nh -> i\ni -> j\nj -> k\nk -> l\nl -> m\n"

// A contr-flow block draws as a flow diagram (D284); one that does not parse or is too large
// shows as its code block.
func TestFlowBlock(t *testing.T) {
for _, tc := range []struct {
name, src string
drawn bool
}{
{"good", "a -> b: go\nb [done]\n", true},
{"bad", "a -> b -> c\n", false},
{"large", bigFlow, false},
{"wide", wideFlow, false},
} {
tt, _ := contentRig(t, mygo.ThemeDark, "```contr-flow\n"+tc.src+"```\n")
_, drawn := tt.Access(diagram.LabelDiagram)
_, code := tt.AccessByRole(ui.RoleGroup, "Code, contr-flow")
if drawn != tc.drawn || code == tc.drawn {
t.Errorf("%s: diagram %v, code block %v", tc.name, drawn, code)
}
}
}
10 changes: 9 additions & 1 deletion internal/app/chatview/sheet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ func TestSpecSheet(t *testing.T) {
open := true
detail.s.ch.calls[id] = &open
}
flow := sheetChat(t, "idle", lPrompt, said("f1", flowAnswer))
marks := sheetMarks(t)
apptest.SpecSheet(t, []apptest.SheetPart{
{Name: "ChatView", W: sheetW, H: sheetH, View: working.view},
Expand All @@ -88,7 +89,8 @@ func TestSpecSheet(t *testing.T) {
{Name: "CallLinesBrands", W: sheetW, H: 420, View: brandsView.view},
{Name: "CallDetail", W: sheetW, H: 360, View: detail.view},
{Name: "ChatWorkerMarks", W: sheetW, H: 600, View: marks.view},
}, []string{"ChatWorkerMarks", "ChatViewComposer", "ChatView", "ChatViewWide", "ChatViewTeam", "ChatViewChips", "ChatContent", "ChatViewScrolled", "ChatBlocks", "ChatBlocksBlocked", "ChatViewImages", "CallLines", "CallLinesBrands", "CallDetail"})
{Name: "ChatFlow", W: sheetW, H: 620, View: flow.view},
}, []string{"ChatFlow", "ChatWorkerMarks", "ChatViewComposer", "ChatView", "ChatViewWide", "ChatViewTeam", "ChatViewChips", "ChatContent", "ChatViewScrolled", "ChatBlocks", "ChatBlocksBlocked", "ChatViewImages", "CallLines", "CallLinesBrands", "CallDetail"})
}

// sheetMarks is an open work group with a note of a worker of each family (Claude, OpenAI,
Expand Down Expand Up @@ -131,6 +133,12 @@ const contentAnswer = "Four builders work with the M1 rules: red never fills or
"```go\n// Sync follows a terminal.\nfunc (s *Source) Sync(id string) error {\n\tif id == \"\" {\n\t\treturn errors.New(\"no terminal\")\n\t}\n\ts.limit = 920\n\treturn nil\n}\n```\n\n" +
"```diff\n--- a/kit/agentrow.go\n+++ b/kit/agentrow.go\n@@ -10,3 +10,3 @@\n if row.Selected {\n-\tb.Background(t.Selected)\n+\tb.Background(t.SelectedQuiet)\n }\n```\n"

// flowAnswer has a flow diagram with states, a branch and a loop back, and a block too large to
// draw, which shows as code (D284).
const flowAnswer = "The fix goes through review:\n\n```contr-flow\nissue -> plan: owner picks\nplan -> build\n" +
"build -> review: PR\nreview -> merge: approved\nreview -> build: blockers\nmerge -> release\nmerge [done]\nreview [waiting]\n```\n\n" +
"Too many steps for a drawing:\n\n```contr-flow\n" + bigFlow + "```\n"

// chipsText has each kind of chip in each state (D262), and names that stay text: an unknown
// number, a name in code and a path that does not exist.
const chipsText = "PRs: #668 open, #656 checks running, #667 passed, #664 failed, #663 merged, #660 closed, #661 draft. " +
Expand Down
116 changes: 116 additions & 0 deletions internal/app/diagram/diagram_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package diagram

import (
"errors"
"strings"
"testing"

"github.com/egoist/mygo/ui"
)

const branch = "issue -> plan: owner picks\nplan -> build\nbuild -> review: PR\n" +
"review -> merge: approved\nreview -> build: blockers\nmerge -> release\nmerge [done]\nreview [waiting]\n"

func TestParse(t *testing.T) {
f, err := Parse(branch)
if err != nil {
t.Fatal(err)
}
if len(f.Nodes) != 6 || len(f.Edges) != 6 {
t.Fatalf("nodes %d, edges %d", len(f.Nodes), len(f.Edges))
}
if e := f.Edges[0]; f.Nodes[e.From].Label != "issue" || f.Nodes[e.To].Label != "plan" || e.Label != "owner picks" {
t.Errorf("first edge %+v", e)
}
if f.Edges[1].Label != "" {
t.Errorf("an edge without a label has %q", f.Edges[1].Label)
}
if f.Nodes[4].Label != "merge" || f.Nodes[4].State != "done" || f.Nodes[3].State != "waiting" || f.Nodes[0].State != "" {
t.Errorf("states %+v", f.Nodes)
}
if f, err := Parse("a -> b: x: y"); err != nil || f.Edges[0].Label != "x: y" {
t.Errorf("a label with a colon: %+v %v", f, err)
}
if f, err := Parse("a [running]\n\n a -> b \n"); err != nil || f.Nodes[0].State != "running" || len(f.Nodes) != 2 {
t.Errorf("a state before the arrow, blank lines: %+v %v", f, err)
}
}

func TestParseErrors(t *testing.T) {
for src, want := range map[string]error{
"a -> b -> c": ErrChain,
"": ErrEmpty,
"a [done]\n": ErrEmpty,
} {
if _, err := Parse(src); !errors.Is(err, want) {
t.Errorf("%q: %v, want %v", src, err, want)
}
}
for _, src := range []string{"just words", "a -> b\nno arrow", "a [busy]"} {
if _, err := Parse(src); err == nil || !strings.Contains(err.Error(), "line") {
t.Errorf("%q: %v, want a line error", src, err)
}
}
}

func TestLimits(t *testing.T) {
var b strings.Builder
for i := range MaxNodes - 1 {
b.WriteString(string(rune('a'+i)) + " -> " + string(rune('a'+i+1)) + "\n")
}
twelve, _ := Parse(b.String())
if ok, _ := twelve.Fits(); !ok {
t.Error("12 nodes do not fit")
}
if ok, _ := Layout(twelve).Fits(); ok {
t.Error("12 layers fit")
}
thirteen, _ := Parse(b.String() + "l -> m\n")
if ok, _ := thirteen.Fits(); ok {
t.Error("13 nodes fit")
}
long, _ := Parse("a -> " + strings.Repeat("x", MaxLabel+1))
if ok, _ := long.Fits(); ok {
t.Error("a long label fits")
}
fan, _ := Parse("a -> b\na -> c\na -> d\na -> e\na -> f\n")
if ok, _ := Layout(fan).Fits(); ok {
t.Error("5 nodes in a layer fit")
}
l := Layout(mustParse(t, branch))
back := 0
for _, x := range l.Back {
if x {
back++
}
}
if back != 1 || len(l.Layers) != 6 {
t.Errorf("back edges %d, layers %d", back, len(l.Layers))
}
}

func mustParse(t *testing.T, s string) Flow {
t.Helper()
f, err := Parse(s)
if err != nil {
t.Fatal(err)
}
return f
}

// View draws a flow that fits, and builds nothing for one wider than maxW.
func TestView(t *testing.T) {
f := mustParse(t, branch)
for _, tc := range []struct {
maxW float32
want bool
}{{2000, true}, {200, false}} {
var got bool
tt := ui.NewTester(func(c *ui.Context) { got = View(c, f, tc.maxW, DefaultStyle) }, 1200, 400)
tt.Frame()
_, drawn := tt.Access(LabelDiagram)
if got != tc.want || drawn != tc.want {
t.Errorf("maxW %v: View %v, element %v; want %v", tc.maxW, got, drawn, tc.want)
}
}
}
Loading
Loading