okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

dawntui: skills, instructions and mcp on listView with global/session toggles and a pane without enter96704f7

main
8.4 KiB137 linesraw
1package tui
2
3import (
4	"albedo/cli/internal/config"
5	"albedo/cli/internal/daemon"
6	"errors"
7
8	tea "charm.land/bubbletea/v2"
9)
10
11// The capability pages' screenshot states: each page's list, filter, odd
12// rows, confirmations, saves, forms, and load outcomes.
13func init() {
14	enter := tea.KeyPressMsg{Code: tea.KeyEnter}
15	shiftEnter := tea.KeyPressMsg{Code: tea.KeyEnter, Mod: tea.ModShift}
16	down := tea.KeyPressMsg{Code: tea.KeyDown}
17	chord := func(r rune) tea.Msg { return tea.KeyPressMsg{Code: r, Mod: tea.ModCtrl} }
18	typed := func(text string) []tea.Msg {
19		var keys []tea.Msg
20		for _, r := range text {
21			keys = append(keys, tea.KeyPressMsg{Code: r, Text: string(r)})
22		}
23		return keys
24	}
25	saveFailed := func(kind string, items []capabilityItem, keys ...tea.Msg) func(int, int) string {
26		return func(w, h int) string {
27			m := capsModel(kind, w, h, items, true, nil, keys...)
28			m, _ = m.Update(capabilitySavedMsg{Gen: m.Generation, Err: errors.New("daemon refused the change")})
29			return m.View()
30		}
31	}
32	skills := capsSkills()
33	servers := capsServers()
34	registerShots("caps",
35		shotState{"skills-list", capsShot("skills", skills, true, capsDiagnostics)},
36		shotState{"skills-cursor-overridden", capsShot("skills", skills, true, nil, down)},
37		shotState{"skills-cursor-shadowed", capsShot("skills", skills, true, nil, down, down, down)},
38		shotState{"skills-cursor-invalid", capsShot("skills", skills, true, nil, down, down, down, down)},
39		shotState{"skills-filtering", capsShot("skills", skills, true, nil, typed("notes")...)},
40		shotState{"skills-no-match", capsShot("skills", skills, true, nil, typed("zzz")...)},
41		shotState{"skills-confirm-global", capsShot("skills", skills, true, nil, enter)},
42		shotState{"skills-confirm-session", capsShot("skills", skills, true, nil, shiftEnter)},
43		shotState{"skills-confirm-follow-global", capsShot("skills", skills, true, nil, down, chord('x'))},
44		shotState{"skills-saving", capsShot("skills", skills, true, nil, shiftEnter, enter)},
45		shotState{"skills-save-failed", saveFailed("skills", skills, shiftEnter, enter)},
46		shotState{"skills-extension-off", capsShot("skills", skills, false, nil)},
47		shotState{"skills-confirm-extension", capsShot("skills", skills, false, nil, chord('t'))},
48		shotState{"skills-notice", func(w, h int) string {
49			m := capsModel("skills", w, h, skills, true, nil)
50			m.Notice = "Saved. Reload the session to apply the selection."
51			return m.View()
52		}},
53		shotState{"skills-loading", func(w, h int) string {
54			m := NewCapabilityPageModel(nil, "s", "skills")
55			m.SetSize(w, h)
56			return m.View()
57		}},
58		shotState{"skills-empty", capsShot("skills", nil, true, nil)},
59		shotState{"skills-load-failed", func(w, h int) string {
60			m := NewCapabilityPageModel(nil, "s", "skills")
61			m.SetSize(w, h)
62			m, _ = m.Update(capabilityLoadedMsg{Gen: m.Generation, Err: errors.New("daemon connection unavailable")})
63			return m.View()
64		}},
65		shotState{"instructions-list", capsShot("instructions", capsInstructions(), true, nil)},
66		shotState{"mcp-list", capsShot("mcp", servers, true, nil)},
67		shotState{"mcp-cursor-disabled", capsShot("mcp", servers, true, nil, down)},
68		shotState{"mcp-filtering", capsShot("mcp", servers, true, nil, typed("fil")...)},
69		shotState{"mcp-no-match", capsShot("mcp", servers, true, nil, typed("zzz")...)},
70		shotState{"mcp-confirm-global", capsShot("mcp", servers, true, nil, enter)},
71		shotState{"mcp-confirm-session", capsShot("mcp", servers, true, nil, shiftEnter)},
72		shotState{"mcp-enable-server", capsShot("mcp", servers, true, nil, down, chord('g'))},
73		shotState{"mcp-form-add", capsShot("mcp", servers, true, nil, chord('o'))},
74		shotState{"mcp-form-add-typed", capsShot("mcp", servers, true, nil, chord('o'), tea.KeyPressMsg{Code: tea.KeyExtended, Text: "http://127.0.0.1:9/mcp"})},
75		shotState{"mcp-form-invalid", capsShot("mcp", servers, true, nil, chord('o'), chord('s'))},
76		shotState{"mcp-form-edit", capsShot("mcp", servers, true, nil, chord('e'))},
77		shotState{"mcp-form-saving", func(w, h int) string {
78			m := capsModel("mcp", w, h, servers, true, nil, chord('o'), tea.KeyPressMsg{Code: tea.KeyExtended, Text: "http://127.0.0.1:9/mcp"}, chord('s'))
79			return m.View()
80		}},
81		shotState{"mcp-form-save-failed", saveFailed("mcp", servers, chord('o'), tea.KeyPressMsg{Code: tea.KeyExtended, Text: "http://127.0.0.1:9/mcp"}, chord('s'))},
82		shotState{"mcp-confirm-delete", capsShot("mcp", servers, true, nil, chord('d'))},
83		shotState{"mcp-delete-save-failed", saveFailed("mcp", servers, chord('d'), enter)},
84		shotState{"mcp-confirm-extension", capsShot("mcp", servers, false, nil, chord('t'))},
85		shotState{"mcp-extension-off", capsShot("mcp", servers, false, nil)},
86		shotState{"mcp-empty", capsShot("mcp", nil, true, nil)},
87	)
88}
89
90// capsModel is a loaded page of one kind with keys pressed on it.
91func capsModel(kind string, width, height int, items []capabilityItem, extension bool, diagnostics []string, keys ...tea.Msg) CapabilityPageModel {
92	m := NewCapabilityPageModel(nil, "s", kind)
93	m.SetSize(width, height)
94	m, _ = m.Update(capabilityLoadedMsg{Gen: m.Generation, ExtensionEnabled: extension, Revision: "rev", Diagnostics: diagnostics, Items: items})
95	for _, key := range keys {
96		m, _ = m.Update(key)
97	}
98	return m
99}
100
101func capsShot(kind string, items []capabilityItem, extension bool, diagnostics []string, keys ...tea.Msg) func(int, int) string {
102	return func(width, height int) string {
103		return capsModel(kind, width, height, items, extension, diagnostics, keys...).View()
104	}
105}
106
107var capsDiagnostics = []string{"skipped one unreadable directory: ~/.agents/skills/old"}
108
109func capsSkills() []capabilityItem {
110	key := "draft"
111	on, off := true, false
112	return []capabilityItem{
113		{ID: "enable-review", Title: "enable-review", Candidate: daemon.CatalogCandidate{ID: "enable-review", Source: "/work/.agents/skills/enable-review/SKILL.md", Description: new("verify enabling the skills extension"), PreferenceKey: &key, Valid: true, EffectiveEnabled: true, Eligible: true, GlobalPreference: &on}},
114		{ID: "lint-rules", Title: "lint-rules", Candidate: daemon.CatalogCandidate{ID: "lint-rules", Source: "/home/dawn/.agents/skills/lint-rules/SKILL.md", Description: new("house lint rules for go, kept quiet on generated code"), PreferenceKey: &key, Valid: true, GlobalPreference: &on, SessionOverride: &off, Eligible: false}},
115		{ID: "release-notes", Title: "release-notes", Candidate: daemon.CatalogCandidate{ID: "release-notes", Source: "/home/dawn/.agents/skills/release-notes/SKILL.md", Description: new("draft changelog entries from commits"), PreferenceKey: &key, Valid: true, GlobalPreference: &off, SessionOverride: &on, EffectiveEnabled: true, Eligible: true}},
116		{ID: "tangled", Title: "tangled", Candidate: daemon.CatalogCandidate{ID: "tangled", Source: "/home/dawn/.agents/skills/tangled/SKILL.md", Description: new("tg cli for tangled tasks"), PreferenceKey: &key, Valid: true, ShadowedBy: new("tangled-local"), GlobalPreference: &on}},
117		{ID: "broken", Title: "broken", Candidate: daemon.CatalogCandidate{ID: "broken", Source: "/work/.agents/skills/broken/SKILL.md", Description: new("missing frontmatter"), Valid: false, Diagnostic: new("frontmatter has no name")}},
118	}
119}
120
121func capsInstructions() []capabilityItem {
122	key := "agents"
123	on := true
124	return []capabilityItem{
125		{ID: "AGENTS.md", Title: "AGENTS.md", Candidate: daemon.CatalogCandidate{ID: "AGENTS.md", Source: "/work/AGENTS.md", Description: new("workspace conventions"), PreferenceKey: &key, Valid: true, GlobalPreference: &on, EffectiveEnabled: true, Eligible: true}},
126		{ID: "CLAUDE.md", Title: "CLAUDE.md", Candidate: daemon.CatalogCandidate{ID: "CLAUDE.md", Source: "/home/dawn/.claude/CLAUDE.md", Description: new("personal notes, shadowed by AGENTS.md"), PreferenceKey: &key, Valid: true, ShadowedBy: new("AGENTS.md")}},
127	}
128}
129
130func capsServers() []capabilityItem {
131	on, off := true, false
132	return []capabilityItem{
133		{ID: "docs", Title: "docs", Detail: "http · https://docs.example/mcp", Server: config.MCPServer{Type: "http", URL: "https://docs.example/mcp", Enabled: &on, StartupTimeoutMs: 8000}, Secrets: daemon.MCPSecretNames{BearerToken: true, Headers: []string{"X-Team"}}},
134		{ID: "filesystem", Title: "filesystem", Detail: "stdio · npx", Server: config.MCPServer{Type: "stdio", Command: "npx", Args: []string{"-y", "@modelcontextprotocol/server-filesystem", "/tmp/notes"}, Enabled: &off, EnabledTools: []string{"read_file"}}},
135		{ID: "local", Title: "local", Detail: "http · http://127.0.0.1:8787/mcp", Server: config.MCPServer{Type: "http", URL: "http://127.0.0.1:8787/mcp"}},
136	}
137}