Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 23 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

<p align="center"><b>showagent</b> — every AI coding session on your machine, in one TUI.<br>
Browse, search, resume, branch — and <em>convert</em> a conversation from one agent to another.<br>
Codex · Claude Code · Gemini CLI · OpenCode · jcode</p>
Codex · Claude Code · Gemini CLI · OpenCode · jcode · Pi</p>

<p align="center">
<a href="https://github.com/aytzey/showagent/actions/workflows/ci.yml"><img src="https://github.com/aytzey/showagent/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
Expand All @@ -29,7 +29,8 @@ branch + convert across agents.
- **One list for everything** — sessions from every agent, grouped by
workspace, fuzzy-searchable, newest first.
- **Resume or branch anywhere** — reopen a session in its own CLI, or fork a
local copy to try a different direction.
local native-format copy of its transferable conversation to try a different
direction.
- **Convert between agents** — rewrite a session into another agent's native
format so that agent's own resume just works. Originals are never modified;
conversions are written atomically.
Expand All @@ -45,6 +46,7 @@ branch + convert across agents.
| Gemini CLI | `gemini` | `~/.gemini/tmp/<project>/chats/` | `GEMINI_CLI_HOME` | ✅ | ✅ |
| OpenCode | `opencode` | `opencode.db`, via the `opencode` CLI | `OPENCODE_DATA_HOME` | ✅ | ✅ |
| jcode | `jcode` | `~/.jcode/sessions/*.json` | `JCODE_HOME` | ✅ | ✅ |
| Pi | `pi` | `~/.pi/agent/sessions/**/*.jsonl` | `PI_CODING_AGENT_DIR`, `PI_CODING_AGENT_SESSION_DIR` | ✅ | ✅ |

Notes:

