Skip to content

Commit 52af814

Browse files
shivamstaqclaude
andcommitted
Add OpenCode support, pane-scoped mouse selection, and search
- Provider abstraction (Provider interface) with Claude Code and OpenCode backends - OpenCode provider reads sessions from SQLite (~/.local/share/opencode/opencode.db) - Tabbed provider UI with 1/2/tab switching when both providers are available - Mouse mode always on: click to focus panes, drag-select in content pane with auto-clipboard copy, scroll wheel support - Content search (/) with live debounced highlighting and n/N match navigation - Session search overlay (/) with global/project scope toggle, debounced results Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a434928 commit 52af814

8 files changed

Lines changed: 1691 additions & 42 deletions

File tree

data.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ type TreeProject struct {
4141
LastActive string `json:"lastActive"`
4242
ConvCount int `json:"convCount"`
4343
MsgCount int `json:"msgCount"`
44+
Source string `json:"source,omitempty"` // "claude" or "opencode"
4445
}
4546

4647
type TreeConversation struct {
@@ -57,6 +58,7 @@ type TreeConversation struct {
5758
SubAgents []TreeSubAgent `json:"subAgents,omitempty"`
5859
FileEditCount int `json:"fileEditCount,omitempty"`
5960
ToolResults int `json:"toolResults,omitempty"`
61+
Source string `json:"source,omitempty"` // "claude" or "opencode"
6062
}
6163

6264
type TreeSubAgent struct {

go.mod

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require (
88
github.com/charmbracelet/glamour v1.0.0
99
github.com/charmbracelet/x/ansi v0.11.6
1010
github.com/yuin/goldmark v1.7.13
11+
modernc.org/sqlite v1.48.0
1112
)
1213

1314
require (
@@ -25,6 +26,8 @@ require (
2526
github.com/clipperhouse/displaywidth v0.11.0 // indirect
2627
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
2728
github.com/dlclark/regexp2 v1.11.5 // indirect
29+
github.com/dustin/go-humanize v1.0.1 // indirect
30+
github.com/google/uuid v1.6.0 // indirect
2831
github.com/gorilla/css v1.0.1 // indirect
2932
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
3033
github.com/mattn/go-isatty v0.0.20 // indirect
@@ -33,6 +36,8 @@ require (
3336
github.com/muesli/cancelreader v0.2.2 // indirect
3437
github.com/muesli/reflow v0.3.0 // indirect
3538
github.com/muesli/termenv v0.16.0 // indirect
39+
github.com/ncruces/go-strftime v1.0.0 // indirect
40+
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
3641
github.com/rivo/uniseg v0.4.7 // indirect
3742
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
3843
github.com/yuin/goldmark-emoji v1.0.6 // indirect
@@ -41,4 +46,7 @@ require (
4146
golang.org/x/sys v0.42.0 // indirect
4247
golang.org/x/term v0.36.0 // indirect
4348
golang.org/x/text v0.30.0 // indirect
49+
modernc.org/libc v1.70.0 // indirect
50+
modernc.org/mathutil v1.7.1 // indirect
51+
modernc.org/memory v1.11.0 // indirect
4452
)

go.sum

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,16 @@ github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJ
4242
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
4343
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
4444
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
45+
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
46+
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
47+
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
48+
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
49+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
50+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
4551
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
4652
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
53+
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
54+
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
4755
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
4856
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
4957
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
@@ -61,6 +69,10 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
6169
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
6270
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
6371
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
72+
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
73+
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
74+
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
75+
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
6476
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
6577
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
6678
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
@@ -71,8 +83,10 @@ github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
7183
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
7284
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
7385
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
74-
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
75-
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
86+
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
87+
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
88+
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
89+
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
7690
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
7791
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
7892
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
@@ -84,3 +98,33 @@ golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
8498
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
8599
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
86100
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
101+
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
102+
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
103+
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
104+
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
105+
modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=
106+
modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0=
107+
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
108+
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
109+
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
110+
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
111+
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
112+
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
113+
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
114+
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
115+
modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
116+
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
117+
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
118+
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
119+
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
120+
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
121+
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
122+
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
123+
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
124+
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
125+
modernc.org/sqlite v1.48.0 h1:ElZyLop3Q2mHYk5IFPPXADejZrlHu7APbpB0sF78bq4=
126+
modernc.org/sqlite v1.48.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
127+
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
128+
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
129+
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
130+
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=

main.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,23 @@ func main() {
4949
return
5050
}
5151

52+
// Discover available providers
53+
var providers []Provider
54+
claude := &ClaudeProvider{}
55+
if claude.Available() {
56+
providers = append(providers, claude)
57+
}
58+
opencode := NewOpenCodeProvider()
59+
if opencode.Available() {
60+
providers = append(providers, opencode)
61+
}
62+
if len(providers) == 0 && *filePath == "" {
63+
fmt.Fprintln(os.Stderr, "No session data found. Checked ~/.claude/ and ~/.local/share/opencode/")
64+
os.Exit(1)
65+
}
66+
5267
// Interactive TUI mode
53-
p := tea.NewProgram(newModel(*filePath))
68+
p := tea.NewProgram(newModel(*filePath, providers))
5469
if _, err := p.Run(); err != nil {
5570
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
5671
os.Exit(1)

provider.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package main
2+
3+
// Provider abstracts a session data source (Claude Code, OpenCode, etc.).
4+
type Provider interface {
5+
Name() string
6+
Available() bool
7+
LoadTree() (*TreeData, error)
8+
LoadConversation(path string) ([]Entry, error)
9+
SearchSessions(query, projectID string) []SearchResult
10+
}
11+
12+
// SearchResult is a single match from session search.
13+
type SearchResult struct {
14+
Source string // provider name
15+
ProjectName string
16+
Title string
17+
Preview string
18+
Path string // conversation path or session ID
19+
ModTime string
20+
ProjIndex int // index into provider's tree.Projects
21+
}

provider_claude.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package main
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"strings"
7+
)
8+
9+
// ClaudeProvider loads sessions from Claude Code's ~/.claude/ directory.
10+
type ClaudeProvider struct{}
11+
12+
func (p *ClaudeProvider) Name() string { return "Claude Code" }
13+
14+
func (p *ClaudeProvider) Available() bool {
15+
home, err := os.UserHomeDir()
16+
if err != nil {
17+
return false
18+
}
19+
return dirExists(filepath.Join(home, ".claude", "projects"))
20+
}
21+
22+
func (p *ClaudeProvider) LoadTree() (*TreeData, error) {
23+
tree, err := loadTree()
24+
if err != nil {
25+
return nil, err
26+
}
27+
// Tag all projects and conversations with source
28+
for i := range tree.Projects {
29+
tree.Projects[i].Source = "claude"
30+
for j := range tree.Projects[i].Conversations {
31+
tree.Projects[i].Conversations[j].Source = "claude"
32+
}
33+
}
34+
return tree, nil
35+
}
36+
37+
func (p *ClaudeProvider) LoadConversation(path string) ([]Entry, error) {
38+
return parseConversation(path)
39+
}
40+
41+
func (p *ClaudeProvider) SearchSessions(query, projectID string) []SearchResult {
42+
tree, err := p.LoadTree()
43+
if err != nil || tree == nil {
44+
return nil
45+
}
46+
q := strings.ToLower(query)
47+
var results []SearchResult
48+
for i, proj := range tree.Projects {
49+
if projectID != "" && proj.DirName != projectID {
50+
continue
51+
}
52+
for _, conv := range proj.Conversations {
53+
title := strings.ToLower(conv.Title)
54+
preview := strings.ToLower(conv.Preview)
55+
slug := strings.ToLower(conv.Slug)
56+
projName := strings.ToLower(proj.DisplayName)
57+
if strings.Contains(title, q) || strings.Contains(preview, q) ||
58+
strings.Contains(slug, q) || strings.Contains(projName, q) {
59+
results = append(results, SearchResult{
60+
Source: "claude",
61+
ProjectName: proj.DisplayName,
62+
Title: conv.Title,
63+
Preview: conv.Preview,
64+
Path: conv.Path,
65+
ModTime: conv.ModTime,
66+
ProjIndex: i,
67+
})
68+
}
69+
}
70+
}
71+
return results
72+
}

0 commit comments

Comments
 (0)