detached 8fd3c01

fix(subagents): scope live observations to turns

commit
8fd3c0149a6fcc0e6fb82ea1578d9af79a4f705b
author
brice · · 2026-08-12 10:35 UTC
committer
brice · · 2026-08-12 12:33 UTC
parent
e1622b9
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
7 files · +88 -11
M packages/agent/modes/subagent_indicator.go +1 -1
@@ -24,7 +24,7 @@ func renderSubagentActivityLines(th tui.Theme, spinnerGlyph string, snapshots []
2424 if operation == nil {
2525 continue
2626 }
27- line := renderSubagentActivityLine(th, spinnerGlyph, snapshot, *operation, view.LastObservation, width, now)
27+ line := renderSubagentActivityLine(th, spinnerGlyph, snapshot, *operation, view.ObservationFor(*operation), width, now)
2828 if line != "" {
2929 lines = append(lines, line)
3030 }
@@ -24,7 +24,7 @@ func renderSubagentActivityLines(th tui.Theme, spinnerGlyph string, snapshots []
24 if operation == nil {24 if operation == nil {
25 continue25 continue
26 }26 }
27- line := renderSubagentActivityLine(th, spinnerGlyph, snapshot, *operation, view.LastObservation, width, now)27+ line := renderSubagentActivityLine(th, spinnerGlyph, snapshot, *operation, view.ObservationFor(*operation), width, now)
28 if line != "" {28 if line != "" {
29 lines = append(lines, line)29 lines = append(lines, line)
30 }30 }
M packages/agent/modes/subagent_indicator_test.go +11 -1
@@ -30,13 +30,23 @@ func TestRenderSubagentActivityLinesShowsOnlyTraceOpenOperations(t *testing.T) {
3030 func TestRenderSubagentActivityLinesShowsSafeLastObservation(t *testing.T) {
3131 now := time.Date(2026, time.August, 6, 12, 0, 0, 0, time.UTC)
3232 lines := plainActivityLines(renderSubagentActivityLines(tui.Dark, "/", []subagents.AgentSnapshot{{ID: "review-123", Subagent: "reviewer"}}, map[string]subagents.AgentTraceView{
33- "review-123": {PrimaryOperation: &subagents.Operation{Type: "turn.started", StartedAt: now.Add(-time.Minute)}, LastObservation: &subagents.LiveObservation{Type: "assistant.stream.observed", At: now.Add(-time.Second)}},
33+ "review-123": {PrimaryOperation: &subagents.Operation{Type: "turn.started", TurnID: "turn-1", StartedAt: now.Add(-time.Minute)}, LastObservation: &subagents.LiveObservation{Type: "assistant.stream.observed", TurnID: "turn-1", At: now.Add(-time.Second)}},
3434 }, 100, now))
3535 if got := strings.Join(lines, "\n"); !strings.Contains(got, "turn open · assistant streaming 1s ago") || strings.Contains(got, "delta") {
3636 t.Fatalf("activity lines = %#v", lines)
3737 }
3838 }
3939
40+func TestRenderSubagentActivityLinesOmitsPriorTurnObservation(t *testing.T) {
41+ now := time.Date(2026, time.August, 6, 12, 0, 0, 0, time.UTC)
42+ lines := plainActivityLines(renderSubagentActivityLines(tui.Dark, "/", []subagents.AgentSnapshot{{ID: "review-123", Subagent: "reviewer"}}, map[string]subagents.AgentTraceView{
43+ "review-123": {PrimaryOperation: &subagents.Operation{Type: "tool.started", TurnID: "turn-2", StartedAt: now.Add(-time.Minute)}, LastObservation: &subagents.LiveObservation{Type: "assistant.stream.observed", TurnID: "turn-1", At: now.Add(-time.Second)}},
44+ }, 100, now))
45+ if got := strings.Join(lines, "\n"); strings.Contains(got, "assistant streaming") || !strings.Contains(got, "tool open") || !strings.Contains(got, "1m") {
46+ t.Fatalf("activity lines = %#v", lines)
47+ }
48+}
49+
4050 func TestRenderSubagentActivityLinesKeepsNameAndAgeOnNarrowTerminals(t *testing.T) {
4151 now := time.Date(2026, time.August, 6, 12, 0, 0, 0, time.UTC)
4252 lines := renderSubagentActivityLines(tui.Dark, "|", []subagents.AgentSnapshot{{ID: "review-123", Subagent: "reviewer"}}, map[string]subagents.AgentTraceView{
@@ -30,13 +30,23 @@ func TestRenderSubagentActivityLinesShowsOnlyTraceOpenOperations(t *testing.T) {
30 func TestRenderSubagentActivityLinesShowsSafeLastObservation(t *testing.T) {30 func TestRenderSubagentActivityLinesShowsSafeLastObservation(t *testing.T) {
31 now := time.Date(2026, time.August, 6, 12, 0, 0, 0, time.UTC)31 now := time.Date(2026, time.August, 6, 12, 0, 0, 0, time.UTC)
32 lines := plainActivityLines(renderSubagentActivityLines(tui.Dark, "/", []subagents.AgentSnapshot{{ID: "review-123", Subagent: "reviewer"}}, map[string]subagents.AgentTraceView{32 lines := plainActivityLines(renderSubagentActivityLines(tui.Dark, "/", []subagents.AgentSnapshot{{ID: "review-123", Subagent: "reviewer"}}, map[string]subagents.AgentTraceView{
33- "review-123": {PrimaryOperation: &subagents.Operation{Type: "turn.started", StartedAt: now.Add(-time.Minute)}, LastObservation: &subagents.LiveObservation{Type: "assistant.stream.observed", At: now.Add(-time.Second)}},33+ "review-123": {PrimaryOperation: &subagents.Operation{Type: "turn.started", TurnID: "turn-1", StartedAt: now.Add(-time.Minute)}, LastObservation: &subagents.LiveObservation{Type: "assistant.stream.observed", TurnID: "turn-1", At: now.Add(-time.Second)}},
34 }, 100, now))34 }, 100, now))
35 if got := strings.Join(lines, "\n"); !strings.Contains(got, "turn open · assistant streaming 1s ago") || strings.Contains(got, "delta") {35 if got := strings.Join(lines, "\n"); !strings.Contains(got, "turn open · assistant streaming 1s ago") || strings.Contains(got, "delta") {
36 t.Fatalf("activity lines = %#v", lines)36 t.Fatalf("activity lines = %#v", lines)
37 }37 }
38 }38 }
39 39
40+func TestRenderSubagentActivityLinesOmitsPriorTurnObservation(t *testing.T) {
41+ now := time.Date(2026, time.August, 6, 12, 0, 0, 0, time.UTC)
42+ lines := plainActivityLines(renderSubagentActivityLines(tui.Dark, "/", []subagents.AgentSnapshot{{ID: "review-123", Subagent: "reviewer"}}, map[string]subagents.AgentTraceView{
43+ "review-123": {PrimaryOperation: &subagents.Operation{Type: "tool.started", TurnID: "turn-2", StartedAt: now.Add(-time.Minute)}, LastObservation: &subagents.LiveObservation{Type: "assistant.stream.observed", TurnID: "turn-1", At: now.Add(-time.Second)}},
44+ }, 100, now))
45+ if got := strings.Join(lines, "\n"); strings.Contains(got, "assistant streaming") || !strings.Contains(got, "tool open") || !strings.Contains(got, "1m") {
46+ t.Fatalf("activity lines = %#v", lines)
47+ }
48+}
49+
40 func TestRenderSubagentActivityLinesKeepsNameAndAgeOnNarrowTerminals(t *testing.T) {50 func TestRenderSubagentActivityLinesKeepsNameAndAgeOnNarrowTerminals(t *testing.T) {
41 now := time.Date(2026, time.August, 6, 12, 0, 0, 0, time.UTC)51 now := time.Date(2026, time.August, 6, 12, 0, 0, 0, time.UTC)
42 lines := renderSubagentActivityLines(tui.Dark, "|", []subagents.AgentSnapshot{{ID: "review-123", Subagent: "reviewer"}}, map[string]subagents.AgentTraceView{52 lines := renderSubagentActivityLines(tui.Dark, "|", []subagents.AgentSnapshot{{ID: "review-123", Subagent: "reviewer"}}, map[string]subagents.AgentTraceView{
M packages/agent/modes/subagents_dialog.go +5 -3
@@ -1630,9 +1630,11 @@ func formatSupervisorRow(r subagents.AgentSnapshot, view subagents.AgentTraceVie
16301630 primary = &view.OpenOperations[0]
16311631 }
16321632 if primary != nil {
1633- fact += " " + formatAge(primary.StartedAt)
1634- } else if view.LastObservation != nil {
1635- fact += " " + formatAge(view.LastObservation.At)
1633+ if observation := view.ObservationFor(*primary); observation != nil {
1634+ fact += " " + formatAge(observation.At)
1635+ } else {
1636+ fact += " " + formatAge(primary.StartedAt)
1637+ }
16361638 } else if view.LastEvent.Type != "" {
16371639 fact += " " + formatAge(view.LastEvent.Timestamp)
16381640 }
@@ -1630,9 +1630,11 @@ func formatSupervisorRow(r subagents.AgentSnapshot, view subagents.AgentTraceVie
1630 primary = &view.OpenOperations[0]1630 primary = &view.OpenOperations[0]
1631 }1631 }
1632 if primary != nil {1632 if primary != nil {
1633- fact += " " + formatAge(primary.StartedAt)1633+ if observation := view.ObservationFor(*primary); observation != nil {
1634- } else if view.LastObservation != nil {1634+ fact += " " + formatAge(observation.At)
1635- fact += " " + formatAge(view.LastObservation.At)1635+ } else {
1636+ fact += " " + formatAge(primary.StartedAt)
1637+ }
1636 } else if view.LastEvent.Type != "" {1638 } else if view.LastEvent.Type != "" {
1637 fact += " " + formatAge(view.LastEvent.Timestamp)1639 fact += " " + formatAge(view.LastEvent.Timestamp)
1638 }1640 }
M packages/agent/modes/subagents_dialog_test.go +11 -0
@@ -35,6 +35,17 @@ func TestFormatSupervisorRowShowsTerminalResultDelivery(t *testing.T) {
3535 }
3636 }
3737
38+func TestFormatSupervisorRowOmitsPriorTurnObservationAge(t *testing.T) {
39+ now := time.Now()
40+ got := formatSupervisorRow(subagents.AgentSnapshot{ID: "review-1", Started: now.Add(-time.Hour)}, subagents.AgentTraceView{
41+ PrimaryOperation: &subagents.Operation{Type: "tool.started", TurnID: "turn-2", StartedAt: now.Add(-time.Minute)},
42+ LastObservation: &subagents.LiveObservation{Type: "assistant.stream.observed", TurnID: "turn-1", At: now.Add(-time.Second)},
43+ }, 120)
44+ if strings.Contains(got, "assistant streaming") || !strings.Contains(got, "tool open") || !strings.Contains(got, "1m") {
45+ t.Fatalf("row = %q", got)
46+ }
47+}
48+
3849 func TestSubagentsDialogEmptyState(t *testing.T) {
3950 d := newSubagentsDialog()
4051 d.Open(staticSnapshots(), nil, nil, nil, nil, nil, "")
@@ -35,6 +35,17 @@ func TestFormatSupervisorRowShowsTerminalResultDelivery(t *testing.T) {
35 }35 }
36 }36 }
37 37
38+func TestFormatSupervisorRowOmitsPriorTurnObservationAge(t *testing.T) {
39+ now := time.Now()
40+ got := formatSupervisorRow(subagents.AgentSnapshot{ID: "review-1", Started: now.Add(-time.Hour)}, subagents.AgentTraceView{
41+ PrimaryOperation: &subagents.Operation{Type: "tool.started", TurnID: "turn-2", StartedAt: now.Add(-time.Minute)},
42+ LastObservation: &subagents.LiveObservation{Type: "assistant.stream.observed", TurnID: "turn-1", At: now.Add(-time.Second)},
43+ }, 120)
44+ if strings.Contains(got, "assistant streaming") || !strings.Contains(got, "tool open") || !strings.Contains(got, "1m") {
45+ t.Fatalf("row = %q", got)
46+ }
47+}
48+
38 func TestSubagentsDialogEmptyState(t *testing.T) {49 func TestSubagentsDialogEmptyState(t *testing.T) {
39 d := newSubagentsDialog()50 d := newSubagentsDialog()
40 d.Open(staticSnapshots(), nil, nil, nil, nil, nil, "")51 d.Open(staticSnapshots(), nil, nil, nil, nil, nil, "")
M packages/agent/subagents/projection.go +13 -6
@@ -114,14 +114,11 @@ func (v AgentTraceView) Summary() string {
114114 primary = &v.OpenOperations[0]
115115 }
116116 if primary != nil {
117- if v.LastObservation != nil {
118- return primary.Label() + " · " + v.LastObservation.Label()
117+ if observation := v.ObservationFor(*primary); observation != nil {
118+ return primary.Label() + " · " + observation.Label()
119119 }
120120 return primary.Label()
121121 }
122- if v.LastObservation != nil {
123- return v.LastObservation.Label()
124- }
125122 if v.LastEvent.Type != "" {
126123 return "last event " + v.LastEvent.Type
127124 }
@@ -195,6 +192,16 @@ func ProjectTrace(events []TraceEvent) map[string]AgentTraceView {
195192 return views
196193 }
197194
195+// ObservationFor returns the last observation only when it belongs to this
196+// operation's turn. A prior turn's stream is not live activity for a resumed
197+// or subsequent turn.
198+func (v AgentTraceView) ObservationFor(operation Operation) *LiveObservation {
199+ if v.LastObservation == nil || operation.TurnID == "" || v.LastObservation.TurnID == "" || operation.TurnID != v.LastObservation.TurnID {
200+ return nil
201+ }
202+ return v.LastObservation
203+}
204+
198205 func operationPriority(o Operation) int {
199206 switch o.Type {
200207 case "tool.started":
@@ -236,7 +243,7 @@ func traceResultFact(event TraceEvent, previous *ResultFact) *ResultFact {
236243 result.At = event.Timestamp
237244 switch event.Type {
238245 case "result.available":
239- result.Available = true
246+ result.Available, result.Delivered, result.Failed = true, false, false
240247 case "result.delivered":
241248 result.Available, result.Delivered, result.Failed = true, true, false
242249 case "result.delivery.failed":
@@ -114,14 +114,11 @@ func (v AgentTraceView) Summary() string {
114 primary = &v.OpenOperations[0]114 primary = &v.OpenOperations[0]
115 }115 }
116 if primary != nil {116 if primary != nil {
117- if v.LastObservation != nil {117+ if observation := v.ObservationFor(*primary); observation != nil {
118- return primary.Label() + " · " + v.LastObservation.Label()118+ return primary.Label() + " · " + observation.Label()
119 }119 }
120 return primary.Label()120 return primary.Label()
121 }121 }
122- if v.LastObservation != nil {
123- return v.LastObservation.Label()
124- }
125 if v.LastEvent.Type != "" {122 if v.LastEvent.Type != "" {
126 return "last event " + v.LastEvent.Type123 return "last event " + v.LastEvent.Type
127 }124 }
@@ -195,6 +192,16 @@ func ProjectTrace(events []TraceEvent) map[string]AgentTraceView {
195 return views192 return views
196 }193 }
197 194
195+// ObservationFor returns the last observation only when it belongs to this
196+// operation's turn. A prior turn's stream is not live activity for a resumed
197+// or subsequent turn.
198+func (v AgentTraceView) ObservationFor(operation Operation) *LiveObservation {
199+ if v.LastObservation == nil || operation.TurnID == "" || v.LastObservation.TurnID == "" || operation.TurnID != v.LastObservation.TurnID {
200+ return nil
201+ }
202+ return v.LastObservation
203+}
204+
198 func operationPriority(o Operation) int {205 func operationPriority(o Operation) int {
199 switch o.Type {206 switch o.Type {
200 case "tool.started":207 case "tool.started":
@@ -236,7 +243,7 @@ func traceResultFact(event TraceEvent, previous *ResultFact) *ResultFact {
236 result.At = event.Timestamp243 result.At = event.Timestamp
237 switch event.Type {244 switch event.Type {
238 case "result.available":245 case "result.available":
239- result.Available = true246+ result.Available, result.Delivered, result.Failed = true, false, false
240 case "result.delivered":247 case "result.delivered":
241 result.Available, result.Delivered, result.Failed = true, true, false248 result.Available, result.Delivered, result.Failed = true, true, false
242 case "result.delivery.failed":249 case "result.delivery.failed":
M packages/agent/subagents/projection_test.go +30 -0
@@ -79,6 +79,36 @@ func TestProjectTraceSelectsSpecificActivityAndProjectsResultDelivery(t *testing
7979 }
8080 }
8181
82+func TestProjectTraceOmitsPriorTurnObservation(t *testing.T) {
83+ started := time.Date(2026, 8, 12, 7, 0, 0, 0, time.UTC)
84+ view := ProjectTrace([]TraceEvent{
85+ {Timestamp: started, Type: "assistant.stream.observed", AgentID: "agent-1", TurnID: "turn-1"},
86+ {Timestamp: started.Add(time.Second), Type: "turn.started", AgentID: "agent-1", TurnID: "turn-2"},
87+ })["agent-1"]
88+ if view.LastObservation == nil {
89+ t.Fatal("last observation missing")
90+ }
91+ if view.PrimaryOperation == nil {
92+ t.Fatal("primary operation missing")
93+ }
94+ if observation := view.ObservationFor(*view.PrimaryOperation); observation != nil {
95+ t.Fatalf("prior turn observation = %#v, want omitted", observation)
96+ }
97+ if got, want := view.Summary(), "turn open"; got != want {
98+ t.Fatalf("summary = %q, want %q", got, want)
99+ }
100+}
101+
102+func TestProjectTraceNewAvailableResultResetsDelivery(t *testing.T) {
103+ view := ProjectTrace([]TraceEvent{
104+ {Type: "result.delivered", AgentID: "agent-1", Data: map[string]any{"ref": "subagent://agent-1/result-1"}},
105+ {Type: "result.available", AgentID: "agent-1", Data: map[string]any{"ref": "subagent://agent-1/result-2"}},
106+ })["agent-1"]
107+ if view.Result == nil || !view.Result.Available || view.Result.Delivered || view.Result.Failed || view.Result.Ref != "subagent://agent-1/result-2" {
108+ t.Fatalf("result = %#v", view.Result)
109+ }
110+}
111+
82112 func TestProjectTraceFailedAgentClosesOpenOperations(t *testing.T) {
83113 view := ProjectTrace([]TraceEvent{
84114 {Type: "tool.started", AgentID: "agent-1", TurnID: "turn-1", Data: map[string]any{"call_id": "one"}},
@@ -79,6 +79,36 @@ func TestProjectTraceSelectsSpecificActivityAndProjectsResultDelivery(t *testing
79 }79 }
80 }80 }
81 81
82+func TestProjectTraceOmitsPriorTurnObservation(t *testing.T) {
83+ started := time.Date(2026, 8, 12, 7, 0, 0, 0, time.UTC)
84+ view := ProjectTrace([]TraceEvent{
85+ {Timestamp: started, Type: "assistant.stream.observed", AgentID: "agent-1", TurnID: "turn-1"},
86+ {Timestamp: started.Add(time.Second), Type: "turn.started", AgentID: "agent-1", TurnID: "turn-2"},
87+ })["agent-1"]
88+ if view.LastObservation == nil {
89+ t.Fatal("last observation missing")
90+ }
91+ if view.PrimaryOperation == nil {
92+ t.Fatal("primary operation missing")
93+ }
94+ if observation := view.ObservationFor(*view.PrimaryOperation); observation != nil {
95+ t.Fatalf("prior turn observation = %#v, want omitted", observation)
96+ }
97+ if got, want := view.Summary(), "turn open"; got != want {
98+ t.Fatalf("summary = %q, want %q", got, want)
99+ }
100+}
101+
102+func TestProjectTraceNewAvailableResultResetsDelivery(t *testing.T) {
103+ view := ProjectTrace([]TraceEvent{
104+ {Type: "result.delivered", AgentID: "agent-1", Data: map[string]any{"ref": "subagent://agent-1/result-1"}},
105+ {Type: "result.available", AgentID: "agent-1", Data: map[string]any{"ref": "subagent://agent-1/result-2"}},
106+ })["agent-1"]
107+ if view.Result == nil || !view.Result.Available || view.Result.Delivered || view.Result.Failed || view.Result.Ref != "subagent://agent-1/result-2" {
108+ t.Fatalf("result = %#v", view.Result)
109+ }
110+}
111+
82 func TestProjectTraceFailedAgentClosesOpenOperations(t *testing.T) {112 func TestProjectTraceFailedAgentClosesOpenOperations(t *testing.T) {
83 view := ProjectTrace([]TraceEvent{113 view := ProjectTrace([]TraceEvent{
84 {Type: "tool.started", AgentID: "agent-1", TurnID: "turn-1", Data: map[string]any{"call_id": "one"}},114 {Type: "tool.started", AgentID: "agent-1", TurnID: "turn-1", Data: map[string]any{"call_id": "one"}},
M packages/agent/subagents/result_test.go +17 -0
@@ -198,6 +198,23 @@ func TestEnsureResultRecordsAvailabilityAfterDurableWrite(t *testing.T) {
198198 if _, err := os.Stat(resultPath(state)); err != nil {
199199 t.Fatalf("durable result missing: %v", err)
200200 }
201+
202+ failedState := t.TempDir()
203+ if err := os.Mkdir(resultPath(failedState), 0o700); err != nil {
204+ t.Fatal(err)
205+ }
206+ failedTrace := NewMemoryTraceWriter()
207+ t.Cleanup(func() { _ = failedTrace.Close() })
208+ failedAgent := &Agent{ID: "agent-2", stateDir: failedState, trace: failedTrace, maxOutputBytes: 1024, maxOutputLines: 10}
209+ (&Supervisor{}).ensureResult(failedAgent, StatusDone, nil)
210+ if err := failedTrace.Flush(); err != nil {
211+ t.Fatal(err)
212+ }
213+ for _, event := range failedTrace.Events() {
214+ if event.Type == "result.available" {
215+ t.Fatalf("result.available recorded after failed write: %#v", event)
216+ }
217+ }
201218 }
202219
203220 func TestCaptureWorkspaceFailureIsDurableAndPrivate(t *testing.T) {
@@ -198,6 +198,23 @@ func TestEnsureResultRecordsAvailabilityAfterDurableWrite(t *testing.T) {
198 if _, err := os.Stat(resultPath(state)); err != nil {198 if _, err := os.Stat(resultPath(state)); err != nil {
199 t.Fatalf("durable result missing: %v", err)199 t.Fatalf("durable result missing: %v", err)
200 }200 }
201+
202+ failedState := t.TempDir()
203+ if err := os.Mkdir(resultPath(failedState), 0o700); err != nil {
204+ t.Fatal(err)
205+ }
206+ failedTrace := NewMemoryTraceWriter()
207+ t.Cleanup(func() { _ = failedTrace.Close() })
208+ failedAgent := &Agent{ID: "agent-2", stateDir: failedState, trace: failedTrace, maxOutputBytes: 1024, maxOutputLines: 10}
209+ (&Supervisor{}).ensureResult(failedAgent, StatusDone, nil)
210+ if err := failedTrace.Flush(); err != nil {
211+ t.Fatal(err)
212+ }
213+ for _, event := range failedTrace.Events() {
214+ if event.Type == "result.available" {
215+ t.Fatalf("result.available recorded after failed write: %#v", event)
216+ }
217+ }
201 }218 }
202 219
203 func TestCaptureWorkspaceFailureIsDurableAndPrivate(t *testing.T) {220 func TestCaptureWorkspaceFailureIsDurableAndPrivate(t *testing.T) {

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