Expand All @@ -58,6 +60,15 @@ Notes:
its CLI because imports go through OpenCode itself.
- jcode is a niche, experimental agent CLI. Its support is auto-hidden: if no
`jcode` binary is on `PATH`, showagent never shows it.
- Pi sessions are versioned JSONL trees. showagent follows Pi's active leaf
through `parentId` links, so abandoned branches are not previewed or moved
into another agent. Converted sessions use Pi's native v3 format, verified
against `@earendil-works/pi-coding-agent` 0.80.6 source plus its export and
RPC session loader.
- A project-local Pi `sessionDir` is visible when showagent is launched from
that project. Like Pi itself, showagent cannot discover arbitrary custom
session roots belonging to other projects unless one is selected globally
with `PI_CODING_AGENT_SESSION_DIR`.
- Platforms: Linux and macOS (amd64 + arm64). Windows (amd64) builds are
released but **experimental**: resume runs the agent as a child process
instead of replacing showagent.
Expand Down Expand Up @@ -110,7 +121,7 @@ showagent --help # full CLI help
| `t` | Cycle the convert scope: all turns, or latest 200/100/50/20/10 |
| `x` | Preview convert; press `x` again to write and select the new session |
| `n` | Branch: create a full local copy of the session |
| `y` | Toggle yolo resume (skip the agent's permission prompts) |
| `y` | Toggle the provider's yolo resume mode (jcode/Pi add no flag) |
| `C` | Compound: resume with a learnings-capture prompt (see below) |
| `d`, `del`, `backspace` | Delete the session — second press confirms, moving disarms |
| `r` | Rescan session stores (keeps cursor, search, and filters) |
Expand Down Expand Up @@ -225,8 +236,9 @@ that did not create the session converts it first, so it has full context.

`showagent setup` installs the companion
[compound-engineering plugin](https://github.com/EveryInc/compound-engineering-plugin)
into the Codex and Claude Code CLIs found on the machine. It is idempotent and
only installs what is missing.
into the Codex, Claude Code, and Pi CLIs found on the machine. For Pi it also
installs the `pi-subagents` and `pi-ask-user` companion packages. The command
is idempotent and only installs what is missing.

## FAQ

Expand All @@ -237,7 +249,7 @@ provider, so MCP transcripts redact common secrets by default; keep that
boundary in mind before registering the server. showagent's own HTTP client is
used only by the optional release updater and startup update check (disable
with `SHOWAGENT_NO_UPDATE_CHECK=1`). `showagent setup` invokes the installed
Codex/Claude CLIs, which may download the requested plugin. Message previews
Codex/Claude/Pi CLIs, which may download the requested plugin. Message previews
also redact password-like strings and API keys before rendering (covered by
tests in [`internal/session/session_test.go`](internal/session/session_test.go)).
Release archives ship with a `SHA256SUMS` file, and releases after v0.7.0
Expand All @@ -257,15 +269,16 @@ second `x` writes it. In scripts, use `showagent convert ... --dry-run` for
the same preview. Conversion intentionally does **not** copy tool-call
internals, approval history, encrypted reasoning blobs, or provider
attachments: those are private to the source agent and would not replay
correctly anyway. `t` / `--scope` trims the scope to the latest N turns before
converting.
correctly anyway. Branching uses the same safe user/assistant-turn projection;
it does not byte-clone provider-private runtime metadata. `t` / `--scope` trims
the scope to the latest N turns before converting.

**What does delete actually do?**
Codex sessions are deleted through `codex delete --force`; OpenCode through
`opencode session delete` (which cascades inside its database). Claude Code
removes the JSONL plus its matching index entry, jcode removes the JSON plus
backup/journal sidecars, and Gemini removes its session file. Delete always
takes two presses, and moving the cursor disarms it.
backup/journal sidecars, and Gemini and Pi remove their session files. Delete
always takes two presses, and moving the cursor disarms it.

**Windows?**
Binaries are released and the whole TUI works, but resume semantics are
Expand Down
9 changes: 6 additions & 3 deletions cmd/showagent/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ func printNoSessions(stderr io.Writer) int {
}
_, _ = fmt.Fprintln(stderr, line)
}
_, _ = fmt.Fprintln(stderr, "start a conversation with a supported agent (codex, claude, gemini, opencode, jcode), then run showagent again")
_, _ = fmt.Fprintln(stderr, "start a conversation with a supported agent (codex, claude, gemini, opencode, jcode, pi), then run showagent again")
return 1
}

Expand Down Expand Up @@ -430,7 +430,7 @@ func usageError(stderr io.Writer, message string) int {
}

