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:
parent
be2b9507dd
commit
b323131c27
3 changed files with 62 additions and 9 deletions
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue