From 7a52d96baad39cb094e4bd6c05a376d220a28bf0 Mon Sep 17 00:00:00 2001 From: "sesh-dispatch[bot]" <296316602+sesh-dispatch[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 04:41:39 +0000 Subject: [PATCH 1/2] fix(tui): preserve typed draft across turn completion A steering message typed while the agent worked was discarded when the turn completed. beginInput unconditionally zeroed the editor buffer when the between-turns prompt re-opened the editor, destroying a draft that survived the turn in the live editor's buffer. Carry an existing non-empty buffer (with its cursor, snippets, and images) into the next prompt. Submit, steer, and /stop all clear the buffer before returning, so a non-empty buffer here uniquely identifies the carry-over. A masked (secret) buffer never carries, keyed on the prior mask so it holds in both directions. --- harness/tui.go | 17 ++++++++++--- harness/tui_test.go | 61 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 3 deletions(-) diff --git a/harness/tui.go b/harness/tui.go index d8e9a7a..9fb9ebd 100644 --- a/harness/tui.go +++ b/harness/tui.go @@ -770,9 +770,20 @@ func (t *tuiConsole) SetTitle(s string) { // footer itself persists between inputs. func (t *tuiConsole) beginInput(prompt string, mask bool) { t.mu.Lock() - t.prompt, t.buf, t.pos, t.mask = prompt, nil, 0, mask - t.snippets = nil - t.images = nil + // A turn can end while the user is mid-typing a steering message. Carry + // that draft into the next prompt instead of discarding it: endInput and + // the steer/stop paths already clear the buffer on submit, so a non-empty + // one here uniquely identifies the carry-over. The buffer's snippet and + // image tokens index into the slices, so they carry too. A buffer left + // masked (a secret prompt) never carries: gating on the prior mask, not + // the new one, stops a secret leaking into an ordinary prompt either way. + if !t.mask && len(t.buf) > 0 { + t.prompt, t.mask = prompt, mask + } else { + t.prompt, t.buf, t.pos, t.mask = prompt, nil, 0, mask + t.snippets = nil + t.images = nil + } t.histIdx = len(t.hist) t.draft = nil t.winTop = 0 diff --git a/harness/tui_test.go b/harness/tui_test.go index bc03d36..88478a5 100644 --- a/harness/tui_test.go +++ b/harness/tui_test.go @@ -301,6 +301,67 @@ func TestAttendTurnStopCommandCancelsWithoutQueuing(t *testing.T) { } } +// TestBeginInputPreservesDraftAcrossTurnEnd: a draft typed during a turn (but +// never submitted) must survive the working-to-completed transition and remain +// in the editor when the next prompt opens, so in-progress text is not lost. +// attendTurn leaves the typed buffer in place when the turn ends; the next +// ReadLine re-opens the editor through beginInput, which must carry that buffer +// forward instead of zeroing it. Breaker: revert beginInput to an unconditional +// t.buf = nil and the submitted line comes back empty instead of "fix it". +func TestBeginInputPreservesDraftAcrossTurnEnd(t *testing.T) { + f, err := os.CreateTemp(t.TempDir(), "tui-out") + if err != nil { + t.Fatal(err) + } + defer f.Close() + tc := &tuiConsole{out: f, in: bufio.NewReader(strings.NewReader("fix it")), cols: 80} + // Phase 1: the user types a steering draft while a turn runs; the stream's + // EOF ends the attend (errTurnOver) the way a turn finishing on its own does. + if err := tc.attendTurn(turnAttend{ + done: make(chan struct{}), // never closes; EOF ends attend + cancel: func() {}, + queue: func(string) {}, + }); err != errTurnOver { + t.Fatalf("attendTurn err = %v, want errTurnOver", err) + } + if got := string(tc.buf); got != "fix it" { + t.Fatalf("draft must survive attendTurn: buf = %q, want %q", got, "fix it") + } + // Phase 2: the next prompt re-opens the editor. EOF closed the pump's + // channel, so re-arm a fresh input source the way a live console keeps + // reading, then submit with Enter. + tc.runes = nil + tc.in = bufio.NewReader(strings.NewReader("\r")) + line, err := tc.ReadLine("-> ") + if err != nil { + t.Fatalf("ReadLine: %v", err) + } + if line != "fix it" { + t.Fatalf("draft must carry into the next prompt: line = %q, want %q", line, "fix it") + } +} + +// TestBeginInputDropsMaskedDraftForOrdinaryPrompt: a non-empty buffer left from +// a masked prompt must never carry into an ordinary one, so a secret cannot +// leak across prompt types. The carry-over branch is gated on !mask, so a +// non-empty masked buffer is cleared when the next (non-masked) prompt opens. +// Breaker: drop the !mask guard on the carry-over branch and the secret text +// survives into the ordinary prompt. +func TestBeginInputDropsMaskedDraftForOrdinaryPrompt(t *testing.T) { + f, err := os.CreateTemp(t.TempDir(), "tui-out") + if err != nil { + t.Fatal(err) + } + defer f.Close() + // Seed the exact state a masked prompt leaves mid-edit (non-empty buffer, + // masked), then open an ordinary prompt: the secret must not carry. + tc := &tuiConsole{out: f, cols: 80, buf: []rune("hush"), pos: 4, mask: true} + tc.beginInput("-> ", false) + if len(tc.buf) != 0 { + t.Fatalf("a masked draft must not carry into an ordinary prompt: buf = %q", string(tc.buf)) + } +} + // TestEscIsBareKeysOffIntroducer: bare-Escape detection decides by the byte // following Esc, not by timing, so it holds up under tmux/SSH latency. A lone // Esc with the stream then closed is bare; an Esc followed by a CSI introducer From 0a965dd91f21348e8eeee14a1395bb8e9873be6d Mon Sep 17 00:00:00 2001 From: "sesh-dispatch[bot]" <296316602+sesh-dispatch[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 04:42:20 +0000 Subject: [PATCH 2/2] dispatch: resolve #33 --- TASK.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 TASK.md diff --git a/TASK.md b/TASK.md new file mode 100644 index 0000000..cd89ec3 --- /dev/null +++ b/TASK.md @@ -0,0 +1,46 @@ +# Task + +Resolve issue #33: [bug] text typed during a turn is lost when the agent completes + +## Issue body + +### What happened +While the agent is working, the user types into the message bar (to steer the +next turn or queue a follow-up). When the agent transitions from working to +completed, that in-progress text is deleted from the message footer and the bar +resets to empty. Anything the user was mid-typing is gone. + +### Steps to reproduce +1. Start an interactive sesh session. +2. Submit a request so the agent is actively working. +3. While it works, type some text into the message bar. +4. Wait for the agent to finish (transition to completed). + +### Expected +The text the user is typing should be preserved in the message bar at all +times, including across the working-to-completed transition. The buffer is the +user's draft and should survive state changes. + +### Actual +When the turn completes, the input editor is reset and the message bar is +cleared, discarding the partial message the user was composing. + +### Likely location +The editor buffer is reset in `harness/tui.go`. `beginInput` zeroes `t.buf` +when the editor re-opens (which fires as the prompt re-shows at completion): +`t.prompt, t.buf, t.pos, t.mask = prompt, nil, 0, mask`. That clear is +correct after a submit (`endInput`), but it also destroys a draft that was +being typed during the turn. The fix likely preserves the current `buf`/`pos` +when re-opening the editor at completion rather than unconditionally zeroing +it. + +### sesh context +- build: 33cb3ae +- provider protocol / model / context window: openai / glm-5.2 / 200000 +- active mods: none +- tuning overrides: handoff_pct:80 hard_pct:90 max_useful_context:250000 assumed_context:200000 (and a second set: handoff_pct:70 hard_pct:85 max_useful_context:400000) task_depth:3 stuck_after:3 seed_ledger_entries:10 recall_links:50 diff_lines:40 skill_note_off:false update_check:true +- skills installed: 6 +- tool mods: 1 +- mcp configured: yes +- invocation: interactive; flags: none +