func printHelp(w io.Writer) {
_, _ = fmt.Fprintf(w, `showagent — browse, resume, branch, and hand off local Codex, Claude Code, Gemini CLI, OpenCode, and jcode sessions.
_, _ = fmt.Fprintf(w, `showagent — browse, resume, branch, and hand off local Codex, Claude Code, Gemini CLI, OpenCode, jcode, and Pi sessions.

Usage:
showagent open the interactive session picker
Expand All @@ -451,7 +451,8 @@ Flags:
-h, --help show this help
-v, --version print version
--json (list) emit a JSON array of sessions
--yolo (resume) skip the agent's permission prompts
--yolo (resume) request the provider's permission bypass
(jcode and Pi add no extra flag)
--to (convert) target provider: %s
--scope (convert) all, or last:N / last-N
--dry-run (convert) preview without writing anything
Expand All @@ -471,6 +472,8 @@ Session locations:
opencode ~/.local/share/opencode (override with OPENCODE_DATA_HOME;
read via the opencode CLI)
gemini ~/.gemini/tmp (override with GEMINI_CLI_HOME)
pi ~/.pi/agent/sessions (override with PI_CODING_AGENT_DIR or
PI_CODING_AGENT_SESSION_DIR)

When stdout is not a terminal, 'showagent' prints the plain table (same as 'showagent list').
`, strings.Join(session.ProviderNames(), ", "))
Expand Down
10 changes: 8 additions & 2 deletions cmd/showagent/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ func setFixtureHomes(t *testing.T) {
t.Setenv("JCODE_HOME", filepath.Join(root, "empty-jcode"))
t.Setenv("OPENCODE_DATA_HOME", filepath.Join(root, "empty-opencode"))
t.Setenv("GEMINI_CLI_HOME", filepath.Join(root, "empty-gemini"))
t.Setenv("PI_CODING_AGENT_DIR", filepath.Join(root, "empty-pi"))
t.Setenv("PI_CODING_AGENT_SESSION_DIR", "")

writeFixture(t, filepath.Join(codexHome, "sessions", "2026", "06", "01", "rollout-2026-06-01T12-00-00-"+codexID+".jsonl"), `
{"timestamp":"2026-06-01T09:00:00Z","type":"session_meta","payload":{"id":"`+codexID+`","cwd":"/work/codex"}}
Expand Down Expand Up @@ -67,7 +69,7 @@ func TestHelpFlag(t *testing.T) {
if code != 0 {
t.Fatalf("%s exit = %d, want 0", flag, code)
}
for _, want := range []string{"Usage:", "list", "resume", "convert", "info", "mcp", "update", "setup", "CODEX_HOME", "CLAUDE_HOME", "JCODE_HOME", "--yolo", "--json", "--dry-run", "--read-only", "--allow-secrets"} {
for _, want := range []string{"Usage:", "list", "resume", "convert", "info", "mcp", "update", "setup", "CODEX_HOME", "CLAUDE_HOME", "JCODE_HOME", "PI_CODING_AGENT_DIR", "PI_CODING_AGENT_SESSION_DIR", "--yolo", "--json", "--dry-run", "--read-only", "--allow-secrets"} {
if !strings.Contains(stdout, want) {
t.Fatalf("%s output missing %q:\n%s", flag, want, stdout)
}
Expand Down Expand Up @@ -210,6 +212,8 @@ func TestListJSONEmptyIsValidArray(t *testing.T) {
t.Setenv("JCODE_HOME", filepath.Join(root, "empty-jcode"))
t.Setenv("OPENCODE_DATA_HOME", filepath.Join(root, "empty-opencode"))
t.Setenv("GEMINI_CLI_HOME", filepath.Join(root, "empty-gemini"))
t.Setenv("PI_CODING_AGENT_DIR", filepath.Join(root, "empty-pi"))
t.Setenv("PI_CODING_AGENT_SESSION_DIR", "")

code, stdout, _ := runCLI(t, "list", "--json")
if code != 0 {
Expand Down Expand Up @@ -528,6 +532,8 @@ func TestListEmptyExplainsScannedDirs(t *testing.T) {
t.Setenv("JCODE_HOME", filepath.Join(root, "empty-jcode"))
t.Setenv("OPENCODE_DATA_HOME", filepath.Join(root, "empty-opencode"))
t.Setenv("GEMINI_CLI_HOME", filepath.Join(root, "empty-gemini"))
t.Setenv("PI_CODING_AGENT_DIR", filepath.Join(root, "empty-pi"))
t.Setenv("PI_CODING_AGENT_SESSION_DIR", "")

code, _, stderr := runCLI(t, "list")
if code != 1 {
Expand All @@ -537,7 +543,7 @@ func TestListEmptyExplainsScannedDirs(t *testing.T) {
"no supported local sessions found",
filepath.Join(root, "empty-codex", "sessions"),
filepath.Join(root, "empty-claude", "projects"),
"CODEX_HOME", "CLAUDE_HOME", "JCODE_HOME",
"CODEX_HOME", "CLAUDE_HOME", "JCODE_HOME", "PI_CODING_AGENT_DIR", "PI_CODING_AGENT_SESSION_DIR",
"start a conversation with a supported agent",
} {
if !strings.Contains(stderr, want) {
Expand Down
8 changes: 4 additions & 4 deletions internal/mcpserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func New(version string, options ...Options) *mcp.Server {

mcp.AddTool(server, &mcp.Tool{
Name: "list_sessions",
Description: "List local AI coding-agent sessions (Codex, Claude Code, Gemini CLI, OpenCode, jcode) newest first. " +
Description: "List local AI coding-agent sessions (Codex, Claude Code, Gemini CLI, OpenCode, jcode, Pi) newest first. " +
"Filter by provider, workspace path substring, or free-text query over workspace and first/last user message. " +
"Use this to find past work on a topic before pulling a session in with get_transcript or convert_session.",
Annotations: readOnly,
Expand All @@ -89,7 +89,7 @@ func New(version string, options ...Options) *mcp.Server {
if !opts.ReadOnly {
mcp.AddTool(server, &mcp.Tool{
Name: "branch_session",
Description: "Fork a session: write a full local copy as a new session of the same agent, leaving the original untouched. " +
Description: "Fork a session: write its transferable user/assistant turns as a new native session of the same agent, leaving the original untouched. " +
"Returns the new session id, its file, and the exact shell command that resumes the copy (never executed by this server).",
Annotations: additive,
}, branchSession)
Expand Down Expand Up @@ -127,7 +127,7 @@ func Run(ctx context.Context, version string, options ...Options) error {
}

type listSessionsArgs struct {
Provider string `json:"provider,omitempty" jsonschema:"only return sessions of this agent; one of the provider ids reported in results (codex, claude, gemini, opencode, jcode)"`
Provider string `json:"provider,omitempty" jsonschema:"only return sessions of this agent; one of the provider ids reported in results (codex, claude, gemini, opencode, jcode, pi)"`
Workspace string `json:"workspace,omitempty" jsonschema:"case-insensitive substring the session's workspace path must contain"`
Query string `json:"query,omitempty" jsonschema:"case-insensitive free text matched against the workspace path and the first/last user message"`
Limit int `json:"limit,omitempty" jsonschema:"maximum sessions to return, newest first (default 25, max 100)"`
Expand Down Expand Up @@ -305,7 +305,7 @@ func branchSession(_ context.Context, _ *mcp.CallToolRequest, args sessionIDArgs

type convertSessionArgs struct {
ID string `json:"id" jsonschema:"session id from list_sessions, or 'latest' for the most recent session"`
Target string `json:"target" jsonschema:"provider to convert the session to: codex, claude, gemini, opencode, or jcode"`
Target string `json:"target" jsonschema:"provider to convert the session to: codex, claude, gemini, opencode, jcode, or pi"`
}

func convertSession(_ context.Context, _ *mcp.CallToolRequest, args convertSessionArgs) (*mcp.CallToolResult, newSessionResult, error) {
Expand Down
25 changes: 25 additions & 0 deletions internal/mcpserver/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ func setFixtureHomes(t *testing.T) (codexHome, claudeHome string) {
t.Setenv("JCODE_HOME", filepath.Join(root, "empty-jcode"))
t.Setenv("OPENCODE_DATA_HOME", filepath.Join(root, "empty-opencode"))
t.Setenv("GEMINI_CLI_HOME", filepath.Join(root, "empty-gemini"))
t.Setenv("PI_CODING_AGENT_DIR", filepath.Join(root, "empty-pi"))
t.Setenv("PI_CODING_AGENT_SESSION_DIR", "")

writeFile(t, filepath.Join(codexHome, "sessions", "2026", "06", "01", "rollout-2026-06-01T12-00-00-"+codexRateLimitID+".jsonl"), `
{"timestamp":"2026-06-01T09:00:00Z","type":"session_meta","payload":{"id":"`+codexRateLimitID+`","cwd":"/work/api-server"}}
Expand Down Expand Up @@ -431,6 +433,29 @@ func TestNoDeleteToolExposed(t *testing.T) {
}
}

func TestProviderToolSchemasAdvertiseRegistry(t *testing.T) {
setFixtureHomes(t)
cs := newTestSession(t)
tools, err := cs.ListTools(context.Background(), nil)
if err != nil {
t.Fatal(err)
}
for _, tool := range tools.Tools {
if tool.Name != "list_sessions" && tool.Name != "convert_session" {
continue
}
data, err := json.Marshal(tool)
if err != nil {
t.Fatal(err)
}
for _, provider := range session.ProviderNames() {
if !strings.Contains(string(data), provider) {
t.Fatalf("%s schema does not advertise provider %q: %s", tool.Name, provider, data)
}
}
}
}

func TestReadOnlyServerOmitsWriteTools(t *testing.T) {
setFixtureHomes(t)
cs := newTestSession(t, Options{ReadOnly: true})
Expand Down
19 changes: 9 additions & 10 deletions internal/session/compound.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@ import (
// directory. If the chosen agent differs from the session's own provider, the
// session is first converted to that provider so the agent has full context.
//
// The shared learnings directory is the common knowledge pool: both Codex and
// Claude read it before working and append to it when done, so what either tool
// learns compounds for the other.
// The shared learnings directory is the common knowledge pool: every supported
// compound-capable agent reads it before working and appends to it when done.
func Compound(row Row, agent Provider, options ResumeOptions) error {
dir, err := ensureLearningsDir(row.CWD)
if err != nil {
Expand All @@ -31,7 +30,7 @@ func Compound(row Row, agent Provider, options ResumeOptions) error {
}
target = converted
}
return launch(target.resumeCWD(), target.CompoundCommand(options, compoundPrompt(dir)))
return launch(target.resumeCWD(), target.CompoundCommand(options, compoundPrompt(dir, agent)))
}

// CompoundCommand is the resume command for the session with an initial prompt
Expand All @@ -55,8 +54,8 @@ func learningsBaseDir() string {

// ProjectLearningsDir reports the per-project, cross-tool learnings directory
// for a workspace, without creating it. Each project gets its own subdirectory
// so learnings never bleed between projects, while Codex and Claude share the
// same one within a project.
// so learnings never bleed between projects, while all supported agents share
// the same one within a project.
func ProjectLearningsDir(cwd string) string {
key, ok := projectLearningsKey(cwd)
if !ok {
Expand Down Expand Up @@ -137,11 +136,11 @@ func projectLearningsKey(cwd string) (string, bool) {
return fmt.Sprintf("%s-%x", label, hash[:6]), true
}

func compoundPrompt(dir string) string {
func compoundPrompt(dir string, agent Provider) string {
return strings.Join([]string{
"Run a compound-engineering pass on the work from this session.",
"",
"This project's shared learnings directory (used by BOTH Codex and Claude for THIS project) is the path inside the fence below:",
"This project's shared learnings directory (used by every supported coding agent for THIS project) is the path inside the fence below:",
"```",
dir,
"```",
Expand All @@ -150,9 +149,9 @@ func compoundPrompt(dir string) string {
"Steps:",
"1. Read the existing notes in that directory (the *.md files) so you build on prior learnings and avoid duplicating them.",
"2. Identify the durable, reusable learnings from this session: the problem, the root cause, the fix, key decisions, and any gotchas or patterns worth keeping.",
"3. Write a concise markdown note into that directory named <date>-<slug>.md with short frontmatter (title, date, tags, tool: codex or claude) followed by the learning. If a closely related note already exists, update it instead of duplicating it.",
fmt.Sprintf("3. Write a concise markdown note into that directory named <date>-<slug>.md with short frontmatter (title, date, tags, tool: %s) followed by the learning. If a closely related note already exists, update it instead of duplicating it.", agent),
"4. Keep it factual and reusable, and never write secrets or tokens.",
"",
"This follows the compound-engineering method: each documented solution compounds the shared knowledge, so the next time either tool hits the same thing it is minutes, not hours.",
"This follows the compound-engineering method: each documented solution compounds the shared knowledge, so the next supported agent to hit the same thing needs minutes, not hours.",
}, "\n")
}
Loading