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:
yetone committed 2026-10-05 22:23:29 +08:00
1 parent 0ef7066915
commit 2086884bef
13 files changed
+81 -27

No files matched your search

+3 -3
View File
@@ -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 {
+9 -2
View File
@@ -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)
+3 -3
View File
@@ -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)
}
}
+3
View File
@@ -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
}
+2 -2
View File
@@ -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}) {
+2 -2
View File
@@ -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")) {
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
+37
View File
@@ -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)
}
}
+15 -8
View File
@@ -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:
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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 != "" {
+2 -2
View File
@@ -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") {