v0.42.0 82578de

dynamic_context.go RawHistoryFolder

1package core
2
3import (
4	"strings"
5	"time"
6
7	"github.com/bnema/zut/packages/provider"
8)
9
10const internalContextMarker = "internal_context"
11
12// appendDynamicContext persists a provider-visible, host-authored context item
13// only when its canonical value changes. It is deliberately separate from the
14// stable top-level instructions so changing time or extension state cannot
15// rewrite the instruction cache prefix.
16func (a *Agent) appendDynamicContext(hostContext, turnContext string) {
17	parts := make([]string, 0, 3)
18	if timeContext := a.providerTimeContext().developerText(); timeContext != "" {
19		parts = append(parts, timeContext)
20	}
21	if hostContext = boundedTurnContext(hostContext); hostContext != "" {
22		parts = append(parts, hostContext)
23	}
24	if turnContext = boundedTurnContext(turnContext); turnContext != "" {
25		parts = append(parts, "[Extension context]\n"+turnContext)
26	}
27	text := strings.Join(parts, "\n\n")
28	if text == "" {
29		return
30	}
31
32	message := provider.Message{
33		Role:    provider.RoleDeveloper,
34		Content: []provider.Content{provider.TextBlock{Text: text}},
35		Time:    time.Now(),
36		Meta:    map[string]string{internalContextMarker: "true"},
37	}
38
39	a.mu.Lock()
40	for i := len(a.messages) - 1; i >= 0; i-- {
41		if !isInternalContextMessage(a.messages[i]) {
42			continue
43		}
44		if internalContextText(a.messages[i]) == text {
45			a.mu.Unlock()
46			return
47		}
48		break
49	}
50	// The first dynamic item must precede the accepted user task. Persisting it
51	// as an append would reverse that order on session reload, so report the
52	// resulting transcript replacement instead. Later snapshots extend the
53	// history at the next model boundary and remain ordinary appends.
54	first := !hasInternalContextMessage(a.messages)
55	insertAt := len(a.messages)
56	if first {
57		insertAt = 0
58	}
59	a.messages = append(a.messages, provider.Message{})
60	copy(a.messages[insertAt+1:], a.messages[insertAt:])
61	a.messages[insertAt] = message
62	a.rev++
63	onCompacted := a.OnTranscriptCompacted
64	persisted := append([]provider.Message(nil), a.messages...)
65	a.mu.Unlock()
66	if first && onCompacted != nil {
67		onCompacted(persisted)
68		return
69	}
70	a.fireMessageAppended(message)
71}
72
73func hasInternalContextMessage(messages []provider.Message) bool {
74	for _, message := range messages {
75		if isInternalContextMessage(message) {
76			return true
77		}
78	}
79	return false
80}
81
82func isInternalContextMessage(message provider.Message) bool {
83	return message.Role == provider.RoleDeveloper && message.Meta[internalContextMarker] == "true"
84}
85
86// IsInternalContextMessage reports whether message is host-authored context
87// injected by the agent loop, such as a time snapshot or an extension
88// reminder, rather than finalized conversation history. Hosts that persist a
89// replacement transcript must exclude these messages: their value belongs to
90// the live request and replaying a stale copy would contradict it.
91func IsInternalContextMessage(message provider.Message) bool {
92	return isInternalContextMessage(message)
93}
94
95func latestInternalContext(messages []provider.Message) (*provider.Message, []provider.Message) {
96	filtered := make([]provider.Message, 0, len(messages))
97	var latest *provider.Message
98	for _, message := range messages {
99		if isInternalContextMessage(message) {
100			copy := message
101			latest = &copy
102			continue
103		}
104		filtered = append(filtered, message)
105	}
106	return latest, filtered
107}
108
109func internalContextText(message provider.Message) string {
110	if len(message.Content) != 1 {
111		return ""
112	}
113	text, _ := message.Content[0].(provider.TextBlock)
114	return text.Text
115}

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