detached 45bfb16

Merge pull request #18 from bnema/fix/taskede-phases-install

fix(tasked-phases): hide empty persistent chrome
commit
45bfb161c91804616d645913a3c9e6258578b14d
author
bnema · · 2026-08-05 04:41 UTC
committer
GitHub · · 2026-08-05 04:41 UTC
merge of
dae7738 + 1839ad7 · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
2 files · +109 -5
M examples/extensions/tasked-phases/main.go +41 -5
@@ -211,15 +211,51 @@ func (a *app) publishUnavailableChrome() {
211211 }
212212 }
213213
214-func (a *app) publishChrome(state PlanState) {
214+type chromeContent struct {
215+ status string
216+ lines []string
217+}
218+
219+func buildChrome(state PlanState) (chromeContent, bool) {
220+ // An empty state is the normal idle state, not useful persistent chrome.
221+ if !hasStoredPlan(state) {
222+ return chromeContent{}, false
223+ }
224+
225+ status := ""
215226 if isPlanClosed(state) {
216- a.ext.SetStatus("progress", "closed: "+state.ClosedSummary)
227+ status = "closed: " + state.ClosedSummary
217228 } else if done, total := getPlanProgress(state); total > 0 {
218- a.ext.SetStatus("progress", fmt.Sprintf("%d/%d tasks checked", done, total))
229+ status = fmt.Sprintf("%d/%d tasks checked", done, total)
219230 } else {
220- a.ext.SetStatus("progress", "no active plan")
231+ status = "no active plan"
221232 }
222- a.ext.SetWidget("plan", "above_input", "Tasked phases", strings.Split(buildCompactSummary(state), "\n"))
233+ return chromeContent{
234+ status: status,
235+ lines: strings.Split(buildCompactSummary(state), "\n"),
236+ }, true
237+}
238+
239+type chromeHost interface {
240+ SetStatus(key, text string)
241+ ClearStatus(key string)
242+ SetWidget(id, position, title string, lines []string)
243+ ClearWidget(id string)
244+}
245+
246+func applyChrome(host chromeHost, state PlanState) {
247+ chrome, ok := buildChrome(state)
248+ if !ok {
249+ host.ClearStatus("progress")
250+ host.ClearWidget("plan")
251+ return
252+ }
253+ host.SetStatus("progress", chrome.status)
254+ host.SetWidget("plan", "above_input", "Tasked phases", chrome.lines)
255+}
256+
257+func (a *app) publishChrome(state PlanState) {
258+ applyChrome(a.ext, state)
223259 }
224260
225261 func toolResult(action string, state PlanState, headline string, err error) ext.ToolResult {
@@ -211,15 +211,51 @@ func (a *app) publishUnavailableChrome() {
211 }211 }
212 }212 }
213 213
214-func (a *app) publishChrome(state PlanState) {214+type chromeContent struct {
215+ status string
216+ lines []string
217+}
218+
219+func buildChrome(state PlanState) (chromeContent, bool) {
220+ // An empty state is the normal idle state, not useful persistent chrome.
221+ if !hasStoredPlan(state) {
222+ return chromeContent{}, false
223+ }
224+
225+ status := ""
215 if isPlanClosed(state) {226 if isPlanClosed(state) {
216- a.ext.SetStatus("progress", "closed: "+state.ClosedSummary)227+ status = "closed: " + state.ClosedSummary
217 } else if done, total := getPlanProgress(state); total > 0 {228 } else if done, total := getPlanProgress(state); total > 0 {
218- a.ext.SetStatus("progress", fmt.Sprintf("%d/%d tasks checked", done, total))229+ status = fmt.Sprintf("%d/%d tasks checked", done, total)
219 } else {230 } else {
220- a.ext.SetStatus("progress", "no active plan")231+ status = "no active plan"
221 }232 }
222- a.ext.SetWidget("plan", "above_input", "Tasked phases", strings.Split(buildCompactSummary(state), "\n"))233+ return chromeContent{
234+ status: status,
235+ lines: strings.Split(buildCompactSummary(state), "\n"),
236+ }, true
237+}
238+
239+type chromeHost interface {
240+ SetStatus(key, text string)
241+ ClearStatus(key string)
242+ SetWidget(id, position, title string, lines []string)
243+ ClearWidget(id string)
244+}
245+
246+func applyChrome(host chromeHost, state PlanState) {
247+ chrome, ok := buildChrome(state)
248+ if !ok {
249+ host.ClearStatus("progress")
250+ host.ClearWidget("plan")
251+ return
252+ }
253+ host.SetStatus("progress", chrome.status)
254+ host.SetWidget("plan", "above_input", "Tasked phases", chrome.lines)
255+}
256+
257+func (a *app) publishChrome(state PlanState) {
258+ applyChrome(a.ext, state)
223 }259 }
224 260
225 func toolResult(action string, state PlanState, headline string, err error) ext.ToolResult {261 func toolResult(action string, state PlanState, headline string, err error) ext.ToolResult {
A examples/extensions/tasked-phases/main_test.go +68 -0
@@ -0,0 +1,68 @@
1+package main
2+
3+import (
4+ "strings"
5+ "testing"
6+)
7+
8+func TestBuildChromeHidesEmptyPlan(t *testing.T) {
9+ chrome, visible := buildChrome(createEmptyState())
10+ if visible {
11+ t.Fatal("empty plan should not publish persistent chrome")
12+ }
13+ if chrome.status != "" || len(chrome.lines) != 0 {
14+ t.Fatalf("empty chrome = %#v, want zero content", chrome)
15+ }
16+}
17+
18+func TestBuildChromeShowsStoredPlan(t *testing.T) {
19+ state := PlanState{
20+ Version: 1,
21+ Spec: "Ship the compact view",
22+ Phases: []Phase{{
23+ ID: "phase-1",
24+ Title: "Implement",
25+ Tasks: []PhaseTask{{ID: "task-1", Text: "Hide empty state"}},
26+ }},
27+ }
28+
29+ chrome, visible := buildChrome(state)
30+ if !visible {
31+ t.Fatal("stored plan should publish persistent chrome")
32+ }
33+ if chrome.status != "0/1 tasks checked" {
34+ t.Fatalf("chrome status = %q, want %q", chrome.status, "0/1 tasks checked")
35+ }
36+ joined := strings.Join(chrome.lines, "\n")
37+ for _, want := range []string{"Spec: Ship the compact view", "Progress: 0/1 tasks checked", "Current phase: Implement"} {
38+ if !strings.Contains(joined, want) {
39+ t.Fatalf("chrome lines missing %q: %s", want, joined)
40+ }
41+ }
42+}
43+
44+type chromeRecorder struct {
45+ statusSet int
46+ statusClear int
47+ widgetSet int
48+ widgetClear int
49+}
50+
51+func (r *chromeRecorder) SetStatus(string, string) { r.statusSet++ }
52+func (r *chromeRecorder) ClearStatus(string) { r.statusClear++ }
53+func (r *chromeRecorder) SetWidget(string, string, string, []string) {
54+ r.widgetSet++
55+}
56+func (r *chromeRecorder) ClearWidget(string) { r.widgetClear++ }
57+
58+func TestApplyChromeClearsEmptyPlan(t *testing.T) {
59+ recorder := &chromeRecorder{}
60+ applyChrome(recorder, createEmptyState())
61+
62+ if recorder.statusClear != 1 || recorder.widgetClear != 1 {
63+ t.Fatalf("empty plan clear operations = status %d, widget %d; want one each", recorder.statusClear, recorder.widgetClear)
64+ }
65+ if recorder.statusSet != 0 || recorder.widgetSet != 0 {
66+ t.Fatalf("empty plan published updates = status %d, widget %d; want none", recorder.statusSet, recorder.widgetSet)
67+ }
68+}
@@ -0,0 +1,68 @@
1+package main
2+
3+import (
4+ "strings"
5+ "testing"
6+)
7+
8+func TestBuildChromeHidesEmptyPlan(t *testing.T) {
9+ chrome, visible := buildChrome(createEmptyState())
10+ if visible {
11+ t.Fatal("empty plan should not publish persistent chrome")
12+ }
13+ if chrome.status != "" || len(chrome.lines) != 0 {
14+ t.Fatalf("empty chrome = %#v, want zero content", chrome)
15+ }
16+}
17+
18+func TestBuildChromeShowsStoredPlan(t *testing.T) {
19+ state := PlanState{
20+ Version: 1,
21+ Spec: "Ship the compact view",
22+ Phases: []Phase{{
23+ ID: "phase-1",
24+ Title: "Implement",
25+ Tasks: []PhaseTask{{ID: "task-1", Text: "Hide empty state"}},
26+ }},
27+ }
28+
29+ chrome, visible := buildChrome(state)
30+ if !visible {
31+ t.Fatal("stored plan should publish persistent chrome")
32+ }
33+ if chrome.status != "0/1 tasks checked" {
34+ t.Fatalf("chrome status = %q, want %q", chrome.status, "0/1 tasks checked")
35+ }
36+ joined := strings.Join(chrome.lines, "\n")
37+ for _, want := range []string{"Spec: Ship the compact view", "Progress: 0/1 tasks checked", "Current phase: Implement"} {
38+ if !strings.Contains(joined, want) {
39+ t.Fatalf("chrome lines missing %q: %s", want, joined)
40+ }
41+ }
42+}
43+
44+type chromeRecorder struct {
45+ statusSet int
46+ statusClear int
47+ widgetSet int
48+ widgetClear int
49+}
50+
51+func (r *chromeRecorder) SetStatus(string, string) { r.statusSet++ }
52+func (r *chromeRecorder) ClearStatus(string) { r.statusClear++ }
53+func (r *chromeRecorder) SetWidget(string, string, string, []string) {
54+ r.widgetSet++
55+}
56+func (r *chromeRecorder) ClearWidget(string) { r.widgetClear++ }
57+
58+func TestApplyChromeClearsEmptyPlan(t *testing.T) {
59+ recorder := &chromeRecorder{}
60+ applyChrome(recorder, createEmptyState())
61+
62+ if recorder.statusClear != 1 || recorder.widgetClear != 1 {
63+ t.Fatalf("empty plan clear operations = status %d, widget %d; want one each", recorder.statusClear, recorder.widgetClear)
64+ }
65+ if recorder.statusSet != 0 || recorder.widgetSet != 0 {
66+ t.Fatalf("empty plan published updates = status %d, widget %d; want none", recorder.statusSet, recorder.widgetSet)
67+ }
68+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 477

↑↓ moveenter openesc close