improve(session): redesign /session list display for clarity

- Show summary (truncated to 40 chars) as primary identifier
- Move #id to trailing parentheses to reduce visual noise
- Use relative time (2h ago) instead of absolute timestamps
- Add Summary field to SessionMeta, populated from Session.Summary
- List row number = resume parameter, zero ambiguity

Before: 1. [*] agent:main:telegram:direct:300900269#10 | updated: 2026-03-01 23:22 | messages: 8
After:  1. [*] Discussing React performance...  | 8 msgs | 2h ago (#10)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
mingmxren 2026-03-04 01:54:32 +08:00
parent be2b9507dd
commit b323131c27
3 changed files with 62 additions and 9 deletions

View file

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/sipeed/picoclaw/pkg/session" "github.com/sipeed/picoclaw/pkg/session"
) )
@ -72,20 +73,65 @@ func sessionResumeHandler() Handler {
func formatSessionList(list []session.SessionMeta) string { func formatSessionList(list []session.SessionMeta) string {
lines := make([]string, 0, len(list)+1) lines := make([]string, 0, len(list)+1)
lines = append(lines, "Sessions for current chat:") lines = append(lines, "Sessions:")
now := time.Now()
for _, item := range list { for _, item := range list {
activeMarker := " " activeMarker := " "
if item.Active { if item.Active {
activeMarker = "*" activeMarker = "[*]"
} }
updated := "-"
summary := truncateSummary(item.Summary, 40)
age := "-"
if !item.UpdatedAt.IsZero() { if !item.UpdatedAt.IsZero() {
updated = item.UpdatedAt.Format("2006-01-02 15:04") age = relativeTime(now, item.UpdatedAt)
} }
tag := extractSessionTag(item.SessionKey)
lines = append(lines, fmt.Sprintf( lines = append(lines, fmt.Sprintf(
"%d. [%s] %s | updated: %s | messages: %d", "%d. %s %s | %d msgs | %s (%s)",
item.Ordinal, activeMarker, item.SessionKey, updated, item.MessageCnt, item.Ordinal, activeMarker, summary, item.MessageCnt, age, tag,
)) ))
} }
return strings.Join(lines, "\n") return strings.Join(lines, "\n")
} }
func truncateSummary(s string, maxLen int) string {
s = strings.TrimSpace(s)
if s == "" {
return "(no summary)"
}
s = strings.ReplaceAll(s, "\n", " ")
if len(s) <= maxLen {
return s
}
return s[:maxLen] + "..."
}
// extractSessionTag returns the "#N" suffix from a session key, or "#1" for
// the initial session that has no "#" separator.
func extractSessionTag(sessionKey string) string {
if i := strings.LastIndex(sessionKey, "#"); i >= 0 {
return "#" + sessionKey[i+1:]
}
return "#1"
}
func relativeTime(now, t time.Time) string {
d := now.Sub(t)
switch {
case d < time.Minute:
return "just now"
case d < time.Hour:
m := int(d.Minutes())
return fmt.Sprintf("%dm ago", m)
case d < 24*time.Hour:
h := int(d.Hours())
return fmt.Sprintf("%dh ago", h)
default:
days := int(d.Hours() / 24)
return fmt.Sprintf("%dd ago", days)
}
}

View file

@ -3,6 +3,7 @@ package commands
import ( import (
"context" "context"
"errors" "errors"
"strings"
"testing" "testing"
"time" "time"
@ -119,6 +120,7 @@ func TestSessionHandlers_SessionList(t *testing.T) {
UpdatedAt: time.Date(2026, 3, 1, 9, 7, 0, 0, time.UTC), UpdatedAt: time.Date(2026, 3, 1, 9, 7, 0, 0, time.UTC),
MessageCnt: 4, MessageCnt: 4,
Active: true, Active: true,
Summary: "Discussing React performance",
}, },
}, },
} }
@ -135,8 +137,11 @@ func TestSessionHandlers_SessionList(t *testing.T) {
if res.Outcome != OutcomeHandled { if res.Outcome != OutcomeHandled {
t.Fatalf("outcome=%v, want=%v", res.Outcome, OutcomeHandled) t.Fatalf("outcome=%v, want=%v", res.Outcome, OutcomeHandled)
} }
if reply != "Sessions for current chat:\n1. [*] scope#3 | updated: 2026-03-01 09:07 | messages: 4" { // Check key elements: header, active marker, summary, message count, session tag
t.Fatalf("reply=%q", reply) for _, want := range []string{"Sessions:", "[*]", "Discussing React performance", "4 msgs", "(#3)"} {
if !strings.Contains(reply, want) {
t.Fatalf("reply missing %q, got %q", want, reply)
}
} }
} }

View file

@ -51,6 +51,7 @@ type SessionMeta struct {
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
MessageCnt int `json:"message_cnt"` MessageCnt int `json:"message_cnt"`
Active bool `json:"active"` Active bool `json:"active"`
Summary string `json:"summary,omitempty"`
} }
type SessionManager struct { type SessionManager struct {
@ -740,6 +741,7 @@ func (sm *SessionManager) buildSessionMetaListLocked(scope *scopeIndex) []Sessio
if session, ok := sm.sessions[key]; ok { if session, ok := sm.sessions[key]; ok {
meta.UpdatedAt = session.Updated meta.UpdatedAt = session.Updated
meta.MessageCnt = len(session.Messages) meta.MessageCnt = len(session.Messages)
meta.Summary = session.Summary
} }
list = append(list, meta) list = append(list, meta)
} }