v0.0.86 43aca0b

confirm_dialog.go RawHistoryFolder

1package modes
2
3import (
4	"fmt"
5	"strings"
6	"sync"
7	"time"
8
9	"github.com/patriceckhart/zot/internal/core"
10	"github.com/patriceckhart/zot/internal/tui"
11)
12
13// confirmRequest is one pending confirmation. The agent goroutine
14// writes it onto the interactive's queue, the TUI renders the
15// dialog, and the user's answer goes back through `resp`.
16type confirmRequest struct {
17	toolName string
18	preview  string
19	resp     chan core.ConfirmDecision
20}
21
22// confirmDialog is the inline prompt shown before every tool call
23// when --no-yolo is on. It presents four options and routes the
24// answer back through the pending request's response channel.
25type confirmDialog struct {
26	mu      sync.Mutex
27	pending []*confirmRequest
28	cursor  int
29	// activeSince timestamps when the current request started
30	// showing. Used only for debug/future UX (e.g. flashing the
31	// dialog border after N seconds of no answer).
32	activeSince time.Time
33}
34
35func newConfirmDialog() *confirmDialog { return &confirmDialog{} }
36
37// Enqueue adds a pending request. Multiple requests can queue up
38// if the agent produces tool calls in parallel (via extensions);
39// the TUI shows them one at a time, in order.
40func (d *confirmDialog) Enqueue(req *confirmRequest) {
41	d.mu.Lock()
42	defer d.mu.Unlock()
43	d.pending = append(d.pending, req)
44	if len(d.pending) == 1 {
45		d.cursor = 0
46		d.activeSince = time.Now()
47	}
48}
49
50// Active reports whether the dialog is consuming input (there's at
51// least one pending request).
52func (d *confirmDialog) Active() bool {
53	if d == nil {
54		return false
55	}
56	d.mu.Lock()
57	defer d.mu.Unlock()
58	return len(d.pending) > 0
59}
60
61// CancelAll refuses every pending request (used when the user
62// cancels the active turn or closes the session while the dialog
63// is open). Safe to call when idle.
64func (d *confirmDialog) CancelAll(reason string) {
65	d.mu.Lock()
66	pending := d.pending
67	d.pending = nil
68	d.mu.Unlock()
69	for _, req := range pending {
70		select {
71		case req.resp <- core.ConfirmDecision{Allow: false, Reason: reason}:
72		default:
73		}
74	}
75}
76
77// AllowAllPending approves every pending request and drains the
78// queue. Used by /yolo so any confirmation dialog already on screen
79// resolves immediately as "yes".
80func (d *confirmDialog) AllowAllPending() {
81	d.mu.Lock()
82	pending := d.pending
83	d.pending = nil
84	d.mu.Unlock()
85	for _, req := range pending {
86		select {
87		case req.resp <- core.ConfirmDecision{Allow: true, RememberAll: true}:
88		default:
89		}
90	}
91}
92
93// confirmOptions lists the four responses in vertical order. Keyed
94// by index so HandleKey can send the right decision.
95var confirmOptions = []struct {
96	label    string
97	decision core.ConfirmDecision
98}{
99	{
100		label:    "yes (run this call)",
101		decision: core.ConfirmDecision{Allow: true},
102	},
103	{
104		label:    "yes, always this tool (skip prompts for this tool for the rest of the session)",
105		decision: core.ConfirmDecision{Allow: true, RememberTool: true},
106	},
107	{
108		label:    "yes, always (skip all prompts for the rest of the session)",
109		decision: core.ConfirmDecision{Allow: true, RememberAll: true},
110	},
111	{
112		label:    "no (refuse and let the model try something else)",
113		decision: core.ConfirmDecision{Allow: false, Reason: "user declined"},
114	},
115}
116
117// HandleKey advances the selection or resolves the dialog. Returns
118// true when a decision was sent back to the agent (the dialog may
119// remain Active if there are more queued requests behind it).
120func (d *confirmDialog) HandleKey(k tui.Key) bool {
121	d.mu.Lock()
122	if len(d.pending) == 0 {
123		d.mu.Unlock()
124		return false
125	}
126	req := d.pending[0]
127
128	switch k.Kind {
129	case tui.KeyUp:
130		if d.cursor > 0 {
131			d.cursor--
132		}
133		d.mu.Unlock()
134		return false
135	case tui.KeyDown:
136		if d.cursor < len(confirmOptions)-1 {
137			d.cursor++
138		}
139		d.mu.Unlock()
140		return false
141	case tui.KeyEsc, tui.KeyCtrlC:
142		// Treat esc/ctrl+c as "no, refuse this tool call". The
143		// active turn can separately be cancelled by the outer esc
144		// handler, but for the dialog itself, we must always answer
145		// so the agent goroutine unblocks.
146		d.pending = d.pending[1:]
147		if len(d.pending) > 0 {
148			d.cursor = 0
149			d.activeSince = time.Now()
150		}
151		d.mu.Unlock()
152		req.resp <- core.ConfirmDecision{Allow: false, Reason: "user cancelled"}
153		return true
154	case tui.KeyEnter:
155		selected := confirmOptions[d.cursor].decision
156		d.pending = d.pending[1:]
157		if len(d.pending) > 0 {
158			d.cursor = 0
159			d.activeSince = time.Now()
160		}
161		d.mu.Unlock()
162		req.resp <- selected
163		return true
164	}
165
166	// Numeric shortcuts 1..4
167	if k.Kind == tui.KeyRune && k.Rune >= '1' && k.Rune <= '4' {
168		idx := int(k.Rune - '1')
169		if idx >= 0 && idx < len(confirmOptions) {
170			selected := confirmOptions[idx].decision
171			d.pending = d.pending[1:]
172			if len(d.pending) > 0 {
173				d.cursor = 0
174				d.activeSince = time.Now()
175			}
176			d.mu.Unlock()
177			req.resp <- selected
178			return true
179		}
180	}
181
182	d.mu.Unlock()
183	return false
184}
185
186// Render draws the dialog for the head-of-queue request.
187func (d *confirmDialog) Render(th tui.Theme, width int) []string {
188	d.mu.Lock()
189	defer d.mu.Unlock()
190	if len(d.pending) == 0 {
191		return nil
192	}
193	req := d.pending[0]
194	cursor := d.cursor
195	queued := len(d.pending) - 1
196
197	var lines []string
198	title := "confirm tool call"
199	if queued > 0 {
200		title = fmt.Sprintf("confirm tool call  (%d more queued)", queued)
201	}
202	lines = append(lines, frameHeader(th, title, width))
203
204	toolLine := "  " + th.FG256(th.Tool, req.toolName)
205	if req.preview != "" {
206		toolLine += "  " + th.FG256(th.Muted, req.preview)
207	}
208	lines = append(lines, toolLine)
209	lines = append(lines, "")
210	lines = append(lines, th.FG256(th.Muted, "choose (\u2191/\u2193 or 1-4, enter to pick, esc to refuse):"))
211
212	for i, opt := range confirmOptions {
213		label := opt.label
214		prefix := fmt.Sprintf("  %d. ", i+1)
215		plain := prefix + label
216		// Truncate the tail if the line would exceed width; keeps
217		// the option numbers always visible.
218		if visibleLen(plain) > width-2 {
219			plain = plain[:width-3] + "\u2026"
220		}
221		if i == cursor {
222			lines = append(lines, th.PadHighlight(plain, width))
223		} else {
224			lines = append(lines, th.FG256(th.Muted, plain))
225		}
226	}
227	lines = append(lines, frameRule(th, width))
228	return lines
229}
230
231// visibleLen is a cheap char-count; ANSI codes aren't in the raw
232// text at this point so len() is fine.
233func visibleLen(s string) int { return strings.Count(s, "") - 1 }

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