Skip to content

Fork from Chat Buffer Position #84

@dnouri

Description

@dnouri

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_messages response
  • Live path: Entry IDs come from get_fork_messages called after agent_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 to buildSessionContext at line 305 of session-manager.ts) and returns messages with their corresponding entry IDs
    • Reference getUserMessagesForForking() (line 2460) for the pattern of iterating entries and extracting IDs
  • packages/coding-agent/src/modes/rpc/rpc-mode.ts

    • Modify get_messages handler (line 507) to call session.getMessagesWithEntryIds() instead of session.messages
  • packages/coding-agent/src/modes/rpc/rpc-types.ts

    • Update response type for get_messages to indicate messages may include entryId?: string

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 buildSessionContext does
  • This is an additive change - existing clients that don't use entryId are 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-id parameter
  • When entry-id is non-nil and label is "You", attach pi-entry-id text 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-id to pi-coding-agent--make-separator
  • When entry-id is nil (live message), save marker to new buffer-local variable pi-coding-agent--pending-entry-id-marker pointing to start of "You" text (which is (point-max) + 1 before 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), extract entryId from 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-property to attach pi-entry-id at 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-marker is set
  • If so, call get_fork_messages RPC asynchronously
  • In callback, extract last entry's entryId and call pi-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):

  • RET is currently bound to pi-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-id text property at point
  • If entry ID found, prompt for confirmation: (yes-or-no-p "Fork from this message?")
  • On confirmation, call existing fork RPC 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-marker to 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_end handling (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 uses get_fork_messages
  • Same end result: All "You:" headers have pi-entry-id text property

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions