v0.25.0 c3476a8

extproto.go RawHistoryFolder

1// Package extproto defines the JSON-over-stdin/stdout wire format
2// spoken between zut and its extension subprocesses. Both the host
3// (packages/agent/extensions) and the SDK (packages/agent/ext) marshal/
4// unmarshal the same types, so changes here ripple through both.
5//
6// All frames are one JSON object terminated by a single LF. Object
7// boundaries follow newline boundaries; no multi-line JSON.
8//
9// Direction conventions in this file:
10//   - Type names ending in "FromExt" are sent by the extension to zut.
11//   - Type names ending in "FromHost" are sent by zut to the extension.
12//   - Names without a suffix are direction-neutral payloads or shared
13//     value types.
14//
15// Every frame has a top-level Type discriminator. Optional ID is
16// present on commands and on responses to commands so the sender can
17// correlate; events and notifications never carry an ID.
18package extproto
19
20import (
21	"encoding/json"
22	"strings"
23)
24
25// ProtocolVersion is the extension protocol major. Version 2 introduces the
26// zut_version wire key and requires both sides to reject mismatched majors.
27const ProtocolVersion = 2
28
29type Frame struct {
30	Type string `json:"type"`
31	ID   string `json:"id,omitempty"`
32}
33
34type HelloFromExt struct {
35	Type            string   `json:"type"`
36	ProtocolVersion int      `json:"protocol_version"`
37	Name            string   `json:"name"`
38	Version         string   `json:"version"`
39	Capabilities    []string `json:"capabilities,omitempty"`
40}
41
42type RegisterCommandFromExt struct {
43	Type        string `json:"type"`
44	Name        string `json:"name"`
45	Description string `json:"description,omitempty"`
46}
47
48type RegisterToolFromExt struct {
49	Type        string          `json:"type"`
50	Name        string          `json:"name"`
51	Description string          `json:"description,omitempty"`
52	Schema      json.RawMessage `json:"schema"`
53	Deferred    bool            `json:"deferred,omitempty"`
54}
55
56type ReadyFromExt struct {
57	Type string `json:"type"`
58}
59
60type SubscribeFromExt struct {
61	Type      string   `json:"type"`
62	Events    []string `json:"events,omitempty"`
63	Intercept []string `json:"intercept,omitempty"`
64}
65
66type EventInterceptResponseFromExt struct {
67	Type         string          `json:"type"`
68	ID           string          `json:"id"`
69	Block        bool            `json:"block,omitempty"`
70	Reason       string          `json:"reason,omitempty"`
71	ModifiedArgs json.RawMessage `json:"modified_args,omitempty"`
72	ReplaceText  string          `json:"replace_text,omitempty"`
73	// Context is hidden, bounded context for the upcoming model turn. It is
74	// not appended to the transcript or shown as a user message.
75	Context string `json:"context,omitempty"`
76}
77
78type ToolResultFromExt struct {
79	Type          string         `json:"type"`
80	ID            string         `json:"id"`
81	Content       []ContentBlock `json:"content"`
82	IsError       bool           `json:"is_error,omitempty"`
83	ActivateTools []string       `json:"activate_tools,omitempty"`
84	// Details is extension-owned metadata. The host never includes it in
85	// provider requests, but may persist it with the active session.
86	Details json.RawMessage `json:"details,omitempty"`
87}
88
89type ContentBlock struct {
90	Type     string `json:"type"`
91	Text     string `json:"text,omitempty"`
92	MimeType string `json:"mime_type,omitempty"`
93	Data     string `json:"data,omitempty"`
94}
95
96type CommandResponseFromExt struct {
97	Type      string     `json:"type"`
98	ID        string     `json:"id"`
99	Action    string     `json:"action"`
100	Prompt    string     `json:"prompt,omitempty"`
101	Insert    string     `json:"insert,omitempty"`
102	Display   string     `json:"display,omitempty"`
103	OpenPanel *PanelSpec `json:"open_panel,omitempty"`
104	Error     string     `json:"error,omitempty"`
105}
106
107type PanelSpec struct {
108	ID     string   `json:"id"`
109	Title  string   `json:"title,omitempty"`
110	Lines  []string `json:"lines,omitempty"`
111	Footer string   `json:"footer,omitempty"`
112}
113
114// OpenPanelFromExt is a spontaneous one-way frame an extension can send at
115// any time to open an interactive panel. Unlike the open_panel action inside
116// CommandResponseFromExt, this form is uncoupled from any command invocation
117// and may be sent from a tool handler goroutine or any background context.
118type OpenPanelFromExt struct {
119	Type  string    `json:"type"` // "open_panel"
120	Panel PanelSpec `json:"panel"`
121}
122
123type PanelRenderFromExt struct {
124	Type    string   `json:"type"`
125	PanelID string   `json:"panel_id"`
126	Title   string   `json:"title,omitempty"`
127	Lines   []string `json:"lines,omitempty"`
128	Footer  string   `json:"footer,omitempty"`
129}
130
131type PanelCloseFromExt struct {
132	Type    string `json:"type"`
133	PanelID string `json:"panel_id"`
134}
135
136// StatusFromExt sets or replaces one persistent status item owned by an
137// extension. An empty text clears the item.
138type StatusFromExt struct {
139	Type  string `json:"type"`
140	Key   string `json:"key"`
141	Level string `json:"level,omitempty"`
142	Text  string `json:"text,omitempty"`
143}
144
145const (
146	WidgetPositionAboveInput = "above_input"
147	WidgetPositionRightBar   = "right_bar"
148)
149
150// NormalizeWidgetPosition returns the host's canonical widget placement.
151// Empty and unknown values intentionally retain the historical above-input
152// behavior so older extensions and future placement hints remain compatible.
153func NormalizeWidgetPosition(position string) string {
154	switch strings.ToLower(strings.TrimSpace(position)) {
155	case WidgetPositionRightBar:
156		return WidgetPositionRightBar
157	default:
158		return WidgetPositionAboveInput
159	}
160}
161
162// WidgetFromExt sets or replaces one persistent widget owned by an
163// extension. Position is a host-defined placement hint. Interactive hosts
164// support "above_input" and "right_bar"; empty and unknown values retain
165// the historical "above_input" behavior.
166type WidgetFromExt struct {
167	Type     string   `json:"type"`
168	ID       string   `json:"id"`
169	Position string   `json:"position,omitempty"`
170	Title    string   `json:"title,omitempty"`
171	Lines    []string `json:"lines,omitempty"`
172}
173
174type ClearWidgetFromExt struct {
175	Type string `json:"type"`
176	ID   string `json:"id"`
177}
178
179const AlertKindBell = "bell"
180
181// AlertRequest is a host-owned alert request. Extensions provide semantic
182// metadata; the host decides whether and how to render it.
183type AlertRequest struct {
184	Kind   string `json:"kind"`
185	Reason string `json:"reason,omitempty"`
186}
187
188// AlertFromExt is a spontaneous one-way frame an extension can send at any
189// time to request a host alert.
190type AlertFromExt struct {
191	Type string `json:"type"`
192	AlertRequest
193}
194
195type NotifyFromExt struct {
196	Type    string `json:"type"`
197	Level   string `json:"level"`
198	Message string `json:"message"`
199}
200
201// ClearNotesFromExt is a spontaneous frame an extension can send to
202// retract every note it previously pushed via notify/display, so
203// transient status lines (e.g. an approval prompt) do not stack up
204// forever in the bottom-sticky notes block.
205type ClearNotesFromExt struct {
206	Type string `json:"type"`
207}
208
209// SubmitSlashFromExt is a spontaneous frame an extension can send at
210// any time (typically from a panel_key handler) to invoke a slash
211// command in the host's TUI as if the user had typed it. Text must
212// start with '/'. Reserved for internal / opt-in extensions today;
213// the wire format is stable but not yet exposed in the public docs.
214type SubmitSlashFromExt struct {
215	Type string `json:"type"`
216	Text string `json:"text"`
217}
218
219// SubmitFromExt is a spontaneous frame an extension can send to queue
220// a model prompt in the interactive host. The manager routes it through
221// HostHooks.Submit, so the TUI handles busy state and turn startup.
222type SubmitFromExt struct {
223	Type string `json:"type"`
224	Text string `json:"text"`
225}
226
227type ShutdownAckFromExt struct {
228	Type string `json:"type"`
229}
230
231type HelloAckFromHost struct {
232	Type            string          `json:"type"`
233	ProtocolVersion int             `json:"protocol_version"`
234	ZutVersion      string          `json:"zut_version"`
235	Provider        string          `json:"provider"`
236	Model           string          `json:"model"`
237	CWD             string          `json:"cwd"`
238	ExtensionDir    string          `json:"extension_dir,omitempty"`
239	DataDir         string          `json:"data_dir,omitempty"`
240	Session         *SessionContext `json:"session,omitempty"`
241}
242
243// SessionContext identifies the active transcript branch. ID is the current
244// session/branch id; ParentID is empty for a root session.
245type SessionContext struct {
246	ID        string `json:"id"`
247	ParentID  string `json:"parent_id,omitempty"`
248	Path      string `json:"path,omitempty"`
249	CWD       string `json:"cwd,omitempty"`
250	ForkPoint int    `json:"fork_point,omitempty"`
251}
252
253type CommandInvokedFromHost struct {
254	Type string `json:"type"`
255	ID   string `json:"id"`
256	Name string `json:"name"`
257	Args string `json:"args,omitempty"`
258}
259
260type ToolCallFromHost struct {
261	Type string          `json:"type"`
262	ID   string          `json:"id"`
263	Name string          `json:"name"`
264	Args json.RawMessage `json:"args"`
265}
266
267type EventFromHost struct {
268	Type        string          `json:"type"`
269	Event       string          `json:"event"`
270	Step        int             `json:"step,omitempty"`
271	Stop        string          `json:"stop,omitempty"`
272	Error       string          `json:"error,omitempty"`
273	ToolID      string          `json:"tool_id,omitempty"`
274	ToolName    string          `json:"tool_name,omitempty"`
275	ToolArgs    json.RawMessage `json:"tool_args,omitempty"`
276	ToolPreview string          `json:"tool_preview,omitempty"`
277	Text        string          `json:"text,omitempty"`
278	Session     *SessionContext `json:"session,omitempty"`
279	// State is private to the receiving extension. The manager selects the
280	// matching extension-owned snapshot before sending the event.
281	State json.RawMessage `json:"state,omitempty"`
282}
283
284type EventInterceptFromHost struct {
285	Type     string          `json:"type"`
286	ID       string          `json:"id"`
287	Event    string          `json:"event"`
288	ToolID   string          `json:"tool_id,omitempty"`
289	ToolName string          `json:"tool_name,omitempty"`
290	ToolArgs json.RawMessage `json:"tool_args,omitempty"`
291	Step     int             `json:"step,omitempty"`
292	Text     string          `json:"text,omitempty"`
293}
294
295type PanelKeyFromHost struct {
296	Type    string `json:"type"`
297	PanelID string `json:"panel_id"`
298	Key     string `json:"key"`
299	Text    string `json:"text,omitempty"`
300}
301
302type PanelResizeFromHost struct {
303	Type    string `json:"type"`
304	PanelID string `json:"panel_id"`
305	Width   int    `json:"width"`
306	Height  int    `json:"height"`
307}
308
309type PanelCloseFromHost struct {
310	Type    string `json:"type"`
311	PanelID string `json:"panel_id"`
312}
313
314type ShutdownFromHost struct {
315	Type string `json:"type"`
316}
317
318func Encode(v any) ([]byte, error) {
319	b, err := json.Marshal(v)
320	if err != nil {
321		return nil, err
322	}
323	return append(b, '\n'), nil
324}

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