Reviewed GitHub issue and related code.
UnderstandIdentified root cause in paste handling.
BuildApplied fix to follow-up handling code.
EditConfirmed fix resolved the issue.
VerifyAnalyze GitHub issue(s): https://github.com/badlogic/pi-mono/issues/912
For each issue:
Read the issue in full, including all comments and linked issues/PRs.
For bugs:
For feature requests:
Do NOT implement unless explicitly asked. Analyze and propose only.
The user wants me to analyze GitHub issue #912. Let me fetch the issue details first.
>gh issue view 912 --repo badlogic/pi-mono --json title,body,comments,labels,stateThis is a bug in the coding-agent package. The issue is:
[paste #1 +15 lines] as a placeholder/preview[paste #1 +15 lines] instead of the actual pasted contentI need to look at:
Let me look at the coding-agent TUI code to understand the flow.
Let me examine the relevant code. The issue is about paste handling in the TUI and the follow-up queue.
>find /Users/badlogic/workspaces/pi-mono/packages/coding-agent -type f -name "*.ts" | xargs grep -l -E "(paste|queue|follow)" 2>/dev/null | head -20Let me look at the interactive mode and find where paste handling and queuing happens.
>find /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src -type f -name "*.ts" | xargs grep -l -E "(paste|queue|follow)" 2>/dev/nullThe key files are likely:
interactive-mode.ts - main interactive loopcustom-editor.ts - handles the paste markersagent-session.ts - handles the queueLet me look at the custom-editor.ts first to understand how pastes work.
The CustomEditor extends Editor from pi-tui. The paste handling is likely in the base Editor class or in the interactive-mode.ts. Let me look at the interactive-mode.ts to find the queue mechanism and how pastes are handled.
This is a big file. Let me search for the follow-up queue mechanism and paste handling.
Let me search for the follow-up handling and paste-related code:
>grep -n -E "(followUp|handleFollowUp|queue|pending.*Message|getFullText|expandPaste)" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/interactive-mode.ts | head -50Let me look at the handleFollowUp function more closely, and also search for how paste expansion works in the Editor class from pi-tui.
I see the issue! In handleFollowUp(), it calls this.editor.getText().trim(). The editor's getText() method likely returns the display text with paste markers (like [paste #1 +15 lines]) instead of the actual full text.
Let me look at the Editor class in pi-tui to understand how pastes are handled.
Let me look at the Editor class in pi-tui to understand paste handling:
>find /Users/badlogic/workspaces/pi-mono/packages/tui/src -name "*.ts" | xargs grep -l -E "(paste|getFullText|getText)" 2>/dev/nullLet me look at the Editor class to see how pastes are handled and what methods are available.
I found it. The Editor class has two methods:
getText() - returns the raw lines joined with newlines (includes paste markers like [paste #1 +15 lines])getExpandedText() - returns the text with paste markers expanded to their actual contentThe bug is in handleFollowUp() in interactive-mode.ts. It calls this.editor.getText() instead of this.editor.getExpandedText().
Let me look at line 2248-2270 again to see exactly how this is used and what session.prompt and session.followUp do.
I found the bug. Let me also check the EditorComponent interface to see if getExpandedText is part of it:
The EditorComponent interface has getExpandedText?() as an optional method. The bug is that handleFollowUp() uses getText() which returns the text with paste markers, instead of using getExpandedText() which returns the actual pasted content.
Let me look at other places where text is extracted from the editor and see how they handle this. For example, onSubmit in the handleInput method of Editor already properly expands paste markers.
Let me verify where getExpandedText is being used vs getText:
>grep -n "getExpandedText\|getText" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/interactive-mode.tsFound it! There are several places using getText():
getEditorText: () => this.editor.getText() - extension UI context (probably fine for display purposes)const currentText = this.editor.getText(); - for setCustomEditorComponent, saving current textconst savedText = this.editor.getText(); - for showExtensionCustom, saving text!this.editor.getText().trim() - checking if empty for double-escape (fine)const text = this.editor.getText().trim(); - this is in handleFollowUp() and is the BUG!const currentText = this.editor.getExpandedText?.() ?? this.editor.getText(); - this is the CORRECT patternconst currentText = options?.currentText ?? this.editor.getText(); - needs checkingLet me look at line 2363 to see what function that's in, and also check 2475.
Let me check what function line 2363 is in and what context line 2475 is:
Line 2363 is in openExternalEditor() and correctly uses this.editor.getExpandedText?.() ?? this.editor.getText().
Now let me check line 2475:
Line 2475 is in restoreQueuedMessagesToEditor(). It uses this.editor.getText() which is fine in that context since it's just getting the current editor content to append queued messages to. The text that will be used for prompting comes from allQueued which was already saved from the queue (and that text should have been properly expanded when it was originally submitted).
However, the issue is at line 2249 in handleFollowUp() where it grabs this.editor.getText() instead of expanded text.
Also, let me check if there's a similar issue in the regular submit handler (not follow-up) to see how it's done there.
Let me also check the normal submit handler to see how it handles text extraction:
I see! The onSubmit callback at line 1630 receives text which is already the parameter passed to it. Let me go back to line 1554 where the onSubmit callback is set up and see where the text comes from.
I need to find where onSubmit is being called with the text parameter. Let me see where the Editor's onSubmit is invoked - it's in the Editor class itself in the handleInput method at line 668.
Looking at that code:
// Submit (Enter)if (kb.matches(data, "submit")) { if (this.disableSubmit) return;
let result = this.state.lines.join("\n").trim(); for (const [pasteId, pasteContent] of this.pastes) { const markerRegex = new RegExp(`\\[paste #${pasteId}( (\\+\\d+ lines|\\d+ chars))?\\]`, "g"); result = result.replace(markerRegex, pasteContent); } // ... if (this.onSubmit) this.onSubmit(result);So the Editor class DOES expand paste markers in its handleInput method when Enter is pressed. The text passed to onSubmit already has paste markers expanded.
BUT... when handleFollowUp() is called (via Alt+Enter), it bypasses the onSubmit path. The Editor's handleInput method handles Shift+Enter and Alt+Enter as newLine operations, not submit. So the onSubmit callback isn't triggered.
Wait let me re-read the Editor's handleInput for how Enter is handled... Looking at line ~604 in editor.ts, I see if (kb.matches(data, "submit")) which triggers onSubmit with expanded text.
But handleFollowUp() is triggered by a keybinding in the CustomEditor, which inherits from Editor. The problem is that handleFollowUp() directly reads this.editor.getText() instead of using the expanded version.
Let me double-check by looking at how the followUp action is set up.
Let me trace how handleFollowUp is triggered to confirm the issue:
>grep -n "followUp" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/core/keybindings.tsSo followUp is an AppAction that's bound to Alt+Enter. When Alt+Enter is pressed, the handleFollowUp() function is called directly, bypassing the normal Editor submit path (which would expand pastes).
The fix is simple: in handleFollowUp(), change this.editor.getText() to this.editor.getExpandedText?.() ?? this.editor.getText() (same pattern as in openExternalEditor).
Let me also check if there's a similar issue in the queueCompactionMessage path where text is queued during compaction.
Let me check where queueCompactionMessage gets its text from:
I see exactly what's happening:1. handleFollowUp() gets text via this.editor.getText().trim() at line 2249
2. This text (with paste markers) is then either: - Sent to queueCompactionMessage() (during compaction)
session.prompt() with streamingBehavior: "followUp" (during streaming)editor.onSubmit() (when idle - this path is fine because onSubmit is already provided expanded text by the Editor's internal handler)The bug is at line 2249: this.editor.getText() should be this.editor.getExpandedText?.() ?? this.editor.getText().
There's also a potential issue at line 2273-2275 where this.editor.onSubmit(text) is called with the unexpanded text. But wait, this path shouldn't happen because when not streaming, the user's Alt+Enter would trigger the normal submit which goes through Editor's handleInput. Actually no, looking more carefully, this code path happens when the agent is idle, and Alt+Enter is pressed. In this case, handleFollowUp() is called via the keybinding handler, not through Editor's internal submit handler. So the text would still have the paste markers.
But actually, looking at the Editor code again, when Alt+Enter is pressed:1. handleInput in Editor checks keybindings
2. The followUp keybinding check happens in CustomEditor.handleInput() before calling super.handleInput()3. So the handleFollowUp() function directly uses getText() which has the markers
The fix is to expand the text at line 2249.
Let me also check if there's a similar issue in the "steer" path at line 1648 (the regular Enter during streaming).
Let me check the "steer" path to see if it has the same bug:
Let me check where the onSubmit callback is set up and where text comes from:
Good, so the onSubmit callback receives text as a parameter. This parameter is the text that the Editor passes to the callback. Looking back at the Editor code:
// Submit (Enter)if (kb.matches(data, "submit")) { if (this.disableSubmit) return;
let result = this.state.lines.join("\n").trim(); for (const [pasteId, pasteContent] of this.pastes) { const markerRegex = new RegExp(`\\[paste #${pasteId}( (\\+\\d+ lines|\\d+ chars))?\\]`, "g"); result = result.replace(markerRegex, pasteContent); } // ... if (this.onSubmit) this.onSubmit(result);The Editor's internal submit handler DOES expand paste markers before calling onSubmit. So the "steer" path that uses the normal Enter key is fine because it goes through the Editor's handleInput which expands paste markers before calling onSubmit.
The bug is specifically in handleFollowUp() which bypasses the Editor's internal submit handling by reading getText() directly.
Root Cause:
In interactive-mode.ts, the handleFollowUp() method (triggered by Alt+Enter) reads editor content using this.editor.getText() which returns text with paste markers like [paste #1 +15 lines]. It should instead use this.editor.getExpandedText?.() ?? this.editor.getText() which expands those markers to the actual pasted content.
Why the normal Enter works:
The regular submit path (Enter key) goes through the Editor's internal handleInput() method, which properly expands paste markers before calling the onSubmit callback.
The Fix:
In handleFollowUp() at line 2249, change:
const text = this.editor.getText().trim();to:
const text = (this.editor.getExpandedText?.() ?? this.editor.getText()).trim();This ensures Alt+Enter uses the same expanded text that the normal Enter key would pass to the callback, rather than raw text with unresolved paste markers.
Affected File:
packages/coding-agent/src/modes/interactive/interactive-mode.tsTitle: Follow-up queue sends paste marker instead of full paste content
Type: Bug
In packages/coding-agent/src/modes/interactive/interactive-mode.ts, the handleFollowUp() method (triggered by Alt+Enter) reads editor content using:
const text = this.editor.getText().trim(); // Line 2249The getText() method returns the raw editor state which includes paste markers like [paste #1 +15 lines]. It should instead use getExpandedText() which expands those markers to the actual pasted content.
Why the normal Enter works correctly:
The regular submit path (Enter key) goes through the Editor's internal handleInput() method in packages/tui/src/components/editor.ts (around line 668), which properly expands paste markers before calling the onSubmit callback:
let result = this.state.lines.join("\n").trim();for (const [pasteId, pasteContent] of this.pastes) { const markerRegex = new RegExp(`\\[paste #${pasteId}( (\\+\\d+ lines|\\d+ chars))?\\]`, "g"); result = result.replace(markerRegex, pasteContent);}// ...if (this.onSubmit) this.onSubmit(result);But handleFollowUp() bypasses this path entirely by reading text directly from the editor.
File: packages/coding-agent/src/modes/interactive/interactive-mode.ts
Line 2249: Change:
const text = this.editor.getText().trim();To:
const text = (this.editor.getExpandedText?.() ?? this.editor.getText()).trim();This follows the same pattern already used in openExternalEditor() at line 2363:
const currentText = this.editor.getExpandedText?.() ?? this.editor.getText();packages/coding-agent/src/modes/interactive/interactive-mode.ts (single line change)const currentText = this.editor.getExpandedText?.() ?? this.editor.getText();
why not just this.editor.getExpandedTest()?
The user is asking why we use the fallback pattern this.editor.getExpandedText?.() ?? this.editor.getText() instead of just calling this.editor.getExpandedText() directly.
Looking at the EditorComponent interface in /Users/badlogic/workspaces/pi-mono/packages/tui/src/editor-component.ts:
/** * Get text with any markers expanded (e.g., paste markers). * Falls back to getText() if not implemented. */getExpandedText?(): string;The getExpandedText method is optional (note the ? in the type definition). This is because EditorComponent is an interface that custom editor implementations can satisfy. Not all custom editors may implement getExpandedText() - they might not have a paste marker system at all.
The this.editor property in interactive-mode.ts is typed as EditorComponent, not as the concrete Editor class. So when using a custom editor component (set via setCustomEditorComponent), that editor might not implement getExpandedText.
The fallback pattern ensures:
getExpandedText exists, use it (expands paste markers)getText()This is defensive programming for the extension system where custom editors can be plugged in.
getExpandedText is optional in the EditorComponent interface:
// packages/tui/src/editor-component.ts/** * Get text with any markers expanded (e.g., paste markers). * Falls back to getText() if not implemented. */getExpandedText?(): string;The this.editor property is typed as EditorComponent, not the concrete Editor class. Extensions can provide custom editor components via setEditorComponent(), and those custom editors might not implement getExpandedText() (they may not have a paste marker system at all).
The fallback pattern handles both cases:
getExpandedText()getText()cool, implement, commit and push close pr with thanks for reporting, implmeneted in main
The user wants me to implement the fix, commit, and push. Let me make the change to the file.
Now I need to make the fix.
Now let me run the check to make sure everything is fine.
>cd /Users/badlogic/workspaces/pi-mono && npm run check 2>&1All checks pass. Now I need to update the changelog for the coding-agent package, then commit, push, and close the issue.
I need to add a "Fixed" entry for this bug fix.
Now let me commit the changes and push.
>cd /Users/badlogic/workspaces/pi-mono && git add packages/coding-agent/src/modes/interactive/interactive-mode.ts packages/coding-agent/CHANGELOG.md && git commit -m "fix(coding-agent): expand paste markers in follow-up queue
fixes #912"Now push and close the issue with a comment.
>cd /Users/badlogic/workspaces/pi-mono && git pushNow I need to close the issue with a comment thanking them for reporting. The user said the fix was "implemented in main".
>gh issue close 912 --repo badlogic/pi-mono --comment "Thanks for reporting! Fixed in main."The issue was already closed (probably auto-closed by the fixes #912 in the commit message). Let me add a comment anyway.
>gh issue comment 912 --repo badlogic/pi-mono --body "Thanks for reporting! Fixed in main."Done. Let me summarize what I did.
Done. Fixed the bug by changing getText() to getExpandedText?.() ?? getText() in handleFollowUp(). The fixes #912 in the commit message auto-closed the issue, and I added the thank you comment.
sleep for 6 seconds
The user wants me to sleep for 6 seconds. I can do this with a bash command.
>sleep 6Done.
Done.
skills?: string[];
Consistency with other resource types (though not currently used in this file).
All changes are intentional and correct. Want me to mark it complete?
yes
Thinking...
edit ~/workspaces/pi-mono/todo.md:14
... 10 - [ ] `packages/coding-agent/src/core/settings-manager.ts` 11 - [ ] `packages/coding-agent/src/core/skills.ts` 12 - [ ] `packages/coding-agent/src/core/prompt-templates.ts` 13 - [ ] `packages/coding-agent/src/core/system-prompt.ts`-14 - [] `packages/coding-agent/src/core/extensions/loader.ts`+14 - [x] `packages/coding-agent/src/core/extensions/loader.ts` 15 - [x] `packages/coding-agent/src/core/extensions/runner.ts` 16 - [x] `packages/coding-agent/src/core/model-registry.ts` 17 - [x] `packages/coding-agent/src/core/footer-data-provider.ts` 18 - [x] `packages/coding-agent/src/core/auth-storage.ts` ...
Thinking...The user seems to have pasted some output from another session or conversation. It looks like they're working through a todo list and marking items complete. They pasted what appears to be a diff showing a checkbox being marked complete in a todo.md file.
I should wait for an actual instruction or question from the user. This seems like acc
seems to work
The user seems to be showing me some output from another session or context - it looks like a todo list being checked off for reviewing files. They're saying "seems to work" which appears to be feedback about something working correctly.
I should acknowledge this but I'm not sure what specific action they want me to take. It looks like they might just be commenting on something unrelated to the previous issue we worked on.
Looks like output from another session. Anything you need me to do here?
// Submit (Enter)if (kb.matches(data, "submit")) { if (this.disableSubmit) return;
let result = this.state.lines.join("\n").trim(); for (const [pasteId, pasteContent] of this.pastes) { const markerRegex = new RegExp(`\\[paste #${pasteId}( (\\+\\d+ lines|\\d+ chars))?\\]`, "g"); result = result.replace(markerRegex, pasteContent); } // ... if (this.onSubmit) this.onSubmit(result);// Submit (Enter)if (kb.matches(data, "submit")) { if (this.disableSubmit) return;
let result = this.state.lines.join("\n").trim(); for (const [pasteId, pasteContent] of this.pastes) { const markerRegex = new RegExp(`\\[paste #${pasteId}( (\\+\\d+ lines|\\d+ chars))?\\]`, "g"); result = result.replace(markerRegex, pasteContent); } // ... if (this.onSubmit) this.onSubmit(result);const text = this.editor.getText().trim();const text = (this.editor.getExpandedText?.() ?? this.editor.getText()).trim();const text = this.editor.getText().trim(); // Line 2249let result = this.state.lines.join("\n").trim();for (const [pasteId, pasteContent] of this.pastes) { const markerRegex = new RegExp(`\\[paste #${pasteId}( (\\+\\d+ lines|\\d+ chars))?\\]`, "g"); result = result.replace(markerRegex, pasteContent);}// ...if (this.onSubmit) this.onSubmit(result);const text = this.editor.getText().trim();const text = (this.editor.getExpandedText?.() ?? this.editor.getText()).trim();const currentText = this.editor.getExpandedText?.() ?? this.editor.getText();/** * Get text with any markers expanded (e.g., paste markers). * Falls back to getText() if not implemented. */getExpandedText?(): string;// packages/tui/src/editor-component.ts/** * Get text with any markers expanded (e.g., paste markers). * Falls back to getText() if not implemented. */getExpandedText?(): string;
Consistency with other resource types (though not currently used in this file).
All changes are intentional and correct. Want me to mark it complete?
yes
Thinking...
edit ~/workspaces/pi-mono/todo.md:14
... 10 - [ ] `packages/coding-agent/src/core/settings-manager.ts` 11 - [ ] `packages/coding-agent/src/core/skills.ts` 12 - [ ] `packages/coding-agent/src/core/prompt-templates.ts` 13 - [ ] `packages/coding-agent/src/core/system-prompt.ts`-14 - [] `packages/coding-agent/src/core/extensions/loader.ts`+14 - [x] `packages/coding-agent/src/core/extensions/loader.ts` 15 - [x] `packages/coding-agent/src/core/extensions/runner.ts` 16 - [x] `packages/coding-agent/src/core/model-registry.ts` 17 - [x] `packages/coding-agent/src/core/footer-data-provider.ts` 18 - [x] `packages/coding-agent/src/core/auth-storage.ts` ...
Thinking...