-
Notifications
You must be signed in to change notification settings - Fork 8
Description
Motivation
Currently, forking a conversation requires invoking C-c C-p f which opens a completing-read selector showing truncated message previews. Users must identify the desired branch point from a list, losing the spatial context they have when looking at the chat buffer.
The proposed improvement lets users fork by placing point on a "You:" header in the chat buffer and pressing RET. This preserves spatial context - users can see the full message and surrounding conversation, then fork directly from that position.
Approach Overview
Entry IDs (required for the fork RPC) will be retrieved via RPC responses rather than streaming events. This unifies the mechanism for both history loading and live messages:
- History path: Entry IDs come from an enriched
get_messagesresponse - Live path: Entry IDs come from
get_fork_messagescalled afteragent_end
Both paths attach entry IDs as text properties on "You:" headers, enabling fork-at-point via RET.
Backend Changes (pi-mono)
1. Enrich get_messages Response
Add entryId field to messages returned by the get_messages RPC command.
Files to modify:
-
packages/coding-agent/src/core/agent-session.ts- Add method
getMessagesWithEntryIds()that walks the entry path (similar tobuildSessionContextat line 305 ofsession-manager.ts) and returns messages with their corresponding entry IDs - Reference
getUserMessagesForForking()(line 2460) for the pattern of iterating entries and extracting IDs
- Add method
-
packages/coding-agent/src/modes/rpc/rpc-mode.ts- Modify
get_messageshandler (line 507) to callsession.getMessagesWithEntryIds()instead ofsession.messages
- Modify
-
packages/coding-agent/src/modes/rpc/rpc-types.ts- Update response type for
get_messagesto indicate messages may includeentryId?: string
- Update response type for
Implementation notes:
- Entry IDs should be included for all message types that create session entries (user, assistant, toolResult, compactionSummary, branchSummary, custom)
- The mapping between messages and entries is established by walking the entry tree path, same as
buildSessionContextdoes - This is an additive change - existing clients that don't use
entryIdare unaffected
Frontend Changes (pi-coding-agent)
1. Modify Separator Creation
Update pi-coding-agent--make-separator to accept an optional entry-id parameter and attach it as a text property.
File: pi-coding-agent.el
Current signature (line 822):
(defun pi-coding-agent--make-separator (label face &optional timestamp)New signature:
(defun pi-coding-agent--make-separator (label face &optional timestamp entry-id)Changes:
- Add
entry-idparameter - When
entry-idis non-nil andlabelis "You", attachpi-entry-idtext property to the header line
2. Modify User Message Display
Update pi-coding-agent--display-user-message to accept and pass through the entry ID, and to save a marker when no entry ID is provided.
File: pi-coding-agent.el
Current signature (line 840):
(defun pi-coding-agent--display-user-message (text &optional timestamp)New signature:
(defun pi-coding-agent--display-user-message (text &optional timestamp entry-id)Changes:
- Pass
entry-idtopi-coding-agent--make-separator - When
entry-idis nil (live message), save marker to new buffer-local variablepi-coding-agent--pending-entry-id-markerpointing to start of "You" text (which is(point-max) + 1before the insert, accounting for the leading newline)
3. Modify History Rendering
Update pi-coding-agent--display-history-messages to extract and pass entry IDs.
File: pi-coding-agent.el, function at line 2664
Changes:
- In the
"user"case (line 2688), extractentryIdfrom the message plist - Pass it to
pi-coding-agent--display-user-message
4. Add Entry ID Backfill Mechanism
Add buffer-local variable and backfill function for live messages.
New variable:
(defvar-local pi-coding-agent--pending-entry-id-marker nil
"Marker pointing to 'You' header awaiting entry ID backfill.")New function:
(defun pi-coding-agent--backfill-entry-id (entry-id)
"Attach ENTRY-ID to the pending user message header."
...)This function:
- Checks if marker is set and valid
- Uses
put-text-propertyto attachpi-entry-idat marker position - Clears the marker
5. Trigger Backfill on agent_end
Modify pi-coding-agent--display-agent-end (line 1008) to call get_fork_messages and backfill.
Changes:
- After existing cleanup, check if
pi-coding-agent--pending-entry-id-markeris set - If so, call
get_fork_messagesRPC asynchronously - In callback, extract last entry's
entryIdand callpi-coding-agent--backfill-entry-id
Performance: Benchmarking shows get_fork_messages takes 1-5ms for typical sessions, up to ~54ms for very large sessions (19MB, 317 user messages). This overhead is negligible.
6. Implement RET Binding for Fork-at-Point
Add handler for RET on "You:" headers in chat mode.
File: pi-coding-agent.el
Modify keymap (line 269):
RETis currently bound topi-coding-agent-visit-file- Change to a dispatcher function that checks context
New function:
(defun pi-coding-agent-chat-ret ()
"Handle RET in chat buffer - fork from user message or visit file."
...)This function:
- Check if point is on a line starting with "You" (use
(looking-at "^You")after(beginning-of-line)) - If yes, check for
pi-entry-idtext property at point - If entry ID found, prompt for confirmation:
(yes-or-no-p "Fork from this message?") - On confirmation, call existing
forkRPC with the entry ID - If not on "You:" line, delegate to
pi-coding-agent-visit-file
7. Reset Marker on Session Changes
Ensure marker is cleared when session state resets.
File: pi-coding-agent.el
Modify pi-coding-agent--reset-session-state (line 2728):
- Add
pi-coding-agent--pending-entry-id-markerto the list of variables reset to nil
Alternatives Considered
Event-based Entry IDs (Approach A)
Modify the message_end event to include entryId when a message is persisted.
Why not chosen:
- Requires changes to event schema in pi-agent-core
- Entry is created during
message_endhandling (after the event is emitted), requiring reordering of persistence and emission - More invasive change across multiple packages
Text Matching
Match displayed message text against get_fork_messages results to find entry IDs.
Why not chosen:
- Fragile: text could be modified by template expansion
- Fails if identical messages appear multiple times
- Conceptually wrong: correlating by content rather than identity
Re-render on agent_end
Call get_messages after each turn and re-render the entire chat buffer.
Why not chosen:
- Heavy: re-rendering causes flicker and loses scroll position
- Wasteful: most content hasn't changed
- Marker-based backfill is more surgical
Unified on message_end for Both Paths
Make history loading emit message_end events to reuse the same code path.
Why not chosen:
- Would require "replaying" potentially hundreds of events on session load
- Architecturally awkward: history is bulk data, events are for streaming
- The two paths exist for good reason and should remain separate
Summary
The chosen approach (RPC-based entry IDs) offers:
- Minimal backend changes: One new method, one RPC response enrichment
- No event schema changes: Avoids touching pi-agent-core
- Negligible overhead: <60ms even for very large sessions
- Clean separation: History uses
get_messages, live usesget_fork_messages - Same end result: All "You:" headers have
pi-entry-idtext property