v0.3.76 1866c03

main.go RawHistoryFolder

1// approve — demonstrates the spontaneous open_panel pattern for
2// human-in-the-loop tool approval gates.
3//
4// Registers one LLM-callable tool:
5//
6//	approve_action(action: string, reason: string)
7//
8// When the model calls it, a panel opens asking the user to approve
9// or deny. The tool goroutine blocks until the user responds; the
10// model only sees the result after the user has acted.
11//
12// Build:
13//
14//	cd examples/extensions/approve
15//	go build -o approve .
16//
17// Install:
18//
19//	zot ext install .
20//
21// Try it — ask zot something like:
22//
23//	"Request approval to delete the temp directory."
24//
25// The model will call approve_action; a panel appears in the TUI. Press
26// y to approve or n (or esc) to deny. The model receives "approved" or
27// "denied: user rejected the action" as the tool result and responds
28// accordingly.
29package main
30
31import (
32	"encoding/json"
33	"fmt"
34	"os"
35	"strings"
36	"sync"
37
38	"github.com/patriceckhart/zot/packages/agent/ext"
39)
40
41const schema = `{
42  "type": "object",
43  "properties": {
44    "action": {
45      "type": "string",
46      "description": "Short description of what is about to happen, shown to the user."
47    },
48    "reason": {
49      "type": "string",
50      "description": "Why the action is needed, shown to the user."
51    }
52  },
53  "required": ["action"]
54}`
55
56func main() {
57	e := ext.New("approve", "1.0.0")
58
59	// counter is used to generate unique panel IDs so that concurrent
60	// tool calls (unlikely in practice but possible) don't collide.
61	var mu sync.Mutex
62	var counter int
63	nextPID := func() string {
64		mu.Lock()
65		defer mu.Unlock()
66		counter++
67		return fmt.Sprintf("approve-%d", counter)
68	}
69
70	e.Tool("approve_action", "Ask the user to approve or deny an action before it proceeds.", json.RawMessage(schema),
71		func(args json.RawMessage) ext.ToolResult {
72			var in struct {
73				Action string `json:"action"`
74				Reason string `json:"reason"`
75			}
76			if err := json.Unmarshal(args, &in); err != nil {
77				return ext.TextErrorResult("invalid args: " + err.Error())
78			}
79			if strings.TrimSpace(in.Action) == "" {
80				return ext.TextErrorResult("action is required")
81			}
82
83			pid := nextPID()
84			decision := make(chan bool, 1)
85
86			buildLines := func(hint string) []string {
87				lines := []string{
88					"  Action:  " + in.Action,
89				}
90				if strings.TrimSpace(in.Reason) != "" {
91					lines = append(lines, "  Reason:  "+in.Reason)
92				}
93				lines = append(lines, "")
94				lines = append(lines, "  "+hint)
95				return lines
96			}
97
98			const footer = "● this panel has focus — y approve  n deny  esc cancel"
99			const prompt = "› approve this action? [y/n]"
100			const badKey = "› unrecognised key — press y to approve or n to deny"
101
102			// Register key handler before opening the panel so there
103			// is no window where a key could arrive unhandled.
104			e.OnPanelKey(pid, func(key, text string) {
105				switch {
106				case key == "rune" && strings.ToLower(text) == "y":
107					e.ClosePanel(pid)
108					decision <- true
109				case key == "rune" && strings.ToLower(text) == "n",
110					key == "esc":
111					e.ClosePanel(pid)
112					decision <- false
113				default:
114					// Unknown key: re-render with a nudge so the user
115					// knows the panel is alive and what to press.
116					e.RenderPanel(pid, "Approval required",
117						buildLines(badKey), footer)
118				}
119			}, func() {
120				// Host closed the panel (e.g. user navigated away).
121				select {
122				case decision <- false:
123				default:
124				}
125			})
126
127			// Open the panel spontaneously from inside the tool handler.
128			e.OpenPanel(pid, "Approval required", buildLines(prompt), footer)
129
130			// Block until the user responds.
131			if <-decision {
132				return ext.TextResult("approved")
133			}
134			return ext.TextErrorResult("denied: user rejected the action")
135		})
136
137	if err := e.Run(); err != nil {
138		e.Logf("fatal: %v", err)
139		os.Exit(1)
140	}
141}

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 476

↑↓ moveenter openesc close