Skip to content

Expose session and tree browsing/editing to RPC protocol#1762

Draft
dnouri wants to merge 12 commits intobadlogic:mainfrom
dnouri:rpc-browsing-surface
Draft

Expose session and tree browsing/editing to RPC protocol#1762
dnouri wants to merge 12 commits intobadlogic:mainfrom
dnouri:rpc-browsing-surface

Conversation

@dnouri
Copy link
Contributor

@dnouri dnouri commented Mar 3, 2026

Reopening #1628 (auto-closed by bot).


Motivation

The RPC protocol already covers prompting, streaming, model/thinking configuration, compaction, forking, session switching, and message retrieval. What's missing is session discovery and tree-structured navigation, the operations the TUI's session selector and tree selector provide interactively.

This PR adds five new RPC commands that close that gap, plus a shared tool-call formatting module extracted from the TUI.

What this adds

RPC commands

Five new commands with typed client helpers, transport types, and docs:

  • list_sessions: scope "current" or "all". Each session item always includes allMessagesText for client-side search.
  • get_tree: lightweight projection of the session tree for browsing UIs. Bookkeeping entries (labels, session_info, custom) are filtered out and their children re-parented. See rpc.md > Tree Node Types for node variants.
  • navigate_tree: delegates to AgentSession.navigateTree with normalized options. Supports optional branch summarization and labeling.
  • set_label: set or clear a label on a tree entry. Whitespace-only strings clear the label, matching navigate_tree behavior.
  • abort_branch_summary: cancels in-progress branch summarization. The existing abort command only aborts the agent loop, not the separate summarization AbortController.

Command shapes live in rpc-types.ts and are exported via modes/index.ts for SDK consumers.

Shared formatToolCall

Tool-call preview formatting was extracted from the TUI's tree-selector.ts into core/format-tool-call.ts and is now shared by both the TUI and RPC tree projection. Previews went from generic JSON truncation to human-readable bracket expressions:

[read: ~/projects/file.ts:10-29]
[bash: git status && git diff]
[grep: /TODO/ in src/]

Intentionally deferred

Two session management operations that the TUI supports are not included:

  • rename_session: The TUI renames any session by temporarily opening the target file and writing a session_info entry. The existing set_session_name RPC command only works on the current session.
  • delete_session: The TUI deletes sessions via the trash CLI with a fallback to unlink. No RPC equivalent exists.

Both are scoped out to keep this PR focused on read and navigate operations. They're straightforward to add as a follow-up.

Design decisions

Internal toolResult session entries carry only a toolCallId, not the tool name or arguments: those live on the originating assistant message. The projector resolves each tool_result against tool calls seen along its branch path, populating toolName, toolArgs, and formattedToolCall on the projected node. Branch-scoped resolution prevents cross-contamination when the same toolCallId appears in different branches.

Iterative traversal: Tree projection and leaf resolution use explicit stacks instead of recursion. Deep trees (long sessions with many branches) can exceed call-stack limits under recursion.

Architecture

Command handlers are inlined in rpc-mode.ts case bodies, matching the pattern of existing RPC commands. Two helper modules provide reusable logic:

  • rpc-tree-projection.ts: projects the raw SessionTreeNode tree into transport-safe RpcTreeNode arrays, resolves tool-call metadata, and finds the projected leaf.
  • rpc-command-wiring.ts: transport mapping (toRpcSessionListItem, toRpcNavigateTreeResult) and label normalization.

syncLocation() is added to SessionManager, called after setSessionFile() during session switches. The new list_sessions(scope="current") command reads cwd and sessionDir to scope results; without this, switching sessions via RPC would leave those fields pointing at the previous project.

Error handling: navigate_tree and set_label catch thrown errors (invalid entry IDs) and return typed RPC errors. The remaining new commands cannot fail under normal operation and propagate exceptions to the top-level handler.

@dnouri
Copy link
Contributor Author

dnouri commented Mar 3, 2026

Here's a bit of a teaser video to show how this works out in the Emacs client:

rpc-browsing-surface.mp4

@dnouri dnouri force-pushed the rpc-browsing-surface branch 3 times, most recently from 484c521 to 45e8597 Compare March 3, 2026 12:22
@dnouri dnouri force-pushed the rpc-browsing-surface branch from 45e8597 to 899cfae Compare March 3, 2026 16:52
@badlogic badlogic marked this pull request as draft March 5, 2026 23:11
@badlogic
Copy link
Owner

badlogic commented Mar 5, 2026

Setting this to draft because it has a couple of issues.

I can't figure out why you split setSessionFile() into setSessionFile() and syncLocation(). That breaks the SessionManager invariant. After setSessionFile(), the manager can be in a split state where the loaded session file, entries, and session id belong to one session, but cwd and sessionDir still belong to another.

