sessions: a session goes by the name its agent shows for it — Codex's thread name (session_index.jsonl), the one a Claude Code session was renamed to (/rename), else the title the agent made — and by its first prompt only when it has none (TJHHHH on Discord)
This commit is contained in:
1 parent
0ef7066915
commit
2086884bef
13 files changed
+81
-27
No files matched your search
@@ -89,7 +89,7 @@ func TestZCode(t *testing.T) {
|
||||
t.Fatalf("want 2 ZCode sessions (the subagent in its parent), got %d", n)
|
||||
}
|
||||
z := find(t, ss, "zcode", "ses_zcA")
|
||||
if z.Cwd != "/work/zc" || z.Title != "Port the parser" || z.Resume != "" {
|
||||
if z.Cwd != "/work/zc" || z.Title != "Port the parser to Go" || z.Resume != "" {
|
||||
t.Fatalf("zcode: %+v", z)
|
||||
}
|
||||
// input without the cache, output with its reasoning already
|
||||
@@ -117,7 +117,7 @@ func checkDsh(t *testing.T, ss []Session) {
|
||||
t.Fatalf("want 3 dsh sessions (the subagent in its parent), got %d", n)
|
||||
}
|
||||
a := find(t, ss, "dsh", "dsh-a")
|
||||
if a.Cwd != "/work/dsh" || a.Title != "Tidy the build script" || a.Resume != "" {
|
||||
if a.Cwd != "/work/dsh" || a.Title != "Build script tidy" || a.Resume != "" {
|
||||
t.Fatalf("dsh: %+v", a)
|
||||
}
|
||||
// the format 3 file, not the one left behind
|
||||
@@ -200,7 +200,7 @@ func TestCline(t *testing.T) {
|
||||
}
|
||||
c := find(t, ss, "cline", clineMain)
|
||||
// the prompt typed, out of Cline's tags, before its title
|
||||
if c.Cwd != "/work/cline" || c.Title != "Speed up the importer" {
|
||||
if c.Cwd != "/work/cline" || c.Title != "Importer speed-up" {
|
||||
t.Fatalf("cline: %+v", c)
|
||||
}
|
||||
if m := model(c, "claude-opus-5-5"); m.Tokens != (Tokens{100, 50, 1000, 500, 0}) || !m.Priced {
|
||||
|
||||
@@ -22,6 +22,7 @@ type ccLine struct {
|
||||
SessionID string `json:"sessionId"`
|
||||
RequestID ccStr `json:"requestId"`
|
||||
AITitle string `json:"aiTitle"`
|
||||
CustomTitle string `json:"customTitle"`
|
||||
Summary string `json:"summary"`
|
||||
Message struct {
|
||||
ID string `json:"id"`
|
||||
@@ -41,6 +42,7 @@ var (
|
||||
ccAssistant = []byte(`"type":"assistant"`)
|
||||
ccUser = []byte(`"type":"user"`)
|
||||
ccTitle = []byte(`"type":"ai-title"`)
|
||||
ccCustom = []byte(`"type":"custom-title"`)
|
||||
ccSummary = []byte(`"type":"summary"`)
|
||||
ccCwd = []byte(`"cwd":"`)
|
||||
)
|
||||
@@ -66,7 +68,7 @@ func claudeLine(s *state, b []byte, main bool) {
|
||||
switch typeAfter(h, ccTop) {
|
||||
case "user", "assistant":
|
||||
// a message laid out type first
|
||||
case "ai-title", "summary":
|
||||
case "ai-title", "custom-title", "summary":
|
||||
claudeFull(s, b, main)
|
||||
return
|
||||
default:
|
||||
@@ -223,7 +225,7 @@ func claudeFull(s *state, b []byte, main bool) {
|
||||
s.saw(at, main)
|
||||
want := bytes.Contains(b, ccAssistant) ||
|
||||
main && bytes.Contains(b, ccUser) ||
|
||||
main && (bytes.Contains(b, ccTitle) || bytes.Contains(b, ccSummary)) ||
|
||||
main && (bytes.Contains(b, ccTitle) || bytes.Contains(b, ccCustom) || bytes.Contains(b, ccSummary)) ||
|
||||
s.Cwd == "" && bytes.Contains(b, ccCwd)
|
||||
if !want {
|
||||
return
|
||||
@@ -243,6 +245,11 @@ func claudeFull(s *state, b []byte, main bool) {
|
||||
if l.AITitle != "" {
|
||||
s.Named = title(l.AITitle)
|
||||
}
|
||||
case "custom-title":
|
||||
// named by the user (/rename): before the one Claude Code made
|
||||
if main && l.CustomTitle != "" {
|
||||
s.Custom = title(l.CustomTitle)
|
||||
}
|
||||
case "summary":
|
||||
if l.Summary != "" && s.Named == "" {
|
||||
s.Named = title(l.Summary)
|
||||
|
||||
@@ -237,7 +237,7 @@ func TestCursor(t *testing.T) {
|
||||
if !ok {
|
||||
t.Fatal("the chat isn't listed")
|
||||
}
|
||||
if s.Title != "Start the port of the parser" || s.Cwd != curCwd {
|
||||
if s.Title != "Port the parser" || s.Cwd != curCwd {
|
||||
t.Fatalf("title %q cwd %q", s.Title, s.Cwd)
|
||||
}
|
||||
if !s.Start.Equal(time.UnixMilli(curT0)) || !s.Last.Equal(time.UnixMilli(curT1)) {
|
||||
@@ -254,7 +254,7 @@ func TestCursor(t *testing.T) {
|
||||
t.Fatalf("resume %q, want %q", s.Resume, want)
|
||||
}
|
||||
o, ok := findSession(ss, "cursor", curOther)
|
||||
if !ok || o.Title != "tidy the imports" || o.Cwd != "" || o.Resume != "" {
|
||||
if !ok || o.Title != "Tidy imports" || o.Cwd != "" || o.Resume != "" {
|
||||
t.Fatalf("the chat with no meta.json: %+v (its folder unknown, it has no resume)", o)
|
||||
}
|
||||
if !o.Last.Equal(curOld) {
|
||||
@@ -330,7 +330,7 @@ func TestCursorDelete(t *testing.T) {
|
||||
}
|
||||
}
|
||||
Reset()
|
||||
if s, ok := findSession(List(0), "cursor", curMain); !ok || s.Title != "Start the port of the parser" {
|
||||
if s, ok := findSession(List(0), "cursor", curMain); !ok || s.Title != "Port the parser" {
|
||||
t.Fatalf("the restored chat: %+v", s)
|
||||
}
|
||||
}
|
||||
@@ -169,6 +169,9 @@ func parseDroid(f file) *state {
|
||||
var st droidStart
|
||||
if l.Type == "session_start" && json.Unmarshal(b, &st) == nil {
|
||||
s.ID, s.Cwd, s.Named = st.ID, st.Cwd, title(st.Title)
|
||||
if s.Named == "New Session" {
|
||||
s.Named = "" // Droid's until it names one
|
||||
}
|
||||
if st.LastCwd != "" {
|
||||
s.Cwd = st.LastCwd
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ func TestDroid(t *testing.T) {
|
||||
t.Fatalf("want 3 Droid sessions (the subagent in its session), got %d", n)
|
||||
}
|
||||
d := find(t, ss, "droid", droidMain)
|
||||
if d.Cwd != "/work/droid" || d.Title != "Fix the login bug" {
|
||||
if d.Cwd != "/work/droid" || d.Title != "Login bug" {
|
||||
t.Fatalf("droid: %+v", d)
|
||||
}
|
||||
// magpie's custom model by the model it asks for, the subagent's in
|
||||
@@ -87,7 +87,7 @@ func TestDroid(t *testing.T) {
|
||||
// the fork: what it copied is its source's; a custom model of the
|
||||
// user's own by its name
|
||||
f := find(t, ss, "droid", droidFork)
|
||||
if f.Title != "Try another way" || !f.Start.Equal(at("2026-09-27T09:00:00Z")) || !f.Last.Equal(at("2026-09-27T09:00:30Z")) {
|
||||
if f.Title != "Login bug" || !f.Start.Equal(at("2026-09-27T09:00:00Z")) || !f.Last.Equal(at("2026-09-27T09:00:30Z")) {
|
||||
t.Fatalf("fork: %+v", f)
|
||||
}
|
||||
if m := model(f, "Kimi-K2"); m.Tokens != (Tokens{7, 3, 0, 0, 0}) {
|
||||
|
||||
@@ -24,7 +24,7 @@ func TestGrok(t *testing.T) {
|
||||
}
|
||||
g := find(t, ss, "grok", grokMain)
|
||||
// the first prompt typed, its chunks put together, before the title Grok made
|
||||
if g.Cwd != "/work/grok" || g.Title != "Port the parser" {
|
||||
if g.Cwd != "/work/grok" || g.Title != "Parser port" {
|
||||
t.Fatalf("grok: %+v", g)
|
||||
}
|
||||
// the cache out of the input, the subagent's turn in once
|
||||
@@ -45,7 +45,7 @@ func TestGrok(t *testing.T) {
|
||||
}
|
||||
// the fork: what it copied counted in the session it came from
|
||||
f := find(t, ss, "grok", grokFork)
|
||||
if f.Tokens != (Tokens{100, 30, 300, 0, 0}) || f.Title != "Try another way" {
|
||||
if f.Tokens != (Tokens{100, 30, 300, 0, 0}) || f.Title != "Parser port, another way" {
|
||||
t.Fatalf("fork: %+v", f)
|
||||
}
|
||||
if !f.Start.Equal(at("2026-09-27T09:00:00.25Z")) || !f.Last.Equal(at("2026-09-27T09:00:30Z")) {
|
||||
|
||||
@@ -23,7 +23,7 @@ func TestOmp(t *testing.T) {
|
||||
t.Fatalf("want 3 omp sessions (the subagents and advisor in theirs), got %d", n)
|
||||
}
|
||||
m := find(t, ss, "omp", ompMain)
|
||||
if m.Cwd != "/work/omp" || m.Title != "Port the parser" {
|
||||
if m.Cwd != "/work/omp" || m.Title != "Port the parser to omp" {
|
||||
t.Fatalf("omp: %+v", m)
|
||||
}
|
||||
// the task tool's summed usage not counted again
|
||||
|
||||
@@ -61,7 +61,7 @@ func checkOpenCode(t *testing.T, ss []Session) {
|
||||
t.Fatalf("want 1 OpenCode session (the child in its parent, the empty one out), got %d: %+v", n, ss)
|
||||
}
|
||||
oc := find(t, ss, "opencode", ocMain)
|
||||
if oc.Cwd != "/work/oc" || oc.Title != "Why does the login test flake?" {
|
||||
if oc.Cwd != "/work/oc" || oc.Title != "Flaky test hunt" {
|
||||
t.Fatalf("opencode: %+v", oc)
|
||||
}
|
||||
if a := model(oc, "gpt-6-astra"); a.Tokens != (Tokens{1000, 120, 5000, 0, 0}) || !a.Priced {
|
||||
@@ -210,7 +210,7 @@ func TestPi(t *testing.T) {
|
||||
setupMore(t)
|
||||
ss := List(0)
|
||||
p := find(t, ss, "pi", piFirst)
|
||||
if p.Cwd != "/work/pi" || p.Title != "Refactor the parser" {
|
||||
if p.Cwd != "/work/pi" || p.Title != "Parser refactor" {
|
||||
t.Fatalf("pi: %+v", p)
|
||||
}
|
||||
// a tool's model work and a compaction on the model in use, a usage
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package sessions
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A session goes by the name its agent shows for it: Codex's thread name,
|
||||
// the one a Claude Code session was renamed to, else the title the agent
|
||||
// made, and the first prompt only when there is none (TJHHHH on Discord).
|
||||
func TestSessionTitles(t *testing.T) {
|
||||
claude, codex := setup(t)
|
||||
index := `{"id":"01a0bdc9-b5fd-7e63-9658-68bc8dce5ecd","thread_name":"Codex 起的名字","updated_at":"2026-09-23T00:27:30Z"}` + "\n"
|
||||
if err := os.WriteFile(filepath.Join(codex, "session_index.jsonl"), []byte(index), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ss := List(0)
|
||||
cx, cc := ss[0], ss[1]
|
||||
if cx.Title != "Codex 起的名字" {
|
||||
t.Fatalf("codex: the thread name, not the first prompt: %q", cx.Title)
|
||||
}
|
||||
if cc.Title != "Fix login bug" {
|
||||
t.Fatalf("claude: the title Claude Code made, not the first prompt: %q", cc.Title)
|
||||
}
|
||||
|
||||
path := filepath.Join(claude, "projects", "-work-app", "11111111-2222-3333-4444-555555555555.jsonl")
|
||||
f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.WriteString(`{"type":"custom-title","customTitle":"Login, renamed","sessionId":"11111111-2222-3333-4444-555555555555"}` + "\n")
|
||||
f.Close()
|
||||
if got := List(0)[1].Title; got != "Login, renamed" {
|
||||
t.Fatalf("claude: the name it was renamed to: %q", got)
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ package sessions
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"cmp"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
@@ -96,7 +97,7 @@ type Session struct {
|
||||
Agent string `json:"agent"` // magpie agent id: claude, codex, opencode, pi, omp, zcode, dsh, cline, qoder, qoder-cn, grok, workbuddy, droid, cursor, hermes, alma
|
||||
ID string `json:"id"`
|
||||
Cwd string `json:"cwd"`
|
||||
Title string `json:"title"` // the first prompt, else the agent's own title
|
||||
Title string `json:"title"` // the name it was given, else the agent's own title, else the first prompt
|
||||
Start time.Time `json:"start"`
|
||||
Last time.Time `json:"last"`
|
||||
Models []Model `json:"models"`
|
||||
@@ -138,6 +139,7 @@ type state struct {
|
||||
Cwd string `json:"cwd,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Named string `json:"named,omitempty"` // the agent's own title for it
|
||||
Custom string `json:"custom,omitempty"` // the name the user gave it
|
||||
First string `json:"first,omitempty"` // the first message, when no prompt looked typed
|
||||
Start time.Time `json:"start"`
|
||||
Last time.Time `json:"last"`
|
||||
@@ -530,7 +532,8 @@ func CachePath() string { return filepath.Join(filepath.Dir(catalog.CachePath())
|
||||
// 16: count Codex response records and compaction usage.
|
||||
// 17: reconcile recent Claude message revisions.
|
||||
// 18: Claude Code's 1-hour cache writes apart from its 5-minute ones.
|
||||
const cacheVersion = 18
|
||||
// 19: the names the user gave Claude Code's and WorkBuddy's (custom-title).
|
||||
const cacheVersion = 19
|
||||
|
||||
type cacheFile struct {
|
||||
Version int `json:"version"`
|
||||
@@ -903,7 +906,7 @@ func assemble(fs []file, price func(string) *catalog.Price) (Session, bool) {
|
||||
s.ReadOnly = s.ReadOnly || f.readOnly
|
||||
}
|
||||
s.ID = strings.TrimPrefix(fs[0].key, s.Agent+":")
|
||||
var named, first string
|
||||
var custom, named, first string
|
||||
models := map[string]*Model{}
|
||||
for _, f := range fs {
|
||||
st := cache[f.path]
|
||||
@@ -920,6 +923,9 @@ func assemble(fs []file, price func(string) *catalog.Price) (Session, bool) {
|
||||
if st.Named != "" {
|
||||
named = st.Named
|
||||
}
|
||||
if st.Custom != "" {
|
||||
custom = st.Custom
|
||||
}
|
||||
if first == "" {
|
||||
first = st.First
|
||||
}
|
||||
@@ -942,12 +948,13 @@ func assemble(fs []file, price func(string) *catalog.Price) (Session, bool) {
|
||||
m.add(t)
|
||||
}
|
||||
}
|
||||
if s.Title == "" {
|
||||
s.Title = named
|
||||
}
|
||||
if s.Title == "" {
|
||||
s.Title = first
|
||||
// the name the session goes by in its agent: one the user gave it, else
|
||||
// Codex's thread name (session_index.jsonl), else the title the agent
|
||||
// made, else the first prompt, else the first message
|
||||
if s.Agent == "codex" && s.WSL == "" && custom == "" {
|
||||
custom = CodexTitles([]string{s.ID})[s.ID]
|
||||
}
|
||||
s.Title = cmp.Or(custom, named, s.Title, first)
|
||||
for _, m := range models {
|
||||
if p := price(m.Model); p != nil {
|
||||
// a sum of the session's calls of the model, at its base price:
|
||||
|
||||
@@ -96,7 +96,7 @@ func TestList(t *testing.T) {
|
||||
}
|
||||
|
||||
// Claude Code: a message's blocks counted once, the subagent's calls in
|
||||
if cc.ID != "11111111-2222-3333-4444-555555555555" || cc.Cwd != "/work/app" || cc.Title != "Fix the login bug in auth.go" {
|
||||
if cc.ID != "11111111-2222-3333-4444-555555555555" || cc.Cwd != "/work/app" || cc.Title != "Fix login bug" {
|
||||
t.Fatalf("claude: %+v", cc)
|
||||
}
|
||||
if o := model(cc, "claude-opus-5-5"); o.Tokens != (Tokens{100, 50, 5000, 1000, 0}) || !o.Priced {
|
||||
|
||||
@@ -142,7 +142,7 @@ func workbuddyLine(s *state, b []byte, main bool) {
|
||||
case "custom-title":
|
||||
// named by the user: before the one WorkBuddy made
|
||||
if main && l.CustomTitle != "" {
|
||||
s.Title = title(l.CustomTitle)
|
||||
s.Custom = title(l.CustomTitle)
|
||||
}
|
||||
case "ai-title":
|
||||
if main && l.AITitle != "" {
|
||||
|
||||
@@ -84,8 +84,8 @@ func TestSessionsCmd(t *testing.T) {
|
||||
now := sessionsHome(t)
|
||||
|
||||
list := runSessions(t, now)
|
||||
wantAll(t, list, "ago", "Codex", "it's", "Add a README", "Claude Code", "app", "Fix the login bug in auth.go", "latest 2")
|
||||
if strings.Index(list, "Add a README") > strings.Index(list, "Fix the login") {
|
||||
wantAll(t, list, "ago", "Codex", "it's", "Add a README", "Claude Code", "app", "Fix login bug", "latest 2")
|
||||
if strings.Index(list, "Add a README") > strings.Index(list, "Fix login bug") {
|
||||
t.Error("not the latest first")
|
||||
}
|
||||
if got := runSessions(t, now, "--folder", "app"); strings.Contains(got, "README") || !strings.Contains(got, "login") {
|
||||
|
||||
Reference in new issue
Block a user