From 154f805ac6189a717b6f86849a53bac2cf6e22be Mon Sep 17 00:00:00 2001 From: cicada <122549661+lr00rl@users.noreply.github.com> Date: Fri, 23 Oct 2026 05:30:54 +0000 Subject: [PATCH] feat: say which copy of a session you are reading, and pick one with -a everywhere A session copied between accounts keeps its id, and the commands that read one session disagreed about how to choose a copy: show took -a, log, export and view rejected it, and none of them said which copy they had picked. `ccli log 1087b979 -a default` failed with "flag provided but not defined". show, log, export and view now all take -a , which means the same as writing :; naming two different accounts is refused, and an unknown account is reported. Without one they use the copy with the latest activity, as before, and now say on stderr which copy that is and how to pick another. view without an id takes -a as a filter on its list, as the list commands do. A copy's turn list explained nothing about its blank costs. log now writes `copied` in the COST column of the turns that came in with a copy or a fork, from the ledger's byte ranges, and a line under the table names where they came from and where their usage counts. show's copies section says the same in one line. The README gains a "Sessions, copies and accounts" reference: what an account, session, copy, fork, turn and agent are, how to name each, and for every command what it works on and what -a means there. The tutorial and DESIGN.md follow. --- DESIGN.md | 9 +++++ README.md | 40 ++++++++++++++++++- docs/tutorial.md | 24 ++++++++++++ internal/cli/export.go | 11 ++++-- internal/cli/log.go | 11 ++++-- internal/cli/transfer_test.go | 28 +++++++++++++ internal/cli/view.go | 13 +++++- internal/cli/views.go | 74 ++++++++++++++++++++++++++++++----- internal/query/detail.go | 18 +++++++++ internal/query/turns.go | 33 ++++++++++++++++ internal/render/log.go | 38 ++++++++++++++++++ internal/render/log_test.go | 21 ++++++++++ internal/render/views.go | 1 + internal/tui/app.go | 4 +- internal/tui/screens.go | 7 +++- 15 files changed, 308 insertions(+), 24 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index 7a8b545..b73cb1e 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -393,6 +393,15 @@ price". ## Query interface +A session id can be in several accounts: each is a copy, its own +transcript, and the index keys sessions by account and id. Commands that +read one session (`show`, `log`, `export`, `view`) take `:` or +`-a`, and otherwise use the copy with the latest activity and name the +others on stderr (`query.DB.CopiesOf`). `Turns` marks each turn whose prompt +lies in a byte range the ledger says ccli copied in (`TurnRow.CopiedFrom`: +the source account, or `account:id` for a fork). Marks that belong to the +id (hide, tag) apply to every copy. + Every command reads through `query.DB`; the terminal renderer only formats its results, and every result serializes as JSON. The main calls are `Accounts(filter)`, `Sessions(filter)`, `Session(ref)`, `Overview(filter)` diff --git a/README.md b/README.md index b608d9e..e5ea654 100644 --- a/README.md +++ b/README.md @@ -489,8 +489,44 @@ and per-day figures) on `sessions`, `show`, `log`, `stats`, `quota`, `accounts` and `view`. On `view` it starts with tool calls expanded. Both can sit before the command or after it. -`ccli -h` lists a command's flags. Session ids can be shortened to -any unique prefix, or written `account:id` to pick one account's copy. +`ccli -h` lists a command's flags. + +### Sessions, copies and accounts + +| Thing | What it is | How you name it | +| --- | --- | --- | +| account | a folder a tool keeps its sessions in: `~/.claude` is `default`, `~/.claude-work` is `work`, and Codex, pi and Cursor homes | its name, after `-a` | +| session | one conversation, known by its id | the id, or any unique prefix of at least 4 characters | +| copy | the same session, with the same id, in another account; `copy`, `move` and `resume -a` make one. Each copy is its own transcript. Copies share their beginning, and each counts only the work done in it | `:`, or the id with `-a ` | +| fork | a new session with a new id, made from a session, whole or through one turn | its own id | +| turn | a prompt and the work it led to, numbered in `ccli log` | `ccli log `, `ccli fork --turn ` | +| agent | a subagent's transcript; it belongs to the session that started it | `ccli log --agent ` | + +When a session is in more than one account and you name no account, ccli +uses the copy used last and says so on stderr: + +``` +ccli: 1087b979 is also in default; this is the copy in talcum, the one used last (-a default picks another) +``` + +`-a` always names the account you are looking at or working in: + +| Commands | Work on | `-a` means | +| --- | --- | --- | +| `sessions`, `prompts`, `projects`, `stats`, `quota`, `clean` | many sessions | only these accounts (repeat it or separate with commas) | +| `show`, `log`, `export`, `view ` | one copy | read the copy in this account | +| `view` without an id | the session list | list only this account | +| `resume` | one copy | resume in this account, after bringing its copy up to date | +| `fork` | a copy, into a new session | put the new session in this account; `--from` or `:` picks the copy to fork | +| `copy`, `move` | a copy, into another account | not used: `--from` and `--to` name both ends | +| `diff` | two accounts | not used: name the two accounts | +| `hide`, `unhide`, `tag` | every copy of the id | not used: the mark belongs to the session id | + +In a copy's `ccli log`, the turns that came in with the copy show `copied` +in the COST column, and a line under the table says where they came from +and where their usage counts. ccli knows this for the copies it made; a +transcript copied by hand has no such record, and its copied turns simply +show no cost. ## How it counts diff --git a/docs/tutorial.md b/docs/tutorial.md index 10603b4..633641d 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -392,6 +392,30 @@ To bring work's sessions into default: ccli copy --from work --to default (prin "work is ahead" means default's transcript is an exact prefix of work's: the session went on in work after default last saw it. +### Which copy you are looking at + +`746cbfa4` is in both accounts under the same id. `show`, `log`, `export` +and `view` read one copy: the one you name with `-a` (or as +`default:746cbfa4`), or else the one used last, and they say which on +stderr: + +``` +$ ccli log 746cbfa4 +ccli: 746cbfa4 is also in work; this is the copy in default, the one used last (-a work picks another) +Why does the nightly export take forty minutes now? +746cbfa4 · default · 25 prompts typed · $0 +``` + +In a copy's turn list, the turns that came in with a copy show `copied` in +the COST column, and a line under the table explains them: + +``` +turns 18 to 25 came with a copy from work; their usage counts in work (ccli log 746cbfa4 -a work) +``` + +The README's "Sessions, copies and accounts" table lists what every command +works on and what `-a` means for it. + ### Continue a session in the other account, and come back Say `746cbfa4` went on in work, and now you want to continue it in default. diff --git a/internal/cli/export.go b/internal/cli/export.go index ed4c9cf..bcaa084 100644 --- a/internal/cli/export.go +++ b/internal/cli/export.go @@ -27,22 +27,27 @@ func cmdExport(ctx context.Context, a *app, args []string) error { thinking := fs.Bool("thinking", false, "include the model's thinking") withContext := fs.Bool("context", false, "include text the tool added (skills, reminders, peer messages)") all := fs.Bool("all", false, "include turns started by tasks, other sessions and the system") + in := copyFlag(fs) pos, err := parse(fs, args) if err != nil { return err } if len(pos) < 1 || len(pos) > 2 { - return errors.New("usage: ccli export [turn | from-to | last] [--out file] [--format md|jsonl]") + return errors.New("usage: ccli export [-a account] [turn | from-to | last] [--out file] [--format md|jsonl]") } - svc, err := a.prepare(ctx, c, nil) + if pos[0], err = withAccount(pos[0], *in); err != nil { + return err + } + svc, err := a.prepare(ctx, c, accountsOf(*in)) if err != nil { return err } defer a.close() - acct, _, err := svc.Resolve(ctx, pos[0]) + acct, id, err := svc.Resolve(ctx, pos[0]) if err != nil { return err } + a.noteCopy(ctx, svc, pos[0], acct, id) a.refreshCursorFor(ctx, []string{acct}, nil) ref := pos[0] list, err := svc.Turns(ctx, ref) diff --git a/internal/cli/log.go b/internal/cli/log.go index 0a9df13..eae6a46 100644 --- a/internal/cli/log.go +++ b/internal/cli/log.go @@ -41,12 +41,16 @@ func cmdLog(ctx context.Context, a *app, args []string) error { agent := fs.String("agent", "", "print a subagent's transcript (agent id or prefix)") run := fs.String("run", "", "list a workflow run's agents (run id or prefix)") noPager := fs.Bool("no-pager", false, "do not page long output") + in := copyFlag(fs) pos, err := parse(fs, args) if err != nil { return err } if len(pos) < 1 || len(pos) > 2 { - return errors.New("usage: ccli log [turn | from-to | last] [--detail full|reply] [--tools] [--thinking] [--agent id] [--run id]") + return errors.New("usage: ccli log [-a account] [turn | from-to | last] [--detail full|reply] [--tools] [--thinking] [--agent id] [--run id]") + } + if pos[0], err = withAccount(pos[0], *in); err != nil { + return err } switch strings.ToLower(strings.TrimSpace(*detail)) { case "", "full", "reply": @@ -58,12 +62,13 @@ func cmdLog(ctx context.Context, a *app, args []string) error { if a.verbose && !reply { opt.Tools = true } - svc, err := a.prepare(ctx, c, nil) + svc, err := a.prepare(ctx, c, accountsOf(*in)) if err != nil { return err } defer a.close() - if acct, _, err := svc.Resolve(ctx, pos[0]); err == nil { + if acct, id, err := svc.Resolve(ctx, pos[0]); err == nil { + a.noteCopy(ctx, svc, pos[0], acct, id) a.refreshCursorFor(ctx, []string{acct}, nil) } ref := pos[0] diff --git a/internal/cli/transfer_test.go b/internal/cli/transfer_test.go index d3013db..ebe28f7 100644 --- a/internal/cli/transfer_test.go +++ b/internal/cli/transfer_test.go @@ -139,3 +139,31 @@ func TestResumeRejectsBothSyncFlags(t *testing.T) { t.Fatalf("exit %d\n%s", code, errw.String()) } } + +// With the session in two accounts, the read commands say which copy they +// show, and -a (or account:id) picks the other; log marks the turns that +// came in with the copy. +func TestReadCommandsPickACopy(t *testing.T) { + twoAccounts(t) + runCLI(t, "copy", roundTrip, "--to", "work", "--apply") + for _, cmd := range [][]string{{"show", roundTrip}, {"log", roundTrip}, {"export", roundTrip}} { + var out, errw bytes.Buffer + if code := Main(cmd, &out, &errw); code != 0 || !strings.Contains(errw.String(), "is also in") { + t.Fatalf("%v: exit %d, stderr %q", cmd, code, errw.String()) + } + } + work := string(runCLI(t, "log", roundTrip, "-a", "work")) + if !strings.Contains(work, "· work ·") || !strings.Contains(work, "turns 1 to 2 came with a copy from default") || !strings.Contains(work, "copied") { + t.Fatalf("log -a work:\n%s", work) + } + if def := string(runCLI(t, "log", "default:"+roundTrip)); !strings.Contains(def, "· default ·") || strings.Contains(def, "came with a copy") { + t.Fatalf("log default:id:\n%s", def) + } + var out, errw bytes.Buffer + if code := Main([]string{"log", "default:" + roundTrip, "-a", "work"}, &out, &errw); code == 0 || !strings.Contains(errw.String(), "different accounts") { + t.Fatalf("conflicting accounts: exit %d\n%s", code, errw.String()) + } + if code := Main([]string{"show", roundTrip, "-a", "nosuch"}, &out, &errw); code == 0 { + t.Fatal("an unknown account was accepted") + } +} diff --git a/internal/cli/view.go b/internal/cli/view.go index 7d362fc..6cbda0b 100644 --- a/internal/cli/view.go +++ b/internal/cli/view.go @@ -24,12 +24,20 @@ func cmdView(ctx context.Context, a *app, args []string) error { dirFlag(fs, &scope) toolFlag(fs, &scope) tools := fs.Bool("tools", false, "expand tool calls inside each turn") + in := fs.String("a", "", "with a session: its copy in this account (same as :); without one: list only this account") pos, err := parse(fs, args) if err != nil { return err } if len(pos) > 1 { - return errors.New("usage: ccli view [session]") + return errors.New("usage: ccli view [-a account] [session]") + } + if len(pos) == 1 { + if pos[0], err = withAccount(pos[0], *in); err != nil { + return err + } + } else if *in != "" { + scope.Accounts = []string{*in} } if len(pos) == 1 && (len(scope.Dirs) > 0 || len(scope.Tools) > 0) { return errors.New("-dir and -tool narrow the session list; leave them out when opening one session") @@ -37,7 +45,7 @@ func cmdView(ctx context.Context, a *app, args []string) error { if !term.IsTerminal(int(os.Stdin.Fd())) || !term.IsTerminal(int(os.Stdout.Fd())) { return errors.New("ccli view needs a terminal; ccli log prints the same turns") } - svc, err := a.prepare(ctx, c, nil) + svc, err := a.prepare(ctx, c, accountsOf(*in)) if err != nil { return err } @@ -48,6 +56,7 @@ func cmdView(ctx context.Context, a *app, args []string) error { if err != nil { return err } + a.noteCopy(ctx, svc, pos[0], acct, id) ref = acct + ":" + id a.refreshCursorFor(ctx, []string{acct}, nil) } else { diff --git a/internal/cli/views.go b/internal/cli/views.go index 54f537b..c4e5c47 100644 --- a/internal/cli/views.go +++ b/internal/cli/views.go @@ -290,9 +290,8 @@ func cmdSessions(ctx context.Context, a *app, args []string) error { func cmdShow(ctx context.Context, a *app, args []string) error { var c common - var accts accountsFlag fs := newFlags("show", a, &c) - fs.Var(&accts, "a", "account, when the session exists in more than one") + in := copyFlag(fs) all := fs.Bool("all", false, "list every subagent") pos, err := parse(fs, args) if err != nil { @@ -301,22 +300,20 @@ func cmdShow(ctx context.Context, a *app, args []string) error { if len(pos) != 1 { return errors.New("usage: ccli show [-a account] ") } - if len(accts) > 1 { - return errors.New("show takes one account") - } - ref := pos[0] - if len(accts) == 1 && !strings.Contains(ref, ":") { - ref = accts[0] + ":" + ref + ref, err := withAccount(pos[0], *in) + if err != nil { + return err } - svc, err := a.prepare(ctx, c, accts) + svc, err := a.prepare(ctx, c, accountsOf(*in)) if err != nil { return err } defer a.close() - acct, _, err := svc.Resolve(ctx, ref) + acct, id, err := svc.Resolve(ctx, ref) if err != nil { return err } + a.noteCopy(ctx, svc, ref, acct, id) a.refreshCursorFor(ctx, []string{acct}, nil) d, err := svc.Session(ctx, ref) if err != nil { @@ -469,6 +466,63 @@ func withoutVar(env []string, name string) []string { return out } +// copyFlag adds -a to a command that reads one session: its copy in that +// account, when the session is in more than one. It means the same as +// writing the reference as :. +func copyFlag(fs *flag.FlagSet) *string { + return fs.String("a", "", "the session's copy in this account, when it is in more than one (same as :)") +} + +// withAccount writes an -a account into a session reference. It refuses a +// reference that already names another account. +func withAccount(ref, acct string) (string, error) { + if acct == "" { + return ref, nil + } + if i := strings.IndexByte(ref, ':'); i > 0 { + if ref[:i] != acct { + return "", fmt.Errorf("-a %s and %s name different accounts; give one", acct, ref) + } + return ref, nil + } + return acct + ":" + ref, nil +} + +// accountsOf is the account list prepare checks: the -a account, if any. +func accountsOf(acct string) []string { + if acct == "" { + return nil + } + return []string{acct} +} + +// noteCopy says on stderr which copy a reference that names no account +// resolved to, when the session is in more than one account. +func (a *app) noteCopy(ctx context.Context, svc *query.DB, ref, acct, id string) { + if strings.Contains(ref, ":") { + return + } + all, err := svc.CopiesOf(ctx, id) + if err != nil { + return + } + var others []string + for _, o := range all { + if o != acct { + others = append(others, o) + } + } + if len(others) == 0 { + return + } + pick := others[0] + if len(others) > 1 { + pick = "" + } + fmt.Fprintf(a.errw, "ccli: %s is also in %s; this is the copy in %s, the one used last (-a %s picks another)\n", + query.ShortID(id), strings.Join(others, ", "), acct, pick) +} + // flagSet reports whether the command line gave the flag. func flagSet(fs *flag.FlagSet, name string) bool { set := false diff --git a/internal/query/detail.go b/internal/query/detail.go index f011295..6bcc3d1 100644 --- a/internal/query/detail.go +++ b/internal/query/detail.go @@ -116,6 +116,24 @@ type Resume struct { // ErrNotFound is returned when no session matches a reference. var ErrNotFound = errors.New("no session matches") +// CopiesOf lists the accounts that hold a session, by name. +func (d *DB) CopiesOf(ctx context.Context, id string) ([]string, error) { + rows, err := d.sql().QueryContext(ctx, `SELECT DISTINCT account FROM files WHERE session_id = ? AND kind = 'main' ORDER BY account`, id) + if err != nil { + return nil, err + } + defer rows.Close() + var out []string + for rows.Next() { + var a string + if err := rows.Scan(&a); err != nil { + return nil, err + } + out = append(out, a) + } + return out, rows.Err() +} + // Resolve finds the session for a reference: a full id or a unique prefix, // optionally "account:" in front. func (d *DB) Resolve(ctx context.Context, ref string) (account, id string, err error) { diff --git a/internal/query/turns.go b/internal/query/turns.go index 237c041..4aab160 100644 --- a/internal/query/turns.go +++ b/internal/query/turns.go @@ -38,6 +38,10 @@ type TurnRow struct { AgentCost float64 `json:"agent_cost"` // subagents and workflow runs it started Offset int64 `json:"-"` Next int64 `json:"-"` // where the next turn starts; 0 = end of file + // CopiedFrom names the account a turn came in from with a copy, or for + // a fork the session it was forked from as account:id. Its usage + // counts there. + CopiedFrom string `json:"copied_from,omitempty"` } // Total is the turn's cost with what its agents spent. @@ -324,10 +328,39 @@ func (d *DB) Turns(ctx context.Context, ref string) (*TurnList, error) { if err := d.sql().QueryRowContext(ctx, `SELECT f.offset FROM files f WHERE f.id = ?`, g.main.ID).Scan(&l.Read); err != nil { return nil, err } + if err := d.markCopied(ctx, l); err != nil { + return nil, err + } } return l, nil } +// markCopied sets CopiedFrom on the turns whose prompt lies in a byte range +// ccli copied into the transcript. +func (d *DB) markCopied(ctx context.Context, l *TurnList) error { + rows, err := d.sql().QueryContext(ctx, `SELECT mode, session_id, from_account, from_offset, to_offset FROM transfers WHERE path = ?`, l.Path) + if err != nil { + return err + } + defer rows.Close() + for rows.Next() { + var mode, sid, from string + var lo, hi int64 + if err := rows.Scan(&mode, &sid, &from, &lo, &hi); err != nil { + return err + } + if mode == "fork" { + from += ":" + sid + } + for i := range l.Turns { + if t := &l.Turns[i]; t.Offset >= lo && t.Offset < hi { + t.CopiedFrom = from + } + } + } + return rows.Err() +} + // ErrNoTranscript is returned when a turn's transcript is gone from disk. var ErrNoTranscript = errors.New("the transcript is gone from disk (ccli kept only the totals and prompts)") diff --git a/internal/render/log.go b/internal/render/log.go index aaceb33..654d68f 100644 --- a/internal/render/log.go +++ b/internal/render/log.go @@ -88,6 +88,8 @@ func (t Term) Turns(l *query.TurnList, all bool) { } else { cost = t.money(cost) } + } else if tr.CopiedFrom != "" { + cost = t.dim("copied") // counted where it came from; see the note below the table } agents := "" if tr.Agents > 0 { @@ -127,6 +129,9 @@ func (t Term) Turns(l *query.TurnList, all bool) { if hidden > 0 { t.println(t.dim(fmt.Sprintf("%d turns started by tasks, other sessions or the system are hidden (--all shows them)", hidden))) } + for _, n := range copiedNotes(l) { + t.println(t.dim(n)) + } t.println(t.dim(fmt.Sprintf("ccli log %s prints a turn in full · ccli view %s browses them", id, id))) if s.Tool == account.Claude { t.println(t.dim(fmt.Sprintf("ccli fork %s --turn -a continues the conversation from a turn as a new session", id))) @@ -604,3 +609,36 @@ func shortPaths(s string) string { } return s } + +// copiedNotes explains the turns that came in with a copy or a fork, one +// line for each run of turns from the same place. +func copiedNotes(l *query.TurnList) []string { + var turns []query.TurnRow + for _, tr := range l.Turns { + if tr.Seq > 0 { + turns = append(turns, tr) + } + } + var out []string + for i := 0; i < len(turns); { + from, j := turns[i].CopiedFrom, i + for j+1 < len(turns) && turns[j+1].CopiedFrom == from { + j++ + } + if from != "" { + span := fmt.Sprintf("turns %d to %d", turns[i].Seq, turns[j].Seq) + if i == j { + span = fmt.Sprintf("turn %d", turns[i].Seq) + } + if acct, sid, fork := strings.Cut(from, ":"); fork { + out = append(out, fmt.Sprintf("%s came from %s in %s when this session was forked from it; their usage counts there (ccli log %s -a %s)", + span, query.ShortID(sid), acct, query.ShortID(sid), acct)) + } else { + out = append(out, fmt.Sprintf("%s came with a copy from %s; their usage counts in %s (ccli log %s -a %s)", + span, acct, acct, query.ShortID(l.Session.ID), acct)) + } + } + i = j + 1 + } + return out +} diff --git a/internal/render/log_test.go b/internal/render/log_test.go index 75e6b0e..0af5f8d 100644 --- a/internal/render/log_test.go +++ b/internal/render/log_test.go @@ -6,6 +6,7 @@ import ( "time" "github.com/lr00rl/ccli/internal/conv" + "github.com/lr00rl/ccli/internal/query" ) func TestConversationReplyDropsSteps(t *testing.T) { @@ -38,3 +39,23 @@ func TestConversationDropsEscapes(t *testing.T) { t.Fatalf("escapes reached the output: %q", got) } } + +// A fork's copied turns point at the session they came from; a copy's at +// the same session in the account it came from. +func TestCopiedNotes(t *testing.T) { + l := &query.TurnList{Session: query.SessionRow{ID: "bbbbbbbb-0000-4000-8000-000000000001"}, Turns: []query.TurnRow{ + {Seq: 0, CopiedFrom: "work:aaaaaaaa-0000-4000-8000-000000000001"}, + {Seq: 1, CopiedFrom: "work:aaaaaaaa-0000-4000-8000-000000000001"}, + {Seq: 2, CopiedFrom: "work:aaaaaaaa-0000-4000-8000-000000000001"}, + {Seq: 3}, + {Seq: 4, CopiedFrom: "default"}, + }} + got := copiedNotes(l) + want := []string{ + "turns 1 to 2 came from aaaaaaaa in work when this session was forked from it; their usage counts there (ccli log aaaaaaaa -a work)", + "turn 4 came with a copy from default; their usage counts in default (ccli log bbbbbbbb -a default)", + } + if strings.Join(got, "\n") != strings.Join(want, "\n") { + t.Fatalf("notes:\n%s\nwant:\n%s", strings.Join(got, "\n"), strings.Join(want, "\n")) + } +} diff --git a/internal/render/views.go b/internal/render/views.go index d39da84..d8dfb0d 100644 --- a/internal/render/views.go +++ b/internal/render/views.go @@ -392,6 +392,7 @@ func (t Term) Session(d *query.SessionDetail, all bool) { } t.println(line + t.dim(fmt.Sprintf(" %s in %s", humanSize(m.Bytes), plural(m.Files, "file")))) } + t.println(t.dim(" what a copy brought in counts toward the account it came from; this copy counts only the work done in it")) } if len(d.Skills) > 0 || len(d.Tools) > 0 || len(d.Commands) > 0 { diff --git a/internal/tui/app.go b/internal/tui/app.go index 31497d4..cdb3fbb 100644 --- a/internal/tui/app.go +++ b/internal/tui/app.go @@ -77,7 +77,7 @@ func newScreen(title string, r role) *screen { type viewer struct { ctx context.Context src Source - scope query.Filter // Dirs and Tools narrow the session list + scope query.Filter // Accounts, Dirs and Tools narrow the session list loc *time.Location stack []*screen w, h int @@ -117,7 +117,7 @@ type Options struct { } // Run starts the browser at the sessions list, or at a session's turns when -// ref names one. scope's Dirs and Tools narrow the sessions list. +// ref names one. scope's Accounts, Dirs and Tools narrow the sessions list. func Run(ctx context.Context, src Source, ref string, scope query.Filter, opt Options) error { v := &viewer{ctx: ctx, src: src, loc: time.Local, rendered: map[string]string{}, scope: scope, opt: render.LogOptions{Tools: opt.Tools}} var first *screen diff --git a/internal/tui/screens.go b/internal/tui/screens.go index 88b37a6..9e06e3e 100644 --- a/internal/tui/screens.go +++ b/internal/tui/screens.go @@ -28,9 +28,12 @@ func (v *viewer) sessionsScreen() (*screen, error) { } // scopeText names what the session list is narrowed to, for its title: -// " in ~/code/app" and " (codex)", or nothing. +// " in talcum", " in ~/code/app" and " (codex)", or nothing. func (v *viewer) scopeText() string { s := "" + if len(v.scope.Accounts) > 0 { + s = " in " + strings.Join(v.scope.Accounts, ", ") + } if len(v.scope.Dirs) > 0 { s = " in " + render.ShortHome(v.scope.Dirs[0]) } @@ -46,7 +49,7 @@ func (v *viewer) scopeText() string { // sessionFilter selects the hidden sessions, or the others. func (v *viewer) sessionFilter(hidden bool) query.Filter { - f := query.Filter{Loc: v.loc, Sort: "last", Dirs: v.scope.Dirs, Tools: v.scope.Tools} + f := query.Filter{Loc: v.loc, Sort: "last", Dirs: v.scope.Dirs, Tools: v.scope.Tools, Accounts: v.scope.Accounts} if hidden { f.Show = query.HiddenOnly }