I'm afraid that won't fly. Can you explain why that split was needed?

@dnouri
Copy link
Contributor Author

dnouri commented Mar 6, 2026

You're right, sorry for the confusion. I got confused by main's behaviour where switchSession does not update process.cwd() nor sessionManager.getCwd() when you switch across projects. This behaviour seems unintentional. If I resume a session from another project, I might expect tool calls in the new session to happen relative to its project folder, but as is they're still going to execute in the cwd of the session that I switched from. To that extent, I don't think the invariant that you describe is enforced on main.

The way this surfaced for me was that list_sessions with scope: "current" after a cross-project switch was listing sessions from the original project instead of the switched-to project. I added syncLocation() to fix that, but that only patched sessionManager.getCwd() and getSessionDir(). It doesn't touch agentSession._cwd, the tools (which capture cwd at creation time), or process.cwd(). So after the fix, session listing follows the switch but tool execution doesn't, which is arguably worse than main's behavior where at least everything consistently stays in the original directory.

Maybe the right thing to do is to change cross-project session switching such that it updates cwd everywhere and re-creates tools? For now I'll remove syncLocation() from this branch to avoid the half-consistent state.

dnouri added 9 commits March 6, 2026 13:26
Add five RPC commands that let external clients browse sessions and
navigate the conversation tree without reimplementing the session
file format or shelling out to the TUI:

- list_sessions (scope: current | all)
- get_tree (lightweight tree projection with optional full content)
- navigate_tree (with optional branch summarization and labeling)
- set_label (set or clear a label on any tree entry)
- abort_branch_summary (cancel in-progress branch summarization)

Tree projection filters metadata entries (label, session_info,
custom) and re-parents their children to keep the tree structurally
valid. Tool-result nodes are enriched with metadata from the
corresponding tool_use, using branch-local resolution to prevent
cross-branch contamination when the same toolCallId appears on
sibling branches. Traversal is iterative to handle deep trees.

Extract formatToolCall from tree-selector.ts into
core/format-tool-call.ts so the RPC tree projection and TUI share
the same tool call previews:
  [read: ~/file.ts:10-29]
  [bash: git status && git diff]

Split setSessionFile() and syncLocation() in session-manager so
session file loading no longer implicitly mutates cwd and sessionDir.

allMessagesText is now always populated on session list items (the
includeSearchText toggle added no meaningful savings since I/O
dominates regardless).
Inline narrow *Like interfaces from rpc-command-wiring into
rpc-mode.ts handlers -- the indirection did not earn its keep for
what are 3-5 line case bodies. Remove wrapper type aliases
(RpcListSessionsResult, RpcGetTreeResult) and inline their shapes
directly into the RpcResponse union.

Guard navigate_tree and set_label with try/catch so invalid entry
IDs return typed RPC errors instead of falling through to the outer
parse-error handler. Add assistant to toRpcMessageRole switch for
robustness against future refactoring.

Delete mock-based rpc-client unit tests that spied on the private
send() method. The E2E tests in rpc.test.ts and the pure-function
wiring unit tests provide sufficient coverage without coupling to
client internals.

Add comprehensive Tree Node Types reference to rpc.md documenting
all 7 node variants with field descriptions and JSON examples.

Add per-type unit tests for non-message tree projection (compaction,
model_change, thinking_level_change, branch_summary, custom_message).
Split monolithic E2E browsing test into individual per-operation
tests.

Reorganize RPC sections: list_sessions moves into Session,
abort_branch_summary moves into Tree.
Pick navigate_tree option fields explicitly instead of spreading
the full RPC command object into the domain method. Prevents
leaking transport-level fields (id, type, targetId) into
AgentSession.navigateTree().

Move error checking from getData() into send() so all commands --
including void methods like setLabel -- throw on server errors.
Previously void methods silently swallowed error responses.
No consumer passes includeContent: true. Remove the flag, the
conditional content fields on three RpcTreeNode variants, and the
parameter threading through six internal functions.
… after cross-project switch

syncLocation updated sessionManager.getCwd() and getSessionDir() after
a cross-project session switch, but left agentSession._cwd, tool cwds,
and process.cwd() unchanged. This made session listing follow the switch
while tool execution stayed in the original directory — worse than main
where everything consistently reflects the launch directory.

Remove syncLocation entirely. list_sessions scope:current now always
returns sessions for the project directory pi was started in, matching
the TUI session selector behaviour on main.
@dnouri dnouri force-pushed the rpc-browsing-surface branch from ffccabb to 94d7b4e Compare March 6, 2026 12:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants