okami.mom/albedo

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

nekomimi.petRestore the webhooks shortcut and keep agent access visiblef51cc1a

main
4.2 KiB145 linesraw
1package tui
2
3import (
4	"albedo/cli/internal/daemon"
5	"slices"
6	"strings"
7
8	tea "charm.land/bubbletea/v2"
9)
10
11type ChatCommand struct {
12	Name        string
13	Description string
14}
15
16var CommonCommands = []ChatCommand{
17	{Name: "/a", Description: "browse sessions"},
18	{Name: "/t", Description: "show or hide thinking"},
19	{Name: "/v", Description: "expand or collapse code, output and diffs"},
20	{Name: "/status", Description: "show state, workspace and usage"},
21	{Name: "/q", Description: "quit"},
22}
23
24var AppCommands = []ChatCommand{
25	{Name: "/link", Description: "inspect and confirm linked workspace groups"},
26	{Name: "/ttl", Description: "inspect provider cache policy"},
27	{Name: "/quota", Description: "inspect observed provider quota"},
28	{Name: "/requests", Description: "inspect provider request diagnostics"},
29	{Name: "/raise-cap", Description: "raise or restore the model context cap"},
30	{Name: "/effort", Description: "choose reasoning effort"},
31	{Name: "/reload", Description: "apply selected configuration and refresh models"},
32	{Name: "/compact", Description: "compact durable context"},
33	{Name: "/kernel", Description: "upgrade the session kernel"},
34	{Name: "/model", Description: "choose a model"},
35	{Name: "/context", Description: "inspect prepared context"},
36	{Name: "/login", Description: "add or select a model provider"},
37	{Name: "/new", Description: "start a new coding session"},
38	{Name: "/sessions", Description: "switch to another session"},
39	{Name: "/agents", Description: "watch and message this session's agents (ctrl+o)"},
40	{Name: "/extensions", Description: "manage this session's extension plugins"},
41	{Name: "/skills", Description: "manage loaded skills and per-skill defaults"},
42	{Name: "/instructions", Description: "manage AGENTS.md and other instruction files"},
43	{Name: "/mcp", Description: "manage MCP servers and authentication"},
44	{Name: "/webhooks", Description: "manage signed webhooks and agent access"},
45	{Name: "/tree", Description: "branch this session from a history checkpoint"},
46	{Name: "/cd", Description: "move this session to another folder"},
47}
48
49type CommandMenuModel struct {
50	Dismissed string
51	Catalog   []daemon.SessionCommand
52	Selected  int
53}
54
55func NewCommandMenuModel() CommandMenuModel {
56	return CommandMenuModel{}
57}
58
59func (m CommandMenuModel) Matches(input string) []ChatCommand {
60	if !strings.HasPrefix(input, "/") || strings.Contains(input, "\n") || input == m.Dismissed {
61		return nil
62	}
63
64	all := slices.Clone(AppCommands)
65	for _, cmd := range m.Catalog {
66		all = append(all, ChatCommand{
67			Name:        cmd.Name,
68			Description: cmd.Description,
69		})
70	}
71	all = append(all, CommonCommands...)
72
73	// De-duplicate by name (last occurrence wins), placing exact match first.
74	seen := make(map[string]bool, len(all))
75	var direct, others []ChatCommand
76	for _, cmd := range slices.Backward(all) {
77		if seen[cmd.Name] {
78			continue
79		}
80		seen[cmd.Name] = true
81		if cmd.Name == input {
82			direct = append(direct, cmd)
83		} else if strings.HasPrefix(cmd.Name, input) {
84			others = append(others, cmd)
85		}
86	}
87	slices.Reverse(others)
88	return append(direct, others...)
89}
90
91// OnKey handles navigation within the command popup. Returns true if consumed.
92func (m *CommandMenuModel) OnKey(msg tea.KeyPressMsg, input string, replace func(string), submit func(string)) bool {
93	matches := m.Matches(input)
94	if len(matches) == 0 {
95		return false
96	}
97
98	idx := max(0, min(m.Selected, len(matches)-1))
99
100	switch msg.String() {
101	case "esc":
102		m.Dismissed = input
103	case "up":
104		if idx > 0 {
105			m.Selected = idx - 1
106		}
107	case "down":
108		if idx < len(matches)-1 {
109			m.Selected = idx + 1
110		}
111	case "tab":
112		replace(matches[idx].Name)
113	case "enter":
114		submit(matches[idx].Name)
115	default:
116		m.Selected = 0
117		return false
118	}
119	return true
120}
121
122func (m CommandMenuModel) View(input string) string {
123	matches := m.Matches(input)
124	if len(matches) == 0 {
125		return ""
126	}
127
128	idx := max(0, min(m.Selected, len(matches)-1))
129	start := max(0, idx-3)
130	end := min(len(matches), max(4, idx+1))
131
132	var b strings.Builder
133	for i := start; i < end; i++ {
134		cmd := matches[i]
135		desc := cmd.Name + "  " + DefaultStyles.Faint.Render(cmd.Description)
136		if i == idx {
137			b.WriteString(selectedLine(selectBar()+" "+desc, 0))
138		} else {
139			b.WriteString("  ")
140			b.WriteString(desc)
141		}
142		b.WriteByte('\n')
143	}
144	return b.String()
145}