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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ agentsview auto-discovers sessions from all of these:
| OpenClaw | `~/.openclaw/agents/` |
| OpenCode | `~/.local/share/opencode/` |
| OpenHands CLI | `~/.openhands/conversations/` |
| OhMyPi | `~/.omp/agent/sessions/` |
| Pi | `~/.pi/agent/sessions/` |
| Piebald | `~/.local/share/piebald/` |
| Positron Assistant | `~/Library/Application Support/Positron/User/` (macOS) |
Expand Down
1 change: 1 addition & 0 deletions cmd/agentsview/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,7 @@ func writeRootHelp(w io.Writer, root *cobra.Command) {
fmt.Fprintln(w, " ZED_DIR Zed data directory")
fmt.Fprintln(w, " QWEN_PROJECTS_DIR Qwen Code projects directory")
fmt.Fprintln(w, " QWENPAW_DIR QwenPaw workspaces directory")
fmt.Fprintln(w, " OMP_DIR OhMyPi sessions directory")
fmt.Fprintln(w, " DEEPSEEK_TUI_SESSIONS_DIR")
fmt.Fprintln(w, " DeepSeek TUI sessions directory")
fmt.Fprintln(w, " QCLAW_DIR QClaw agents directory")
Expand Down
22 changes: 21 additions & 1 deletion internal/parser/discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -1477,6 +1477,17 @@ func IsPiSessionFile(path string) bool {
// the directory name. Project is left empty so ParsePiSession
// can derive it from the header cwd field.
func DiscoverPiSessions(piDir string) []DiscoveredFile {
return discoverPiLikeSessions(piDir, AgentPi)
}

// DiscoverOMPSessions finds JSONL files under an OhMyPi session root.
// OMP uses the same layout and file format as Pi, rooted by default at
// ~/.omp/agent/sessions.
func DiscoverOMPSessions(ompDir string) []DiscoveredFile {
return discoverPiLikeSessions(ompDir, AgentOMP)
}

func discoverPiLikeSessions(piDir string, agent AgentType) []DiscoveredFile {
if piDir == "" {
return nil
}
Expand Down Expand Up @@ -1507,7 +1518,7 @@ func DiscoverPiSessions(piDir string) []DiscoveredFile {
}
files = append(files, DiscoveredFile{
Path: path,
Agent: AgentPi,
Agent: agent,
// Project intentionally empty; ParsePiSession
// derives project from the header cwd field.
})
Expand All @@ -1523,6 +1534,15 @@ func DiscoverPiSessions(piDir string) []DiscoveredFile {
// session ID by searching all encoded-cwd subdirectories
// under piDir for a file named <sessionID>.jsonl.
func FindPiSourceFile(piDir, sessionID string) string {
return findPiLikeSourceFile(piDir, sessionID)
}

// FindOMPSourceFile finds the original JSONL file for an OMP session ID.
func FindOMPSourceFile(ompDir, sessionID string) string {
return findPiLikeSourceFile(ompDir, sessionID)
}

func findPiLikeSourceFile(piDir, sessionID string) string {
if piDir == "" || !IsValidSessionID(sessionID) {
return ""
}
Expand Down
23 changes: 20 additions & 3 deletions internal/parser/pi.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,23 @@ import (
// message, model_change, and compaction entries.
func ParsePiSession(
path, project, machine string,
) (*ParsedSession, []ParsedMessage, error) {
return parsePiLikeSession(path, project, machine, AgentPi, "pi:")
}

// ParseOMPSession parses an OhMyPi JSONL session file. OMP uses the
// same on-disk session format as Pi, but sessions are identified with
// the omp agent type and omp: session ID prefix.
func ParseOMPSession(
path, project, machine string,
) (*ParsedSession, []ParsedMessage, error) {
return parsePiLikeSession(path, project, machine, AgentOMP, "omp:")
}

func parsePiLikeSession(
path, project, machine string,
agent AgentType,
idPrefix string,
) (*ParsedSession, []ParsedMessage, error) {
info, err := os.Stat(path)
if err != nil {
Expand Down Expand Up @@ -73,7 +90,7 @@ func ParsePiSession(
branchedFrom := gjson.Get(headerLine, "branchedFrom").Str
if branchedFrom != "" {
base := filepath.Base(branchedFrom)
parentSessionID = "pi:" + strings.TrimSuffix(base, filepath.Ext(base))
parentSessionID = idPrefix + strings.TrimSuffix(base, filepath.Ext(base))
}

// V1 detection: if header has no id, we may need to derive from filename.
Expand Down Expand Up @@ -194,10 +211,10 @@ func ParsePiSession(
}

sess := &ParsedSession{
ID: "pi:" + sessionID,
ID: idPrefix + sessionID,
Project: project,
Machine: machine,
Agent: AgentPi,
Agent: agent,
ParentSessionID: parentSessionID,
Cwd: cwd,
FirstMessage: firstMessage,
Expand Down
49 changes: 49 additions & 0 deletions internal/parser/pi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package parser
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"testing"

Expand Down Expand Up @@ -55,6 +57,53 @@ func TestParsePiSession_SessionHeader(t *testing.T) {
_ = msgs // not the focus of this sub-test
}

func TestOMPRegistryMetadata(t *testing.T) {
def, ok := AgentByType(AgentOMP)
require.True(t, ok)

assert.Equal(t, AgentOMP, def.Type)
assert.Equal(t, "OhMyPi", def.DisplayName)
assert.Equal(t, "OMP_DIR", def.EnvVar)
assert.Equal(t, "omp_dirs", def.ConfigKey)
assert.Equal(t, []string{".omp/agent/sessions"}, def.DefaultDirs)
assert.Equal(t, "omp:", def.IDPrefix)
assert.True(t, def.FileBased)
require.NotNil(t, def.DiscoverFunc)
require.NotNil(t, def.FindSourceFunc)
}

func TestParseOMPSession_SessionIdentity(t *testing.T) {
fixturePath := createTestFile(
t, "omp-test-session-uuid.jsonl",
loadFixture(t, "pi/session.jsonl"),
)
sess, msgs, err := ParseOMPSession(fixturePath, "", "local")
require.NoError(t, err)
require.NotNil(t, sess)

assert.Equal(t, "omp:pi-test-session-uuid", sess.ID)
assert.Equal(t, AgentOMP, sess.Agent)
assert.Equal(t, "omp:2025-01-01T09-00-00-000Z_parent-uuid", sess.ParentSessionID)
assert.Equal(t, "/Users/alice/code/my-project", sess.Cwd)
assert.Equal(t, "my_project", sess.Project)
require.NotEmpty(t, msgs)
}

func TestDiscoverOMPSessions(t *testing.T) {
root := t.TempDir()
projectDir := filepath.Join(root, "-Users-alice-code-my-project")
require.NoError(t, os.MkdirAll(projectDir, 0o755))
path := filepath.Join(projectDir, "omp-test-session-uuid.jsonl")
require.NoError(t, os.WriteFile(path, []byte(loadFixture(t, "pi/session.jsonl")), 0o644))

files := DiscoverOMPSessions(root)
require.Len(t, files, 1)
assert.Equal(t, path, files[0].Path)
assert.Equal(t, AgentOMP, files[0].Agent)
assert.Empty(t, files[0].Project)
assert.Equal(t, path, FindOMPSourceFile(root, "omp-test-session-uuid"))
}

func TestParsePiSession_SessionInfoName(t *testing.T) {
content := strings.Join([]string{
`{"type":"session","version":3,"id":"named-sess","timestamp":"2025-01-01T10:00:00Z","cwd":"/Users/alice/code/my-project"}`,
Expand Down
12 changes: 12 additions & 0 deletions internal/parser/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const (
AgentVSCodeCopilot AgentType = "vscode-copilot"
AgentVSCopilot AgentType = "visualstudio-copilot"
AgentPi AgentType = "pi"
AgentOMP AgentType = "omp"
AgentQwen AgentType = "qwen"
AgentCommandCode AgentType = "commandcode"
AgentDeepSeekTUI AgentType = "deepseek-tui"
Expand Down Expand Up @@ -320,6 +321,17 @@ var Registry = []AgentDef{
DiscoverFunc: DiscoverPiSessions,
FindSourceFunc: FindPiSourceFile,
},
{
Type: AgentOMP,
DisplayName: "OhMyPi",
EnvVar: "OMP_DIR",
ConfigKey: "omp_dirs",
DefaultDirs: []string{".omp/agent/sessions"},
IDPrefix: "omp:",
FileBased: true,
DiscoverFunc: DiscoverOMPSessions,
FindSourceFunc: FindOMPSourceFile,
},
{
Type: AgentQwen,
DisplayName: "Qwen Code",
Expand Down
8 changes: 8 additions & 0 deletions internal/parser/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ func TestAgentByType(t *testing.T) {
{AgentAmp, true},
{AgentVSCodeCopilot, true},
{AgentPi, true},
{AgentOMP, true},
{AgentDeepSeekTUI, true},
{"unknown", false},
}
Expand Down Expand Up @@ -236,6 +237,12 @@ func TestAgentByPrefix(t *testing.T) {
AgentPi,
true,
},
{
"omp prefix",
"omp:omp-session-uuid",
AgentOMP,
true,
},
{
"zed prefix",
"zed:sess-id",
Expand Down Expand Up @@ -315,6 +322,7 @@ func TestRegistryCompleteness(t *testing.T) {
AgentVSCodeCopilot,
AgentVSCopilot,
AgentPi,
AgentOMP,
AgentQwen,
AgentCommandCode,
AgentDeepSeekTUI,
Expand Down
54 changes: 32 additions & 22 deletions internal/sync/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -1101,27 +1101,29 @@ func (e *Engine) classifyOnePath(
return df, true
}

// Pi: <piDir>/<encoded-cwd>/<session>.jsonl
for _, piDir := range e.agentDirs[parser.AgentPi] {
if piDir == "" {
continue
}
if rel, ok := isUnder(piDir, path); ok {
parts := strings.Split(rel, sep)
if len(parts) != 2 {
continue
}
if !strings.HasSuffix(parts[1], ".jsonl") {
// Pi/OMP: <sessionsDir>/<encoded-cwd>/<session>.jsonl
for _, agent := range []parser.AgentType{parser.AgentPi, parser.AgentOMP} {
for _, piDir := range e.agentDirs[agent] {
if piDir == "" {
continue
}
if !parser.IsPiSessionFile(path) {
continue
if rel, ok := isUnder(piDir, path); ok {
parts := strings.Split(rel, sep)
if len(parts) != 2 {
continue
}
if !strings.HasSuffix(parts[1], ".jsonl") {
continue
}
if !parser.IsPiSessionFile(path) {
continue
}
return parser.DiscoveredFile{
Path: path,
Agent: agent,
// Project left empty; parser derives from header cwd.
}, true
}
return parser.DiscoveredFile{
Path: path,
Agent: parser.AgentPi,
// Project left empty; parser derives from header cwd.
}, true
}
}

Expand Down Expand Up @@ -2559,7 +2561,7 @@ func (e *Engine) syncAllLocked(

if verbose {
log.Printf(
"discovered %d files (%d claude, %d codex, %d copilot, %d gemini, %d cursor, %d amp, %d zencoder, %d iflow, %d vscode-copilot, %d visualstudio-copilot, %d pi, %d kiro, %d zed, %d vibe) in %s",
"discovered %d files (%d claude, %d codex, %d copilot, %d gemini, %d cursor, %d amp, %d zencoder, %d iflow, %d vscode-copilot, %d visualstudio-copilot, %d pi, %d omp, %d kiro, %d zed, %d vibe) in %s",
len(all),
counts[parser.AgentClaude],
counts[parser.AgentCodex],
Expand All @@ -2572,6 +2574,7 @@ func (e *Engine) syncAllLocked(
counts[parser.AgentVSCodeCopilot],
counts[parser.AgentVSCopilot],
counts[parser.AgentPi],
counts[parser.AgentOMP],
counts[parser.AgentKiro],
counts[parser.AgentZed],
counts[parser.AgentVibe],
Expand Down Expand Up @@ -4073,7 +4076,7 @@ func (e *Engine) processFile(
res = e.processVSCodeCopilot(file, info)
case parser.AgentVSCopilot:
res = e.processVisualStudioCopilot(file, info)
case parser.AgentPi:
case parser.AgentPi, parser.AgentOMP:
res = e.processPi(file, info)
case parser.AgentQwen:
res = e.processQwen(file, info)
Expand Down Expand Up @@ -6435,9 +6438,16 @@ func (e *Engine) processPi(
return processResult{skip: true}
}

sess, msgs, err := parser.ParsePiSession(
file.Path, file.Project, e.machine,
var (
sess *parser.ParsedSession
msgs []parser.ParsedMessage
err error
)
if file.Agent == parser.AgentOMP {
sess, msgs, err = parser.ParseOMPSession(file.Path, file.Project, e.machine)
} else {
sess, msgs, err = parser.ParsePiSession(file.Path, file.Project, e.machine)
}
if err != nil {
return processResult{err: err}
}
Expand Down