v0.48.1 deaab59

resident_projection.go RawHistoryFolder

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

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