feat(web): hide ephemeral media refs from persisted session history

This commit is contained in:
afjcjsbx 2026-04-21 19:19:39 +02:00
parent ba5ef787b4
commit 19c35317ad
2 changed files with 90 additions and 39 deletions

View file

@ -5,7 +5,6 @@ import (
"encoding/json"
"errors"
"net/http"
"net/url"
"os"
"path/filepath"
"sort"
@ -407,10 +406,12 @@ func (h *Handler) findLegacyPicoSession(dir, sessionID string) (picoLegacySessio
}
func buildSessionListItem(sessionID string, sess sessionFile, toolFeedbackMaxArgsLength int) sessionListItem {
transcript := visibleSessionMessages(sess.Messages, toolFeedbackMaxArgsLength)
preview := ""
for _, msg := range sess.Messages {
for _, msg := range transcript {
if msg.Role == "user" {
preview = sessionMessagePreview(msg)
preview = sessionChatMessagePreview(msg)
}
if preview != "" {
break
@ -423,13 +424,11 @@ func buildSessionListItem(sessionID string, sess sessionFile, toolFeedbackMaxArg
}
title := preview
validMessageCount := len(visibleSessionMessages(sess.Messages, toolFeedbackMaxArgsLength))
return sessionListItem{
ID: sessionID,
Title: title,
Preview: preview,
MessageCount: validMessageCount,
MessageCount: len(transcript),
Created: sess.Created.Format(time.RFC3339),
Updated: sess.Updated.Format(time.RFC3339),
}
@ -450,11 +449,11 @@ func truncateRunes(s string, maxLen int) string {
return string(runes[:maxLen]) + "..."
}
func sessionMessageVisible(msg providers.Message) bool {
func sessionChatMessageVisible(msg sessionChatMessage) bool {
return strings.TrimSpace(msg.Content) != "" || len(msg.Media) > 0 || len(msg.Attachments) > 0
}
func sessionMessagePreview(msg providers.Message) string {
func sessionChatMessagePreview(msg sessionChatMessage) string {
if content := strings.TrimSpace(msg.Content); content != "" {
return content
}
@ -484,13 +483,14 @@ func visibleSessionMessages(messages []providers.Message, toolFeedbackMaxArgsLen
continue
case "user":
if sessionMessageVisible(msg) {
transcript = append(transcript, sessionChatMessage{
Role: "user",
Content: msg.Content,
Media: append([]string(nil), msg.Media...),
Attachments: attachments,
})
chatMsg := sessionChatMessage{
Role: "user",
Content: msg.Content,
Media: append([]string(nil), msg.Media...),
Attachments: attachments,
}
if sessionChatMessageVisible(chatMsg) {
transcript = append(transcript, chatMsg)
}
case "assistant":
@ -513,10 +513,6 @@ func visibleSessionMessages(messages []providers.Message, toolFeedbackMaxArgsLen
// Pico web chat can persist both visible `message` tool output and a
// later plain assistant reply in the same turn. Hide only the fixed
// internal summary that marks handled tool delivery.
if !sessionMessageVisible(msg) {
continue
}
content := msg.Content
if assistantMessageInternalOnly(msg) {
if len(attachments) == 0 {
@ -525,12 +521,17 @@ func visibleSessionMessages(messages []providers.Message, toolFeedbackMaxArgsLen
content = ""
}
transcript = append(transcript, sessionChatMessage{
chatMsg := sessionChatMessage{
Role: "assistant",
Content: content,
Media: append([]string(nil), msg.Media...),
Attachments: attachments,
})
}
if !sessionChatMessageVisible(chatMsg) {
continue
}
transcript = append(transcript, chatMsg)
}
}
@ -587,11 +588,10 @@ func sessionAttachmentURL(attachment providers.Attachment) (string, bool) {
return "", false
}
if strings.HasPrefix(ref, "media://") {
refID := strings.TrimSpace(strings.TrimPrefix(ref, "media://"))
if refID == "" {
return "", false
}
return "/pico/media/" + url.PathEscape(refID), true
// Persisted session history must only expose durable attachment locations.
// media:// refs depend on the live in-memory MediaStore and may stop
// resolving after a restart or cleanup, so omit them from reopened history.
return "", false
}
return ref, true
}

View file

@ -218,7 +218,7 @@ func TestHandleGetSession_JSONLStorage(t *testing.T) {
}
}
func TestHandleGetSession_ExposesHandledToolAttachments(t *testing.T) {
func TestHandleGetSession_HidesHandledToolAttachmentsBackedByMediaRefs(t *testing.T) {
configPath, cleanup := setupOAuthTestEnv(t)
defer cleanup()
@ -260,16 +260,63 @@ func TestHandleGetSession_ExposesHandledToolAttachments(t *testing.T) {
}
var resp struct {
Messages []struct {
Role string `json:"role"`
Content string `json:"content"`
Attachments []struct {
Type string `json:"type"`
URL string `json:"url"`
Filename string `json:"filename"`
ContentType string `json:"content_type"`
} `json:"attachments"`
} `json:"messages"`
Messages []sessionChatMessage `json:"messages"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("Unmarshal() error = %v", err)
}
if len(resp.Messages) != 1 {
t.Fatalf("len(resp.Messages) = %d, want 1", len(resp.Messages))
}
if resp.Messages[0].Role != "user" || resp.Messages[0].Content != "send me the report" {
t.Fatalf("message = %#v, want only user request", resp.Messages[0])
}
}
func TestHandleGetSession_ExposesHandledToolAttachmentsWithDurableURL(t *testing.T) {
configPath, cleanup := setupOAuthTestEnv(t)
defer cleanup()
dir := sessionsTestDir(t, configPath)
store, err := memory.NewJSONLStore(dir)
if err != nil {
t.Fatalf("NewJSONLStore() error = %v", err)
}
sessionKey := legacyPicoSessionPrefix + "attachment-history-durable"
for _, msg := range []providers.Message{
{Role: "user", Content: "send me the report"},
{
Role: "assistant",
Content: handledToolResponseSummaryText,
Attachments: []providers.Attachment{{
Type: "file",
URL: "https://example.com/report.txt",
Filename: "report.txt",
ContentType: "text/plain",
}},
},
} {
if err := store.AddFullMessage(nil, sessionKey, msg); err != nil {
t.Fatalf("AddFullMessage() error = %v", err)
}
}
h := NewHandler(configPath)
mux := http.NewServeMux()
h.RegisterRoutes(mux)
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/sessions/attachment-history-durable", nil)
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String())
}
var resp struct {
Messages []sessionChatMessage `json:"messages"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("Unmarshal() error = %v", err)
@ -289,8 +336,12 @@ func TestHandleGetSession_ExposesHandledToolAttachments(t *testing.T) {
if len(assistant.Attachments) != 1 {
t.Fatalf("len(assistant.Attachments) = %d, want 1", len(assistant.Attachments))
}
if assistant.Attachments[0].URL != "/pico/media/attachment-1" {
t.Fatalf("attachment url = %q, want %q", assistant.Attachments[0].URL, "/pico/media/attachment-1")
if assistant.Attachments[0].URL != "https://example.com/report.txt" {
t.Fatalf(
"attachment url = %q, want %q",
assistant.Attachments[0].URL,
"https://example.com/report.txt",
)
}
if assistant.Attachments[0].Filename != "report.txt" {
t.Fatalf("attachment filename = %q, want %q", assistant.Attachments[0].Filename, "report.txt")