detached e56ec0e

Merge 6acf8ba24d4b73936dc6f1ce2c120067281a6c4d into a9d674e0d05c093e5ff7dea21bc7d6723527b490

commit
e56ec0ea6fda17e95ee06e9017b54a839e9e929b
author
ermite · · 2026-10-20 05:13 UTC
merge of
a9d674e + 6acf8ba · diff against the first parent
signature
unsigned
4 files · +45 -11
M packages/agent/modes/interactive_commands.go +5 -0
@@ -254,11 +254,16 @@ func (i *Interactive) runSlash(ctx context.Context, cmd string) (done bool) {
254254 i.planTotal = 0
255255 i.planRevision++
256256 handoff, persistHandoff := i.resetCompactContinuationLocked()
257+ // A cleared conversation starts over with a fresh terminal title.
258+ titleCancel := i.resetSessionTitleLocked()
257259 i.view.InvalidateRenderCache()
258260 // A cleared conversation gets its pinned skills again.
259261 i.armSkillPinsLocked(pinFetch)
260262 i.mu.Unlock()
261263 pinDone()
264+ if titleCancel != nil {
265+ titleCancel()
266+ }
262267 if persistHandoff {
263268 i.persistCompactHandoff(handoff)
264269 }
@@ -254,11 +254,16 @@ func (i *Interactive) runSlash(ctx context.Context, cmd string) (done bool) {
254 i.planTotal = 0254 i.planTotal = 0
255 i.planRevision++255 i.planRevision++
256 handoff, persistHandoff := i.resetCompactContinuationLocked()256 handoff, persistHandoff := i.resetCompactContinuationLocked()
257+ // A cleared conversation starts over with a fresh terminal title.
258+ titleCancel := i.resetSessionTitleLocked()
257 i.view.InvalidateRenderCache()259 i.view.InvalidateRenderCache()
258 // A cleared conversation gets its pinned skills again.260 // A cleared conversation gets its pinned skills again.
259 i.armSkillPinsLocked(pinFetch)261 i.armSkillPinsLocked(pinFetch)
260 i.mu.Unlock()262 i.mu.Unlock()
261 pinDone()263 pinDone()
264+ if titleCancel != nil {
265+ titleCancel()
266+ }
262 if persistHandoff {267 if persistHandoff {
263 i.persistCompactHandoff(handoff)268 i.persistCompactHandoff(handoff)
264 }269 }
M packages/agent/modes/interactive_host.go +1 -7
@@ -203,13 +203,7 @@ func (i *Interactive) applyChangedCWD(ag *core.Agent, provider, model, cwd strin
203203 i.cfg.Provider = provider
204204 i.cfg.Model = model
205205 i.cfg.detachMismatchedModelProfile()
206- titleCancel := i.titleCancel
207- i.titleCancel = nil
208- i.titleVersion++
209- i.sessionTitle = ""
210- i.titleRealPromptSeen = false
211- i.titleGenerationStarted = false
212- i.writeTerminalTitleLocked("")
206+ titleCancel := i.resetSessionTitleLocked()
213207 i.toolCalls = map[string]*tui.ToolCallView{}
214208 i.toolOrder = nil
215209 i.stream.Reset()
@@ -203,13 +203,7 @@ func (i *Interactive) applyChangedCWD(ag *core.Agent, provider, model, cwd strin
203 i.cfg.Provider = provider203 i.cfg.Provider = provider
204 i.cfg.Model = model204 i.cfg.Model = model
205 i.cfg.detachMismatchedModelProfile()205 i.cfg.detachMismatchedModelProfile()
206- titleCancel := i.titleCancel206+ titleCancel := i.resetSessionTitleLocked()
207- i.titleCancel = nil
208- i.titleVersion++
209- i.sessionTitle = ""
210- i.titleRealPromptSeen = false
211- i.titleGenerationStarted = false
212- i.writeTerminalTitleLocked("")
213 i.toolCalls = map[string]*tui.ToolCallView{}207 i.toolCalls = map[string]*tui.ToolCallView{}
214 i.toolOrder = nil208 i.toolOrder = nil
215 i.stream.Reset()209 i.stream.Reset()
M packages/agent/modes/session_title.go +12 -4
@@ -211,6 +211,17 @@ func (i *Interactive) restoreFailedSessionTitle() {
211211
212212 func (i *Interactive) resetSessionTitleForFreshBranch() {
213213 i.mu.Lock()
214+ cancel := i.resetSessionTitleLocked()
215+ i.mu.Unlock()
216+ if cancel != nil {
217+ cancel()
218+ }
219+}
220+
221+// resetSessionTitleLocked drops the current title, shows the neutral terminal
222+// title, and lets the next real prompt generate a new one. Callers hold i.mu
223+// and must call the returned cancel func (if any) after unlocking.
224+func (i *Interactive) resetSessionTitleLocked() context.CancelFunc {
214225 cancel := i.titleCancel
215226 i.titleCancel = nil
216227 i.titleVersion++
@@ -219,10 +230,7 @@ func (i *Interactive) resetSessionTitleForFreshBranch() {
219230 i.titleGenerationStarted = false
220231 i.firstRealPrompt = ""
221232 i.writeTerminalTitleLocked("")
222- i.mu.Unlock()
223- if cancel != nil {
224- cancel()
225- }
233+ return cancel
226234 }
227235
228236 func (i *Interactive) agentMessagesLocked() []provider.Message {
@@ -211,6 +211,17 @@ func (i *Interactive) restoreFailedSessionTitle() {
211 211
212 func (i *Interactive) resetSessionTitleForFreshBranch() {212 func (i *Interactive) resetSessionTitleForFreshBranch() {
213 i.mu.Lock()213 i.mu.Lock()
214+ cancel := i.resetSessionTitleLocked()
215+ i.mu.Unlock()
216+ if cancel != nil {
217+ cancel()
218+ }
219+}
220+
221+// resetSessionTitleLocked drops the current title, shows the neutral terminal
222+// title, and lets the next real prompt generate a new one. Callers hold i.mu
223+// and must call the returned cancel func (if any) after unlocking.
224+func (i *Interactive) resetSessionTitleLocked() context.CancelFunc {
214 cancel := i.titleCancel225 cancel := i.titleCancel
215 i.titleCancel = nil226 i.titleCancel = nil
216 i.titleVersion++227 i.titleVersion++
@@ -219,10 +230,7 @@ func (i *Interactive) resetSessionTitleForFreshBranch() {
219 i.titleGenerationStarted = false230 i.titleGenerationStarted = false
220 i.firstRealPrompt = ""231 i.firstRealPrompt = ""
221 i.writeTerminalTitleLocked("")232 i.writeTerminalTitleLocked("")
222- i.mu.Unlock()233+ return cancel
223- if cancel != nil {
224- cancel()
225- }
226 }234 }
227 235
228 func (i *Interactive) agentMessagesLocked() []provider.Message {236 func (i *Interactive) agentMessagesLocked() []provider.Message {
M packages/agent/modes/session_title_test.go +27 -0
@@ -128,6 +128,33 @@ func TestFirstRealPromptSetsAndPersistsTerminalTitle(t *testing.T) {
128128 }
129129 }
130130
131+func TestClearResetsTerminalTitleAndRegeneratesOnNextPrompt(t *testing.T) {
132+ term := &alertTestTerminal{}
133+ client := &interactiveTitleClient{title: "Fix login retries"}
134+ ag := core.NewAgent(client, "test-model", "", nil)
135+ i := NewInteractive(InteractiveConfig{Agent: ag, Terminal: term})
136+ i.markInteractiveStarted()
137+
138+ i.Submit("Please fix login retries")
139+ waitInteractiveIdle(t, i)
140+ waitInteractiveTitle(t, i, "Fix login retries")
141+
142+ i.runSlash(context.Background(), "/clear")
143+ if got := term.String(); !strings.HasSuffix(got, "\x1b]0;zut\x07") {
144+ t.Fatalf("terminal title output after /clear = %q, want neutral title last", got)
145+ }
146+
147+ client.mu.Lock()
148+ client.title = "Add dark theme"
149+ client.mu.Unlock()
150+ i.Submit("Please add a dark theme")
151+ waitInteractiveIdle(t, i)
152+ waitInteractiveTitle(t, i, "Add dark theme")
153+ if got := term.String(); !strings.HasSuffix(got, "\x1b]0;zut: Add dark theme\x07") {
154+ t.Fatalf("terminal title output = %q, want new title last", got)
155+ }
156+}
157+
131158 func TestTitleFailureFallsBackWithoutFailingMainTurn(t *testing.T) {
132159 term := &alertTestTerminal{}
133160 client := &interactiveTitleClient{titleErr: errors.New("title provider unavailable")}
@@ -128,6 +128,33 @@ func TestFirstRealPromptSetsAndPersistsTerminalTitle(t *testing.T) {
128 }128 }
129 }129 }
130 130
131+func TestClearResetsTerminalTitleAndRegeneratesOnNextPrompt(t *testing.T) {
132+ term := &alertTestTerminal{}
133+ client := &interactiveTitleClient{title: "Fix login retries"}
134+ ag := core.NewAgent(client, "test-model", "", nil)
135+ i := NewInteractive(InteractiveConfig{Agent: ag, Terminal: term})
136+ i.markInteractiveStarted()
137+
138+ i.Submit("Please fix login retries")
139+ waitInteractiveIdle(t, i)
140+ waitInteractiveTitle(t, i, "Fix login retries")
141+
142+ i.runSlash(context.Background(), "/clear")
143+ if got := term.String(); !strings.HasSuffix(got, "\x1b]0;zut\x07") {
144+ t.Fatalf("terminal title output after /clear = %q, want neutral title last", got)
145+ }
146+
147+ client.mu.Lock()
148+ client.title = "Add dark theme"
149+ client.mu.Unlock()
150+ i.Submit("Please add a dark theme")
151+ waitInteractiveIdle(t, i)
152+ waitInteractiveTitle(t, i, "Add dark theme")
153+ if got := term.String(); !strings.HasSuffix(got, "\x1b]0;zut: Add dark theme\x07") {
154+ t.Fatalf("terminal title output = %q, want new title last", got)
155+ }
156+}
157+
131 func TestTitleFailureFallsBackWithoutFailingMainTurn(t *testing.T) {158 func TestTitleFailureFallsBackWithoutFailingMainTurn(t *testing.T) {
132 term := &alertTestTerminal{}159 term := &alertTestTerminal{}
133 client := &interactiveTitleClient{titleErr: errors.New("title provider unavailable")}160 client := &interactiveTitleClient{titleErr: errors.New("title provider unavailable")}

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