diff --git a/cmd/Ollie/main.go b/cmd/Ollie/main.go index 0135975..77a16c6 100644 --- a/cmd/Ollie/main.go +++ b/cmd/Ollie/main.go @@ -113,9 +113,14 @@ func agentPath(sid string) string { // --- session list window --- +type sessionEntry struct { + id string // full UUID + name string // display name +} + type sessionList struct { win *acme.Win - allSIDs []string + entries []sessionEntry mu sync.Mutex } @@ -134,42 +139,43 @@ func newSessionList() error { } func (sl *sessionList) refresh() { - data, err := read9p("session/idx") + fsys := dial9p() + fid, err := fsys.Open("session", plan9.OREAD) if err != nil { - sl.win.Errf("read idx: %v", err) + sl.win.Errf("list sessions: %v", err) return } + dirs, err := fid.Dirreadall() + fid.Close() + if err != nil { + sl.win.Errf("list sessions: %v", err) + return + } + sl.win.Addr(",") sl.win.Write("data", nil) - raw := strings.TrimSpace(string(data)) - if raw == "" { - sl.win.Ctl("clean") - return - } - - lines := strings.Split(raw, "\n") - sort.Strings(lines) - sl.mu.Lock() - sl.allSIDs = nil + sl.entries = nil var display strings.Builder - for _, l := range lines { - if s := strings.TrimSpace(l); s != "" { - id, rest, _ := strings.Cut(s, "\t") - sl.allSIDs = append(sl.allSIDs, id) - short := id - if idx := strings.IndexByte(id, '-'); idx > 0 { - short = id[:idx] - } - display.WriteString(short) - if rest != "" { - display.WriteByte('\t') - display.WriteString(rest) - } - display.WriteByte('\n') + for _, d := range dirs { + if d.Mode&plan9.DMDIR == 0 { + continue } + id := d.Name + name := id + if d, err := read9p("session/" + id + "/name"); err == nil { + if n := strings.TrimSpace(string(d)); n != "" { + name = n + } + } + sl.entries = append(sl.entries, sessionEntry{id: id, name: name}) + display.WriteString(name) + display.WriteByte('\n') } + sort.Slice(sl.entries, func(i, j int) bool { + return sl.entries[i].name < sl.entries[j].name + }) sl.mu.Unlock() sl.win.Write("body", []byte(display.String())) @@ -183,13 +189,9 @@ func (sl *sessionList) resolveFullSID(text string) string { } sl.mu.Lock() defer sl.mu.Unlock() - for _, sid := range sl.allSIDs { - short := sid - if idx := strings.IndexByte(sid, '-'); idx > 0 { - short = sid[:idx] - } - if short == text || sid == text { - return sid + for _, e := range sl.entries { + if e.name == text || e.id == text { + return e.id } } return "" @@ -252,18 +254,11 @@ func openChat(sid string) { log.Printf("open chat %s: %v", sid, err) return } - short := sid - if idx := strings.IndexByte(sid, '-'); idx > 0 { - short = sid[:idx] - } - winName := "ollie/sessions/" + short - if data, err := read9p("session/idx"); err == nil { - for _, line := range strings.Split(string(data), "\n") { - fields := strings.Split(line, "\t") - if len(fields) >= 3 && fields[0] == sid { - winName = fields[2] + "/+Ollie" - break - } + winName := "ollie/sessions" + if data, err := read9p("session/" + sid + "/name"); err == nil { + name := strings.TrimSpace(string(data)) + if name != "" { + winName = name + "/+Ollie" } } w.Name("%s", winName) diff --git a/fs/session/create.go b/fs/session/create.go index 6ddf9a0..b353c67 100644 --- a/fs/session/create.go +++ b/fs/session/create.go @@ -70,9 +70,12 @@ func Create(rs *rootState, args []string) (string, error) { } } - sessID := name - if sessID == "" { - sessID = coresession.NewSessionID() + sessID := coresession.NewSessionID() + if name == "" { + name = sessID + if idx := strings.IndexByte(sessID, '-'); idx > 0 { + name = sessID[:idx] + } } rs.mu.RLock() @@ -246,6 +249,7 @@ func Create(rs *rootState, args []string) (string, error) { sessionCtx, sessionCancel := context.WithCancel(rs.cfg.Ctx) sess := NewSession(sessID, core, sessionCtx, sessionCancel) + sess.SetName(name) sessPtr = sess sess.AgentLog = al sess.proc = proc diff --git a/fs/session/files.go b/fs/session/files.go index 8e156ff..c7d897b 100644 --- a/fs/session/files.go +++ b/fs/session/files.go @@ -113,6 +113,18 @@ func (h *sessionHelper) fileSpec(name string, mode os.FileMode) FileSpec { fs := FileSpec{Name: name, Mode: mode} switch name { + case "id": + fs.Read = func() ([]byte, error) { return []byte(h.sess.id), nil } + case "name": + fs.Read = func() ([]byte, error) { return []byte(h.sess.Name()), nil } + fs.Write = func(data []byte) error { + newName := strings.TrimSpace(string(data)) + if newName == "" { + return nil + } + h.sess.SetName(newName) + return nil + } case "env": fs.Read = func() ([]byte, error) { return []byte(h.envContent()), nil } case "ctl": diff --git a/fs/session/perm.go b/fs/session/perm.go index c64c8a1..691be06 100644 --- a/fs/session/perm.go +++ b/fs/session/perm.go @@ -90,6 +90,8 @@ var Perms = map[string]Perm{ "toolsrv.loaded": 0444, "toolsrv.rev": 0444, "tools": 0666, + "name": 0666, + "id": 0444, }, }, } diff --git a/fs/session/persist.go b/fs/session/persist.go index be8a33c..e675a33 100644 --- a/fs/session/persist.go +++ b/fs/session/persist.go @@ -39,33 +39,19 @@ func KillFromRoot(root *Tree, id string) { Kill(state(root), id) } -// Rename renames a session. +// Rename renames a session by setting its Name. The immutable ID stays unchanged. func Rename(rs *rootState, old, new string) error { - rs.mu.Lock() + rs.mu.RLock() sess, ok := rs.sessions[old] + rs.mu.RUnlock() if !ok { - rs.mu.Unlock() return fmt.Errorf("session not found: %s", old) } - if _, exists := rs.sessions[new]; exists { - rs.mu.Unlock() - return fmt.Errorf("session already exists: %s", new) - } if sess.Core.Agent().IsRunning() { - rs.mu.Unlock() return fmt.Errorf("cannot rename while agent is running") } - if err := sess.Core.SetSessionID(new); err != nil { - rs.mu.Unlock() - return err - } - - sess.id = new - rs.sessions[new] = sess - delete(rs.sessions, old) - rs.mu.Unlock() - + sess.SetName(new) rs.cfg.Log.Info("renamed session %s -> %s", old, new) broadcastEvents() return nil diff --git a/fs/session/root.go b/fs/session/root.go index efaac96..4d4621d 100644 --- a/fs/session/root.go +++ b/fs/session/root.go @@ -486,17 +486,18 @@ func buildIndex(rs *rootState) []byte { ids = append(ids, id) } sort.Strings(ids) - for _, id := range ids { - sess := rs.sessions[id] - sess.mu.RLock() - st := sess.Core.Agent().State() - cwd := sess.Core.CWD() - be := sess.Core.Agent().BackendName() - model := sess.Core.Agent().ModelName() - ag := sess.Core.Agent().Name() - sess.mu.RUnlock() - fmt.Fprintf(&sb, "%s\t%s\t%s\t%s\t%s\t%s\n", id, st, cwd, be, model, ag) - } +for _, id := range ids { + sess := rs.sessions[id] + sess.mu.RLock() + st := sess.Core.Agent().State() + cwd := sess.Core.CWD() + be := sess.Core.Agent().BackendName() + model := sess.Core.Agent().ModelName() + ag := sess.Core.Agent().Name() + name := sess.name + sess.mu.RUnlock() + fmt.Fprintf(&sb, "%s\t%s\t%s\t%s\t%s\t%s\n", name, st, cwd, be, model, ag) + } rs.mu.RUnlock() return []byte(sb.String()) } diff --git a/fs/session/types.go b/fs/session/types.go index ab259af..9c7c816 100644 --- a/fs/session/types.go +++ b/fs/session/types.go @@ -15,7 +15,8 @@ import ( // Agent-scoped state (log, plan, etc.) lives in AgentLog. type Session struct { mu sync.RWMutex - id string + id string // immutable UUID, set at creation, never changes + name string // mutable friendly name, defaults to id uname string // immutable user principal (numeric UID), set at creation Core *session.Session SessionCtx context.Context @@ -53,7 +54,11 @@ func NewAgentLog(remote string) *AgentLog { } func NewSession(id string, core *session.Session, ctx context.Context, cancel context.CancelFunc) *Session { - sess := &Session{id: id, Core: core, SessionCtx: ctx, cancel: cancel} + name := id + if idx := strings.IndexByte(id, '-'); idx > 0 { + name = id[:idx] + } + sess := &Session{id: id, name: name, Core: core, SessionCtx: ctx, cancel: cancel} return sess } @@ -92,6 +97,16 @@ func (sess *Session) InvalidateModelsCache() { func (sess *Session) RunnableID() string { return sess.id } func (sess *Session) Uname() string { return sess.uname } +func (sess *Session) Name() string { + sess.mu.RLock() + defer sess.mu.RUnlock() + return sess.name +} +func (sess *Session) SetName(name string) { + sess.mu.Lock() + defer sess.mu.Unlock() + sess.name = name +} func (sess *Session) Cancel() { sess.cancel()