From 19a99ac6c43d6cc61ce328da1c5d322cbcef94bd Mon Sep 17 00:00:00 2001 From: ryancnelson Date: Thu, 14 May 2026 19:20:59 -0700 Subject: [PATCH 1/2] feat: add OMP adapter and fix symlink path encoding for macOS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add internal/adapter/omp: thin adapter for Oh My Pi sessions (~/.omp/agent/sessions/) using the same JSONL format as piagent - Refactor piagent.Adapter to expose NewCustom(sessionsDir, id, name, icon) so format-compatible forks don't duplicate 1000 lines of parser - Fix projectDirPath to call filepath.EvalSymlinks after filepath.Abs, so /tmp → /private/tmp on macOS and session directories are found - Add test case covering the /tmp symlink scenario --- cmd/sidecar/main.go | 1 + internal/adapter/omp/doc.go | 6 ++++++ internal/adapter/omp/register.go | 21 ++++++++++++++++++++ internal/adapter/piagent/adapter.go | 25 ++++++++++++++++++++---- internal/adapter/piagent/adapter_test.go | 4 +++- 5 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 internal/adapter/omp/doc.go create mode 100644 internal/adapter/omp/register.go diff --git a/cmd/sidecar/main.go b/cmd/sidecar/main.go index e5152e23..f69c72da 100644 --- a/cmd/sidecar/main.go +++ b/cmd/sidecar/main.go @@ -24,6 +24,7 @@ import ( _ "github.com/marcus/sidecar/internal/adapter/opencode" _ "github.com/marcus/sidecar/internal/adapter/pi" _ "github.com/marcus/sidecar/internal/adapter/piagent" + _ "github.com/marcus/sidecar/internal/adapter/omp" _ "github.com/marcus/sidecar/internal/adapter/warp" "github.com/marcus/sidecar/internal/app" "github.com/marcus/sidecar/internal/config" diff --git a/internal/adapter/omp/doc.go b/internal/adapter/omp/doc.go new file mode 100644 index 00000000..4cf0568d --- /dev/null +++ b/internal/adapter/omp/doc.go @@ -0,0 +1,6 @@ +// Package omp implements an adapter for Oh My Pi (OMP) agent sessions. +// +// OMP is a fork of Pi Agent and uses the same JSONL format. Sessions are stored +// in ~/.omp/agent/sessions// where encoded-path is the project +// path with slashes replaced by dashes, wrapped in double dashes. +package omp diff --git a/internal/adapter/omp/register.go b/internal/adapter/omp/register.go new file mode 100644 index 00000000..9b13e8d8 --- /dev/null +++ b/internal/adapter/omp/register.go @@ -0,0 +1,21 @@ +package omp + +import ( + "os" + "path/filepath" + + "github.com/marcus/sidecar/internal/adapter" + "github.com/marcus/sidecar/internal/adapter/piagent" +) + +func init() { + adapter.RegisterFactory(func() adapter.Adapter { + home, _ := os.UserHomeDir() + return piagent.NewCustom( + filepath.Join(home, ".omp", "agent", "sessions"), + "omp", + "OMP", + "Ω", + ) + }) +} diff --git a/internal/adapter/piagent/adapter.go b/internal/adapter/piagent/adapter.go index 7bfcd091..6fc74ac0 100644 --- a/internal/adapter/piagent/adapter.go +++ b/internal/adapter/piagent/adapter.go @@ -61,6 +61,9 @@ type toolUseRef struct { // Adapter implements the adapter.Adapter interface for standalone Pi Agent sessions. type Adapter struct { sessionsDir string + id string + name string + icon string sessionIndex map[string]string // sessionID -> file path metaCache map[string]sessionMetaCacheEntry msgCache *cache.Cache[messageCacheEntry] @@ -71,8 +74,17 @@ type Adapter struct { // New creates a new Pi Agent adapter. func New() *Adapter { home, _ := os.UserHomeDir() + return NewCustom(filepath.Join(home, ".pi", "agent", "sessions"), adapterID, adapterName, adapterIcon) +} + +// NewCustom creates a Pi Agent-compatible adapter with a custom sessions directory and identity. +// Use this for forks of Pi Agent that share the same JSONL format but store sessions elsewhere. +func NewCustom(sessionsDir, id, name, icon string) *Adapter { return &Adapter{ - sessionsDir: filepath.Join(home, ".pi", "agent", "sessions"), + sessionsDir: sessionsDir, + id: id, + name: name, + icon: icon, sessionIndex: make(map[string]string), metaCache: make(map[string]sessionMetaCacheEntry), msgCache: cache.New[messageCacheEntry](msgCacheMaxEntries), @@ -80,13 +92,13 @@ func New() *Adapter { } // ID returns the adapter identifier. -func (a *Adapter) ID() string { return adapterID } +func (a *Adapter) ID() string { return a.id } // Name returns the human-readable adapter name. -func (a *Adapter) Name() string { return adapterName } +func (a *Adapter) Name() string { return a.name } // Icon returns the adapter icon for badge display. -func (a *Adapter) Icon() string { return adapterIcon } +func (a *Adapter) Icon() string { return a.icon } // Detect checks if Pi Agent sessions exist for the given project. func (a *Adapter) Detect(projectRoot string) (bool, error) { @@ -272,6 +284,11 @@ func (a *Adapter) projectDirPath(projectRoot string) string { if err != nil { absPath = projectRoot } + // Resolve symlinks so that paths like /tmp (→ /private/tmp on macOS) match + // the encoding that Pi Agent uses when it records the session cwd. + if resolved, err := filepath.EvalSymlinks(absPath); err == nil { + absPath = resolved + } // /home/user/project → --home-user-project-- // Strip leading slash, replace remaining slashes with dashes, wrap in -- path := strings.TrimPrefix(absPath, "/") diff --git a/internal/adapter/piagent/adapter_test.go b/internal/adapter/piagent/adapter_test.go index 33d6c077..d11d9590 100644 --- a/internal/adapter/piagent/adapter_test.go +++ b/internal/adapter/piagent/adapter_test.go @@ -27,12 +27,14 @@ func TestProjectDirPath(t *testing.T) { a := New() tests := []struct { - input string + input string wantSuffix string }{ {"/home/user/project", "--home-user-project--"}, {"/home/user/my-app", "--home-user-my-app--"}, {"/", "----"}, + // /tmp is a symlink to /private/tmp on macOS; must resolve to --private-tmp-- + {"/tmp", "--private-tmp--"}, } for _, tt := range tests { From 8f4f2e21cbabacc203b780c004926711315ad6f7 Mon Sep 17 00:00:00 2001 From: ryancnelson Date: Thu, 14 May 2026 19:45:51 -0700 Subject: [PATCH 2/2] fix(omp): implement OMP v15+ path encoding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OMP changed its session directory encoding from the legacy piagent absolute style (--path-encoded--) to a home/tmp-relative format: ~/devel/escrow → -devel-escrow /tmp → -tmp (or legacy --private-tmp-- if not yet migrated) /other/path → --other-path-- (legacy fallback) Add WithProjectDirFunc() to piagent.Adapter so forks can supply their own encoding without duplicating the 1000-line parser. OMP's register.go provides the correct implementation matching packages/coding-agent/src/ session/session-manager.ts getDefaultSessionDirName(). --- internal/adapter/omp/register.go | 71 ++++++++++++++++++++++++++--- internal/adapter/piagent/adapter.go | 12 +++++ 2 files changed, 77 insertions(+), 6 deletions(-) diff --git a/internal/adapter/omp/register.go b/internal/adapter/omp/register.go index 9b13e8d8..e438b75b 100644 --- a/internal/adapter/omp/register.go +++ b/internal/adapter/omp/register.go @@ -3,6 +3,7 @@ package omp import ( "os" "path/filepath" + "strings" "github.com/marcus/sidecar/internal/adapter" "github.com/marcus/sidecar/internal/adapter/piagent" @@ -11,11 +12,69 @@ import ( func init() { adapter.RegisterFactory(func() adapter.Adapter { home, _ := os.UserHomeDir() - return piagent.NewCustom( - filepath.Join(home, ".omp", "agent", "sessions"), - "omp", - "OMP", - "Ω", - ) + sessionsDir := filepath.Join(home, ".omp", "agent", "sessions") + return piagent.NewCustom(sessionsDir, "omp", "OMP", "Ω"). + WithProjectDirFunc(projectDirPath) }) } + +// projectDirPath implements OMP's session directory encoding. +// +// OMP v15+ uses a home-relative encoding for paths within the user's home +// directory, a tmpdir-relative encoding for paths within the temp directory, +// and falls back to the legacy absolute encoding (--path-encoded--) for +// everything else. Old sessions created before the encoding change retain +// the legacy name; OMP migrates home-relative sessions automatically but +// leaves tmp sessions in place. +// +// Encoding rules (after symlink resolution and macOS /private stripping): +// - within home: - +// - within tmpdir: -tmp- (or -tmp if cwd==tmpdir) +// - elsewhere: ---- (legacy) +func projectDirPath(sessionsDir, projectRoot string) string { + cwd, err := filepath.Abs(projectRoot) + if err != nil { + cwd = projectRoot + } + if resolved, err := filepath.EvalSymlinks(cwd); err == nil { + cwd = resolved + } + // On macOS, strip the /private prefix that EvalSymlinks adds for /tmp etc. + // OMP does this to keep paths like /tmp stable across symlink resolution. + if strings.HasPrefix(cwd, "/private/") { + stripped := cwd[len("/private"):] + if sr, err := filepath.EvalSymlinks(stripped); err == nil && sr == cwd { + cwd = stripped + } + } + + home, _ := os.UserHomeDir() + if resolved, err := filepath.EvalSymlinks(home); err == nil { + home = resolved + } + tmpdir := os.TempDir() // returns /tmp on macOS (already without /private) + + switch { + case cwd == home || strings.HasPrefix(cwd, home+string(filepath.Separator)): + rel, _ := filepath.Rel(home, cwd) + encoded := strings.ReplaceAll(rel, string(filepath.Separator), "-") + if encoded == "." || encoded == "" { + return filepath.Join(sessionsDir, "-") + } + return filepath.Join(sessionsDir, "-"+encoded) + + case cwd == tmpdir || strings.HasPrefix(cwd, tmpdir+string(filepath.Separator)): + rel, _ := filepath.Rel(tmpdir, cwd) + encoded := strings.ReplaceAll(rel, string(filepath.Separator), "-") + if encoded == "." || encoded == "" { + return filepath.Join(sessionsDir, "-tmp") + } + return filepath.Join(sessionsDir, "-tmp-"+encoded) + + default: + // Legacy absolute encoding — also used for dirs that haven't been migrated. + path := strings.TrimPrefix(cwd, "/") + encoded := strings.ReplaceAll(path, "/", "-") + return filepath.Join(sessionsDir, "--"+encoded+"--") + } +} diff --git a/internal/adapter/piagent/adapter.go b/internal/adapter/piagent/adapter.go index 6fc74ac0..e41d26a4 100644 --- a/internal/adapter/piagent/adapter.go +++ b/internal/adapter/piagent/adapter.go @@ -64,6 +64,7 @@ type Adapter struct { id string name string icon string + projectDirFn func(sessionsDir, projectRoot string) string // nil → default piagent encoding sessionIndex map[string]string // sessionID -> file path metaCache map[string]sessionMetaCacheEntry msgCache *cache.Cache[messageCacheEntry] @@ -91,6 +92,13 @@ func NewCustom(sessionsDir, id, name, icon string) *Adapter { } } +// WithProjectDirFunc overrides the function used to map a project root to its +// sessions directory. Use this for Pi forks that encode paths differently. +func (a *Adapter) WithProjectDirFunc(fn func(sessionsDir, projectRoot string) string) *Adapter { + a.projectDirFn = fn + return a +} + // ID returns the adapter identifier. func (a *Adapter) ID() string { return a.id } @@ -279,7 +287,11 @@ func (a *Adapter) Watch(projectRoot string) (<-chan adapter.Event, io.Closer, er // projectDirPath converts a project root path to the Pi Agent sessions directory path. // Pi Agent encodes paths as: /home/user/project → --home-user-project-- +// If a.projectDirFn is set, it is called instead. func (a *Adapter) projectDirPath(projectRoot string) string { + if a.projectDirFn != nil { + return a.projectDirFn(a.sessionsDir, projectRoot) + } absPath, err := filepath.Abs(projectRoot) if err != nil { absPath = projectRoot