v0.31.0 9467429

activity.go RawHistoryFolder

1package modes
2
3import (
4	"strconv"
5	"strings"
6	"time"
7	"unicode"
8
9	"github.com/bnema/zut/packages/core"
10)
11
12// activityKind describes a user-visible operation. It is deliberately local
13// to interactive modes: public consumers receive factual core events instead
14// of rendered UI labels.
15type activityKind string
16
17const (
18	activityIdle                 activityKind = "idle"
19	activityPreparingRequest     activityKind = "preparing_request"
20	activitySendingRequest       activityKind = "sending_request"
21	activityWaitingForResponse   activityKind = "waiting_for_response"
22	activityReceivingResponse    activityKind = "receiving_response"
23	activityPreparingTool        activityKind = "preparing_tool"
24	activityAwaitingConfirmation activityKind = "awaiting_confirmation"
25	activityRunningTool          activityKind = "running_tool"
26	activitySendingToolResults   activityKind = "sending_tool_results"
27	activityRetryingRequest      activityKind = "retrying_request"
28	activityCompactingHistory    activityKind = "compacting_history"
29	activityCondensingHistory    activityKind = "condensing_history"
30	activityRunningShellCommand  activityKind = "running_shell_command"
31)
32
33type activity struct {
34	kind     activityKind
35	provider string
36	model    string
37	tool     string
38	attempt  int
39	maxTries int
40	retryIn  time.Duration
41}
42
43func (a activity) label() string {
44	switch a.kind {
45	case activityPreparingRequest:
46		return "Preparing request"
47	case activitySendingRequest:
48		return "Sending request to " + activityText(a.provider, "provider")
49	case activityWaitingForResponse:
50		return "Waiting for " + activityText(a.model, "model") + " to respond"
51	case activityReceivingResponse:
52		return "Receiving response from " + activityText(a.model, "model")
53	case activityPreparingTool:
54		return "Preparing tool: " + activityText(a.tool, "tool")
55	case activityAwaitingConfirmation:
56		return "Waiting for approval: " + activityText(a.tool, "tool")
57	case activityRunningTool:
58		return "Running tool: " + activityText(a.tool, "tool")
59	case activitySendingToolResults:
60		return "Sending tool results to " + activityText(a.provider, "provider")
61	case activityRetryingRequest:
62		if a.attempt <= 0 || a.maxTries <= 0 {
63			return "Retrying request"
64		}
65		return "Retrying request in " + a.retryIn.Round(time.Millisecond).String() + " (attempt " + strconv.Itoa(a.attempt) + "/" + strconv.Itoa(a.maxTries) + ")"
66	case activityCompactingHistory:
67		return "Compacting conversation history"
68	case activityCondensingHistory:
69		return "Condensing conversation history"
70	case activityRunningShellCommand:
71		return "Running shell command"
72	default:
73		return ""
74	}
75}
76
77func activityText(value, fallback string) string {
78	value = strings.Map(func(r rune) rune {
79		if unicode.IsControl(r) {
80			return -1
81		}
82		return r
83	}, value)
84	value = strings.TrimSpace(value)
85	if value == "" {
86		return fallback
87	}
88	return value
89}
90
91// agentActivity reduces agent lifecycle facts into one current activity. It
92// tracks all pending tool calls so a multi-tool round does not claim results
93// are being sent while another tool still needs to run.
94type agentActivity struct {
95	activity
96	pendingToolCalls map[string]string
97	toolOrder        []string
98}
99
100func newAgentActivity(provider, model string) agentActivity {
101	return agentActivity{activity: activity{
102		kind:     activityPreparingRequest,
103		provider: provider,
104		model:    model,
105	}}
106}
107
108func (a *agentActivity) apply(ev core.AgentEvent) {
109	if a == nil {
110		return
111	}
112	switch e := ev.(type) {
113	case core.EvTurnStart:
114		a.kind = activityPreparingRequest
115		a.pendingToolCalls = nil
116		a.toolOrder = nil
117	case core.EvRequestStarted:
118		a.kind = activitySendingRequest
119		if e.Provider != "" {
120			a.provider = e.Provider
121		}
122		if e.Model != "" {
123			a.model = e.Model
124		}
125	case core.EvAssistantStart:
126		a.kind = activityWaitingForResponse
127	case core.EvTextDelta:
128		a.kind = activityReceivingResponse
129	case core.EvToolUseStart:
130		a.kind = activityPreparingTool
131		a.tool = e.Name
132	case core.EvToolCall:
133		if a.pendingToolCalls == nil {
134			a.pendingToolCalls = make(map[string]string)
135		}
136		if _, exists := a.pendingToolCalls[e.ID]; !exists {
137			a.toolOrder = append(a.toolOrder, e.ID)
138		}
139		a.pendingToolCalls[e.ID] = e.Name
140		a.kind = activityPreparingTool
141		a.tool = e.Name
142	case core.EvToolExecutionStarted:
143		a.kind = activityRunningTool
144		a.tool = e.Name
145	case core.EvToolResult:
146		if _, exists := a.pendingToolCalls[e.ID]; !exists {
147			return
148		}
149		delete(a.pendingToolCalls, e.ID)
150		if len(a.pendingToolCalls) == 0 {
151			a.kind = activitySendingToolResults
152			a.tool = ""
153			return
154		}
155		for _, id := range a.toolOrder {
156			if name, ok := a.pendingToolCalls[id]; ok {
157				a.kind = activityPreparingTool
158				a.tool = name
159				return
160			}
161		}
162	case core.EvRetryScheduled:
163		// Agent retries discard the preceding partial assistant message.
164		// Its proposed tool calls cannot run, so they must not affect the
165		// next successful attempt's batch accounting.
166		if e.Scope == core.RetryScopeAgent {
167			a.pendingToolCalls = nil
168			a.toolOrder = nil
169		}
170		a.kind = activityRetryingRequest
171		a.attempt = e.Attempt
172		a.maxTries = e.MaxAttempts
173		a.retryIn = e.Delay
174	}
175}

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