v0.17.0 949637b

resident_projection.go RawHistoryFolder

1package subagents
2
3import (
4	"sync"
5
6	"github.com/bnema/zut/packages/core"
7)
8
9const residentLiveArgumentBytes = 64 << 10
10
11// ResidentLiveTool is the bounded in-memory state of one active tool call.
12// It is never written to the journal; finalized calls and results are served
13// from the authoritative transcript instead.
14type ResidentLiveTool struct {
15	ID    string
16	Name  string
17	Args  []byte
18	State ResidentLiveToolState
19}
20
21type ResidentLiveToolState string
22
23const (
24	ResidentLiveToolComposing ResidentLiveToolState = "composing"
25	ResidentLiveToolReady     ResidentLiveToolState = "ready"
26	ResidentLiveToolRunning   ResidentLiveToolState = "running"
27)
28
29// ResidentLiveSnapshot is an immutable copy of the unfinished visible turn.
30// Hidden reasoning and unbounded tool output deliberately do not belong here.
31type ResidentLiveSnapshot struct {
32	TurnID          string
33	State           ResidentState
34	Revision        uint64
35	WaitingForModel bool
36	AssistantText   string
37	Tools           []ResidentLiveTool
38}
39
40type residentLiveProjection struct {
41	mu       sync.RWMutex
42	snapshot ResidentLiveSnapshot
43}
44
45func newResidentLiveProjection() *residentLiveProjection { return &residentLiveProjection{} }
46
47func (p *residentLiveProjection) Start(turnID string) {
48	if p == nil {
49		return
50	}
51	p.mu.Lock()
52	p.snapshot.TurnID = turnID
53	p.snapshot.State = ResidentRunning
54	p.snapshot.WaitingForModel = true
55	p.snapshot.AssistantText = ""
56	p.snapshot.Tools = nil
57	p.snapshot.Revision++
58	p.mu.Unlock()
59}
60
61func (p *residentLiveProjection) Finish(state ResidentState) {
62	if p == nil {
63		return
64	}
65	p.mu.Lock()
66	p.snapshot.State = state
67	p.snapshot.WaitingForModel = false
68	p.snapshot.AssistantText = ""
69	p.snapshot.Tools = nil
70	p.snapshot.Revision++
71	p.mu.Unlock()
72}
73
74func (p *residentLiveProjection) Apply(event core.AgentEvent) {
75	if p == nil || event == nil {
76		return
77	}
78	p.mu.Lock()
79	defer p.mu.Unlock()
80	changed := true
81	switch value := event.(type) {
82	case core.EvRequestStarted:
83		p.snapshot.WaitingForModel = true
84	case core.EvAssistantStart:
85		p.snapshot.WaitingForModel = true
86	case core.EvTextDelta:
87		p.snapshot.WaitingForModel = false
88		if len(p.snapshot.AssistantText) < residentLiveArgumentBytes {
89			remaining := residentLiveArgumentBytes - len(p.snapshot.AssistantText)
90			if len(value.Delta) > remaining {
91				value.Delta = value.Delta[:remaining]
92			}
93			p.snapshot.AssistantText += value.Delta
94		}
95	case core.EvToolUseStart:
96		p.snapshot.WaitingForModel = false
97		p.upsertTool(value.ID, value.Name, ResidentLiveToolComposing)
98	case core.EvToolUseArgs:
99		p.snapshot.WaitingForModel = false
100		if tool := p.tool(value.ID); tool != nil && len(tool.Args) < residentLiveArgumentBytes {
101			remaining := residentLiveArgumentBytes - len(tool.Args)
102			if len(value.Delta) > remaining {
103				value.Delta = value.Delta[:remaining]
104			}
105			tool.Args = append(tool.Args, value.Delta...)
106		}
107	case core.EvToolCall:
108		p.snapshot.WaitingForModel = false
109		tool := p.upsertTool(value.ID, value.Name, ResidentLiveToolReady)
110		tool.Args = append(tool.Args[:0], value.Args...)
111		if len(tool.Args) > residentLiveArgumentBytes {
112			tool.Args = tool.Args[:residentLiveArgumentBytes]
113		}
114	case core.EvToolExecutionStarted:
115		p.snapshot.WaitingForModel = false
116		p.upsertTool(value.ID, value.Name, ResidentLiveToolRunning)
117	case core.EvToolResult:
118		p.snapshot.WaitingForModel = false
119		for index := range p.snapshot.Tools {
120			if p.snapshot.Tools[index].ID == value.ID {
121				p.snapshot.Tools = append(p.snapshot.Tools[:index], p.snapshot.Tools[index+1:]...)
122				break
123			}
124		}
125	case core.EvAssistantMessage:
126		p.snapshot.WaitingForModel = false
127		p.snapshot.AssistantText = ""
128	default:
129		changed = false
130	}
131	if changed {
132		p.snapshot.Revision++
133	}
134}
135
136func (p *residentLiveProjection) Snapshot() ResidentLiveSnapshot {
137	if p == nil {
138		return ResidentLiveSnapshot{}
139	}
140	p.mu.RLock()
141	snapshot := p.snapshot
142	snapshot.Tools = make([]ResidentLiveTool, len(p.snapshot.Tools))
143	for index, tool := range p.snapshot.Tools {
144		snapshot.Tools[index] = tool
145		snapshot.Tools[index].Args = append([]byte(nil), tool.Args...)
146	}
147	p.mu.RUnlock()
148	return snapshot
149}
150
151func (p *residentLiveProjection) tool(id string) *ResidentLiveTool {
152	for index := range p.snapshot.Tools {
153		if p.snapshot.Tools[index].ID == id {
154			return &p.snapshot.Tools[index]
155		}
156	}
157	return nil
158}
159
160func (p *residentLiveProjection) upsertTool(id, name string, state ResidentLiveToolState) *ResidentLiveTool {
161	if tool := p.tool(id); tool != nil {
162		if name != "" {
163			tool.Name = name
164		}
165		tool.State = state
166		return tool
167	}
168	p.snapshot.Tools = append(p.snapshot.Tools, ResidentLiveTool{ID: id, Name: name, State: state})
169	return &p.snapshot.Tools[len(p.snapshot.Tools)-1]
170}

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