From 7aa41f537b4592a496ca774d17de30e1df68a8bc Mon Sep 17 00:00:00 2001 From: liuguohua Date: Fri, 26 Jun 2026 05:41:46 +0000 Subject: [PATCH 1/2] fix(cli): folded paste content was invisible to the model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A folded multi-line paste ([Pasted text #N · M lines]) was never seen by the model — only the placeholder label was. The CLI passed the folded label (the un-expanded `line`) as the controller's `raw` value. The Memory v5 compiler (enabled by default) takes `raw` as the source_event of its execution contract, and that contract replaces the whole user turn — so the model's only view of the paste became the label, with the actual content lost. The Desktop path already passes the expanded text as `raw`; only the CLI passed the label. Pass the expanded content (sentLine / msg.display) as `raw` in both the non-refs and @-refs submit paths, matching Desktop. `raw` still excludes resolved @-reference payloads (per the existing TestRunCompilesMemorySourceFromUnexpandedContext intent) — it now carries the expanded paste block but not inline file contents. Add TestPasteFoldExpandOnSubmit, which asserts the memory-compiler source_event holds the expanded paste (it fails before the fix: source_event is only "[Pasted text #1 · 11 lines]"). Co-Authored-By: Claude --- internal/cli/chat_tui.go | 12 +++++- internal/cli/chat_tui_test.go | 79 ++++++++++++++++++++++++++++++++++- 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/internal/cli/chat_tui.go b/internal/cli/chat_tui.go index 645da3bf9..0de53c2dd 100644 --- a/internal/cli/chat_tui.go +++ b/internal/cli/chat_tui.go @@ -1188,7 +1188,12 @@ func (m chatTUI) update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, finalize(m, cmds) } - cmds = append(cmds, m.startTurnWithRaw(sentLine, sentLine, line, line)) + // `raw` is the un-resolved user prompt used for auto-plan scoring AND the + // memory compiler's source_event. It must be the EXPANDED paste content + // (sentLine), not the folded label (line) — otherwise the memory compiler's + // execution contract replaces the user turn with one whose source_event is + // just the placeholder label, and the model never sees the pasted content. + cmds = append(cmds, m.startTurnWithRaw(sentLine, sentLine, line, sentLine)) return m, finalize(m, cmds) } @@ -1315,7 +1320,10 @@ func (m chatTUI) update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.block != "" { sent = "Referenced context:\n\n" + msg.block + "\n\n" + msg.sent } - cmds = append(cmds, m.startTurnWithRaw(sent, msg.display, msg.restore, msg.restore)) + // raw = msg.display (the expanded paste content, without resolved @-ref + // payloads) — NOT msg.restore (the folded label). See the non-refs branch + // above for why the memory compiler's source_event needs the expansion. + cmds = append(cmds, m.startTurnWithRaw(sent, msg.display, msg.restore, msg.display)) case clipboardImageMsg: if msg.err != nil { diff --git a/internal/cli/chat_tui_test.go b/internal/cli/chat_tui_test.go index f4df7bef4..c62a46fa5 100644 --- a/internal/cli/chat_tui_test.go +++ b/internal/cli/chat_tui_test.go @@ -66,11 +66,19 @@ func (r *stubbornTurnRunner) Run(ctx context.Context, _ string) error { } type recordingTurnRunner struct { - inputs []string + inputs []string + memoryCompilerInputs []string } -func (r *recordingTurnRunner) Run(_ context.Context, input string) error { +func (r *recordingTurnRunner) Run(ctx context.Context, input string) error { r.inputs = append(r.inputs, input) + // The memory compiler's source_event is set by the orchestrator from the + // controller's `raw` value. Capture it so we can prove the CLI passes the + // EXPANDED paste (not the folded label) — the label would starve the model + // of the pasted content once the compiler's contract replaces the user turn. + if source, ok := agent.MemoryCompilerSourceInputFromContext(ctx); ok { + r.memoryCompilerInputs = append(r.memoryCompilerInputs, source) + } return nil } @@ -1611,6 +1619,73 @@ func TestFoldedPasteUsesPlaceholderAndExpandsOnSend(t *testing.T) { } } +// TestPasteFoldExpandOnSubmit verifies that a folded paste is fully expanded +// before being sent to the controller (the LLM sees the actual content, not just +// the placeholder label). +func TestPasteFoldExpandOnSubmit(t *testing.T) { + r := &recordingTurnRunner{} + ctrl := control.New(control.Options{Runner: r, Sink: event.Discard, SessionDir: t.TempDir(), Label: "test"}) + + m := newTestChatTUI() + m.ctrl = ctrl + m.eventCh = make(chan event.Event, 64) + + // Simulate a multi-line paste that meets the fold threshold (≥5 lines). + pasted := strings.Repeat("line of pasted content\n", 10) + model, _ := m.Update(tea.PasteMsg{Content: pasted}) + m = model.(chatTUI) + + display := m.input.Value() + if !strings.Contains(display, "[Pasted text #1") { + t.Fatalf("paste should be folded, got: %q", display) + } + if len(m.pastedBlocks) != 1 { + t.Fatalf("expected 1 pastedBlock, got %d", len(m.pastedBlocks)) + } + + // Simulate pressing Enter to submit. + // NOTE: in a real terminal KeyEnter has empty Text, so String() returns "enter". + model, _ = m.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) + m = model.(chatTUI) + + // Wait briefly for the async controller to process. + time.Sleep(100 * time.Millisecond) + + if len(r.inputs) == 0 { + t.Fatal("runner.Run was not called — the paste was never submitted") + } + sentToRunner := r.inputs[0] + t.Logf("sent to runner (%d bytes):\n%s", len(sentToRunner), sentToRunner) + + // The runner must receive the FULL expanded paste, not just the label. + if !strings.Contains(sentToRunner, "line of pasted content") { + t.Fatalf("runner received only the placeholder label, not the expanded paste content.\nGot: %q", sentToRunner) + } + // Verify the expanded markers are present. + if !strings.Contains(sentToRunner, "--- Begin [Pasted text #1") { + t.Fatalf("missing Begin marker in runner input.\nGot: %q", sentToRunner) + } + if !strings.Contains(sentToRunner, "--- End [Pasted text #1") { + t.Fatalf("missing End marker in runner input.\nGot: %q", sentToRunner) + } + + // The memory compiler (enabled by default) replaces the user turn with an + // execution contract whose source_event is the controller's `raw` value. + // If `raw` were the folded label, the model would only ever see + // "[Pasted text #1 · N lines]" and never the pasted content. Assert the + // source_event carries the EXPANDED content. + if len(r.memoryCompilerInputs) == 0 { + t.Fatal("memory compiler source input was not set on the context") + } + mcSource := r.memoryCompilerInputs[0] + if strings.Contains(mcSource, "[Pasted text #1") && !strings.Contains(mcSource, "line of pasted content") { + t.Fatalf("memory compiler source_event has the folded label but not the expanded content:\n%q", mcSource) + } + if !strings.Contains(mcSource, "line of pasted content") { + t.Fatalf("memory compiler source_event must contain the expanded paste content, got:\n%q", mcSource) + } +} + func TestPasteMsgFoldsBeforeTextareaConsumesNewlines(t *testing.T) { m := newTestChatTUI() model, _ := m.Update(tea.PasteMsg{Content: "1\n2\n3\n4\n5"}) From f6b7b429d9a389f6defd3e07c6bd2439960ddd8d Mon Sep 17 00:00:00 2001 From: SivanCola <32437197+SivanCola@users.noreply.github.com> Date: Sat, 27 Jun 2026 03:42:28 +0800 Subject: [PATCH 2/2] test(cli): wait for folded paste turn completion Wait for the controller TurnDone event before reading the recording runner output. This gives the async send path a real happens-before edge under go test -race instead of relying on a fixed sleep. --- internal/cli/chat_tui_test.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/internal/cli/chat_tui_test.go b/internal/cli/chat_tui_test.go index c62a46fa5..a3d391fbb 100644 --- a/internal/cli/chat_tui_test.go +++ b/internal/cli/chat_tui_test.go @@ -1624,7 +1624,13 @@ func TestFoldedPasteUsesPlaceholderAndExpandsOnSend(t *testing.T) { // the placeholder label). func TestPasteFoldExpandOnSubmit(t *testing.T) { r := &recordingTurnRunner{} - ctrl := control.New(control.Options{Runner: r, Sink: event.Discard, SessionDir: t.TempDir(), Label: "test"}) + events := make(chan event.Event, 64) + ctrl := control.New(control.Options{ + Runner: r, + Sink: event.FuncSink(func(e event.Event) { events <- e }), + SessionDir: t.TempDir(), + Label: "test", + }) m := newTestChatTUI() m.ctrl = ctrl @@ -1648,8 +1654,7 @@ func TestPasteFoldExpandOnSubmit(t *testing.T) { model, _ = m.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) m = model.(chatTUI) - // Wait briefly for the async controller to process. - time.Sleep(100 * time.Millisecond) + waitForCLIEvent(t, events, event.TurnDone) if len(r.inputs) == 0 { t.Fatal("runner.Run was not called — the paste was never submitted")