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"
"strconv"
"strings"
"time"
"github.com/sipeed/picoclaw/pkg/session"
)
@ -72,20 +73,65 @@ func sessionResumeHandler() Handler {
func formatSessionList(list []session.SessionMeta) string {
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 {
activeMarker := " "
activeMarker := " "
if item.Active {
activeMarker = "*"
activeMarker = "[*]"
}
updated := "-"
summary := truncateSummary(item.Summary, 40)
age := "-"
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(
"%d. [%s] %s | updated: %s | messages: %d",
item.Ordinal, activeMarker, item.SessionKey, updated, item.MessageCnt,
"%d. %s %s | %d msgs | %s (%s)",
item.Ordinal, activeMarker, summary, item.MessageCnt, age, tag,
))
}
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 (
"context"
"errors"
"strings"
"testing"
"time"
@ -119,6 +120,7 @@ func TestSessionHandlers_SessionList(t *testing.T) {
UpdatedAt: time.Date(2026, 3, 1, 9, 7, 0, 0, time.UTC),
MessageCnt: 4,
Active: true,
Summary: "Discussing React performance",
},
},
}
@ -135,8 +137,11 @@ func TestSessionHandlers_SessionList(t *testing.T) {
if 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" {
t.Fatalf("reply=%q", reply)
// Check key elements: header, active marker, summary, message count, session tag
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"`
MessageCnt int `json:"message_cnt"`
Active bool `json:"active"`
Summary string `json:"summary,omitempty"`
}
type SessionManager struct {
@ -740,6 +741,7 @@ func (sm *SessionManager) buildSessionMetaListLocked(scope *scopeIndex) []Sessio
if session, ok := sm.sessions[key]; ok {
meta.UpdatedAt = session.Updated
meta.MessageCnt = len(session.Messages)
meta.Summary = session.Summary
}
list = append(list, meta)
}