Merge edde942b97 into 215d98aa78
This commit is contained in:
commit
2185a7709e
13 changed files with 233 additions and 20 deletions
|
|
@ -440,6 +440,8 @@ func TestProcessMessage_BtwCommandRunsWithoutPersistingHistory(t *testing.T) {
|
||||||
defaultAgent.Sessions.SetHistory(sessionKey, initialHistory)
|
defaultAgent.Sessions.SetHistory(sessionKey, initialHistory)
|
||||||
defaultAgent.Sessions.SetSummary(sessionKey, "The team decided to keep state request-scoped.")
|
defaultAgent.Sessions.SetSummary(sessionKey, "The team decided to keep state request-scoped.")
|
||||||
|
|
||||||
|
initialHistory = defaultAgent.Sessions.GetHistory(sessionKey)
|
||||||
|
|
||||||
response, err := al.processMessage(context.Background(), msg)
|
response, err := al.processMessage(context.Background(), msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("processMessage() error = %v", err)
|
t.Fatalf("processMessage() error = %v", err)
|
||||||
|
|
@ -558,6 +560,8 @@ func TestProcessMessage_BtwCommandUsesIsolatedProvider(t *testing.T) {
|
||||||
}
|
}
|
||||||
defaultAgent.Sessions.SetHistory(mainSessionKey, initialHistory)
|
defaultAgent.Sessions.SetHistory(mainSessionKey, initialHistory)
|
||||||
|
|
||||||
|
initialHistory = defaultAgent.Sessions.GetHistory(mainSessionKey)
|
||||||
|
|
||||||
// Process a /btw command
|
// Process a /btw command
|
||||||
response, err := al.processMessage(context.Background(), bus.InboundMessage{
|
response, err := al.processMessage(context.Background(), bus.InboundMessage{
|
||||||
Channel: "telegram",
|
Channel: "telegram",
|
||||||
|
|
|
||||||
|
|
@ -1618,6 +1618,8 @@ func TestAgentLoop_InterruptHard_RestoresSession(t *testing.T) {
|
||||||
}
|
}
|
||||||
defaultAgent.Sessions.SetHistory(sessionKey, originalHistory)
|
defaultAgent.Sessions.SetHistory(sessionKey, originalHistory)
|
||||||
|
|
||||||
|
originalHistory = defaultAgent.Sessions.GetHistory(sessionKey)
|
||||||
|
|
||||||
runtimeCh, closeRuntimeEvents := subscribeRuntimeEventsForTest(
|
runtimeCh, closeRuntimeEvents := subscribeRuntimeEventsForTest(
|
||||||
t,
|
t,
|
||||||
al,
|
al,
|
||||||
|
|
|
||||||
|
|
@ -677,10 +677,25 @@ func (ts *turnState) restoreSession(agent *AgentInstance) error {
|
||||||
return agent.Sessions.Save(ts.sessionKey)
|
return agent.Sessions.Save(ts.sessionKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// messagesContentEqual compares two message slices by content only, ignoring CreatedAt.
|
||||||
|
// JSON roundtrip loses the monotonic clock portion of time.Time, so direct
|
||||||
|
// reflect.DeepEqual would always differ on messages that roundtripped through
|
||||||
|
// the JSONL store.
|
||||||
|
func messagesContentEqual(a, b []providers.Message) bool {
|
||||||
|
for i := range a {
|
||||||
|
aCopy, bCopy := a[i], b[i]
|
||||||
|
aCopy.CreatedAt, bCopy.CreatedAt = nil, nil
|
||||||
|
if !reflect.DeepEqual(aCopy, bCopy) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func matchingTurnMessageTail(history, persisted []providers.Message) int {
|
func matchingTurnMessageTail(history, persisted []providers.Message) int {
|
||||||
maxMatch := min(len(history), len(persisted))
|
maxMatch := min(len(history), len(persisted))
|
||||||
for size := maxMatch; size > 0; size-- {
|
for size := maxMatch; size > 0; size-- {
|
||||||
if reflect.DeepEqual(history[len(history)-size:], persisted[len(persisted)-size:]) {
|
if messagesContentEqual(history[len(history)-size:], persisted[len(persisted)-size:]) {
|
||||||
return size
|
return size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -561,6 +561,12 @@ func (s *JSONLStore) addMsg(sessionKey string, msg providers.Message) error {
|
||||||
l.Lock()
|
l.Lock()
|
||||||
defer l.Unlock()
|
defer l.Unlock()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
if msg.CreatedAt == nil {
|
||||||
|
msg.CreatedAt = &now
|
||||||
|
}
|
||||||
|
|
||||||
// Append the message as a single JSON line.
|
// Append the message as a single JSON line.
|
||||||
line, err := json.Marshal(msg)
|
line, err := json.Marshal(msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -598,7 +604,6 @@ func (s *JSONLStore) addMsg(sessionKey string, msg providers.Message) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
now := time.Now()
|
|
||||||
if meta.Count == 0 && meta.CreatedAt.IsZero() {
|
if meta.Count == 0 && meta.CreatedAt.IsZero() {
|
||||||
meta.CreatedAt = now
|
meta.CreatedAt = now
|
||||||
}
|
}
|
||||||
|
|
@ -726,6 +731,12 @@ func (s *JSONLStore) SetHistory(
|
||||||
meta.Count = len(history)
|
meta.Count = len(history)
|
||||||
meta.UpdatedAt = now
|
meta.UpdatedAt = now
|
||||||
|
|
||||||
|
for i := range history {
|
||||||
|
if history[i].CreatedAt == nil {
|
||||||
|
history[i].CreatedAt = &now
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Write meta BEFORE rewriting the JSONL file. If we crash between
|
// Write meta BEFORE rewriting the JSONL file. If we crash between
|
||||||
// the two writes, meta has Skip=0 and the old file is still intact,
|
// the two writes, meta has Skip=0 and the old file is still intact,
|
||||||
// so GetHistory reads from line 1 — returning "too many" messages
|
// so GetHistory reads from line 1 — returning "too many" messages
|
||||||
|
|
|
||||||
|
|
@ -1032,6 +1032,143 @@ func TestMultipleSessions_Isolation(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStore_SetsCreatedAtWhenNil(t *testing.T) {
|
||||||
|
type writeOp struct {
|
||||||
|
name string
|
||||||
|
fn func(store *JSONLStore, key string) (expectedCount int)
|
||||||
|
}
|
||||||
|
|
||||||
|
ops := []writeOp{
|
||||||
|
{
|
||||||
|
name: "AddMessage",
|
||||||
|
fn: func(store *JSONLStore, key string) int {
|
||||||
|
if err := store.AddMessage(context.Background(), key, "user", "hello"); err != nil {
|
||||||
|
t.Fatalf("AddMessage: %v", err)
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "AddFullMessage",
|
||||||
|
fn: func(store *JSONLStore, key string) int {
|
||||||
|
if err := store.AddFullMessage(context.Background(), key, providers.Message{
|
||||||
|
Role: "user",
|
||||||
|
Content: "hello from full",
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("AddFullMessage: %v", err)
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SetHistory",
|
||||||
|
fn: func(store *JSONLStore, key string) int {
|
||||||
|
if err := store.SetHistory(context.Background(), key, []providers.Message{
|
||||||
|
{Role: "user", Content: "msg1"},
|
||||||
|
{Role: "assistant", Content: "msg2"},
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("SetHistory: %v", err)
|
||||||
|
}
|
||||||
|
return 2
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, op := range ops {
|
||||||
|
t.Run(op.name, func(t *testing.T) {
|
||||||
|
store := newTestStore(t)
|
||||||
|
key := "s1"
|
||||||
|
|
||||||
|
before := time.Now().Add(-time.Second)
|
||||||
|
expectedCount := op.fn(store, key)
|
||||||
|
after := time.Now().Add(time.Second)
|
||||||
|
|
||||||
|
history, err := store.GetHistory(context.Background(), key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetHistory: %v", err)
|
||||||
|
}
|
||||||
|
if len(history) != expectedCount {
|
||||||
|
t.Fatalf("expected %d messages, got %d", expectedCount, len(history))
|
||||||
|
}
|
||||||
|
for i := range history {
|
||||||
|
if history[i].CreatedAt == nil || history[i].CreatedAt.IsZero() {
|
||||||
|
t.Errorf("message %d CreatedAt is zero — not set by %s", i, op.name)
|
||||||
|
}
|
||||||
|
if history[i].CreatedAt.Before(before) || history[i].CreatedAt.After(after) {
|
||||||
|
t.Errorf(
|
||||||
|
"message %d CreatedAt %v outside expected window [%v, %v]",
|
||||||
|
i, history[i].CreatedAt, before, after,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStore_PreservesExistingCreatedAt(t *testing.T) {
|
||||||
|
t1 := time.Date(2026, 1, 1, 10, 0, 0, 0, time.UTC)
|
||||||
|
t2 := time.Date(2026, 1, 1, 11, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
type writeOp struct {
|
||||||
|
name string
|
||||||
|
fn func(store *JSONLStore, key string)
|
||||||
|
wantTimes []time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
ops := []writeOp{
|
||||||
|
{
|
||||||
|
name: "AddFullMessage",
|
||||||
|
fn: func(store *JSONLStore, key string) {
|
||||||
|
if err := store.AddFullMessage(context.Background(), key, providers.Message{
|
||||||
|
Role: "user",
|
||||||
|
Content: "custom time",
|
||||||
|
CreatedAt: &t1,
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("AddFullMessage: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
wantTimes: []time.Time{t1},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SetHistory",
|
||||||
|
fn: func(store *JSONLStore, key string) {
|
||||||
|
if err := store.SetHistory(context.Background(), key, []providers.Message{
|
||||||
|
{Role: "user", Content: "msg1", CreatedAt: &t1},
|
||||||
|
{Role: "assistant", Content: "msg2", CreatedAt: &t2},
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("SetHistory: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
wantTimes: []time.Time{t1, t2},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, op := range ops {
|
||||||
|
t.Run(op.name, func(t *testing.T) {
|
||||||
|
store := newTestStore(t)
|
||||||
|
key := "s1"
|
||||||
|
|
||||||
|
op.fn(store, key)
|
||||||
|
|
||||||
|
history, err := store.GetHistory(context.Background(), key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetHistory: %v", err)
|
||||||
|
}
|
||||||
|
if len(history) != len(op.wantTimes) {
|
||||||
|
t.Fatalf("expected %d messages, got %d", len(op.wantTimes), len(history))
|
||||||
|
}
|
||||||
|
for i, want := range op.wantTimes {
|
||||||
|
if history[i].CreatedAt == nil || !history[i].CreatedAt.Equal(want) {
|
||||||
|
t.Errorf(
|
||||||
|
"message %d CreatedAt = %v, want %v (should preserve caller-provided time)",
|
||||||
|
i, history[i].CreatedAt, want,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func BenchmarkAddMessage(b *testing.B) {
|
func BenchmarkAddMessage(b *testing.B) {
|
||||||
dir := b.TempDir()
|
dir := b.TempDir()
|
||||||
store, err := NewJSONLStore(dir)
|
store, err := NewJSONLStore(dir)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package protocoltypes
|
package protocoltypes
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
type ToolCall struct {
|
type ToolCall struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
|
|
@ -81,6 +83,7 @@ type Attachment struct {
|
||||||
type Message struct {
|
type Message struct {
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
|
CreatedAt *time.Time `json:"created_at,omitempty"`
|
||||||
Media []string `json:"media,omitempty"`
|
Media []string `json:"media,omitempty"`
|
||||||
Attachments []Attachment `json:"attachments,omitempty"`
|
Attachments []Attachment `json:"attachments,omitempty"`
|
||||||
ReasoningContent string `json:"reasoning_content,omitempty"`
|
ReasoningContent string `json:"reasoning_content,omitempty"`
|
||||||
|
|
|
||||||
|
|
@ -87,8 +87,13 @@ func (sm *SessionManager) AddFullMessage(sessionKey string, msg providers.Messag
|
||||||
sm.sessions[sessionKey] = session
|
sm.sessions[sessionKey] = session
|
||||||
}
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
if msg.CreatedAt == nil {
|
||||||
|
msg.CreatedAt = &now
|
||||||
|
}
|
||||||
|
|
||||||
session.Messages = append(session.Messages, msg)
|
session.Messages = append(session.Messages, msg)
|
||||||
session.Updated = time.Now()
|
session.Updated = now
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sm *SessionManager) GetHistory(key string) []providers.Message {
|
func (sm *SessionManager) GetHistory(key string) []providers.Message {
|
||||||
|
|
@ -300,7 +305,13 @@ func (sm *SessionManager) SetHistory(key string, history []providers.Message) {
|
||||||
// from the caller's slice.
|
// from the caller's slice.
|
||||||
msgs := make([]providers.Message, len(history))
|
msgs := make([]providers.Message, len(history))
|
||||||
copy(msgs, history)
|
copy(msgs, history)
|
||||||
|
now := time.Now()
|
||||||
|
for i := range msgs {
|
||||||
|
if msgs[i].CreatedAt == nil {
|
||||||
|
msgs[i].CreatedAt = &now
|
||||||
|
}
|
||||||
|
}
|
||||||
session.Messages = msgs
|
session.Messages = msgs
|
||||||
session.Updated = time.Now()
|
session.Updated = now
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ type sessionChatMessage struct {
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
Kind string `json:"kind,omitempty"`
|
Kind string `json:"kind,omitempty"`
|
||||||
|
CreatedAt *time.Time `json:"created_at,omitempty"`
|
||||||
Media []string `json:"media,omitempty"`
|
Media []string `json:"media,omitempty"`
|
||||||
Attachments []sessionChatAttachment `json:"attachments,omitempty"`
|
Attachments []sessionChatAttachment `json:"attachments,omitempty"`
|
||||||
ToolCalls []utils.VisibleToolCall `json:"tool_calls,omitempty"`
|
ToolCalls []utils.VisibleToolCall `json:"tool_calls,omitempty"`
|
||||||
|
|
@ -510,6 +511,7 @@ func sessionTranscriptMessages(
|
||||||
chatMsg := sessionChatMessage{
|
chatMsg := sessionChatMessage{
|
||||||
Role: "user",
|
Role: "user",
|
||||||
Content: msg.Content,
|
Content: msg.Content,
|
||||||
|
CreatedAt: msg.CreatedAt,
|
||||||
Media: append([]string(nil), msg.Media...),
|
Media: append([]string(nil), msg.Media...),
|
||||||
Attachments: attachments,
|
Attachments: attachments,
|
||||||
}
|
}
|
||||||
|
|
@ -530,8 +532,9 @@ func sessionTranscriptMessages(
|
||||||
toolCallsMsg, hasToolCallsMsg := assistantToolCallsMessage(
|
toolCallsMsg, hasToolCallsMsg := assistantToolCallsMessage(
|
||||||
msg.ToolCalls,
|
msg.ToolCalls,
|
||||||
toolFeedbackMaxArgsLength,
|
toolFeedbackMaxArgsLength,
|
||||||
|
msg.CreatedAt,
|
||||||
)
|
)
|
||||||
visibleToolMessages := visibleAssistantToolMessages(msg.ToolCalls)
|
visibleToolMessages := visibleAssistantToolMessages(msg.ToolCalls, msg.CreatedAt)
|
||||||
|
|
||||||
// Pico web chat can persist both visible `message` tool output and a
|
// Pico web chat can persist both visible `message` tool output and a
|
||||||
// later plain assistant reply in the same turn. Hide only the fixed
|
// later plain assistant reply in the same turn. Hide only the fixed
|
||||||
|
|
@ -556,6 +559,7 @@ func sessionTranscriptMessages(
|
||||||
chatMsg := sessionChatMessage{
|
chatMsg := sessionChatMessage{
|
||||||
Role: "assistant",
|
Role: "assistant",
|
||||||
Content: content,
|
Content: content,
|
||||||
|
CreatedAt: msg.CreatedAt,
|
||||||
Media: append([]string(nil), msg.Media...),
|
Media: append([]string(nil), msg.Media...),
|
||||||
Attachments: attachments,
|
Attachments: attachments,
|
||||||
}
|
}
|
||||||
|
|
@ -682,15 +686,17 @@ func assistantThoughtMessage(msg providers.Message) (sessionChatMessage, bool) {
|
||||||
return sessionChatMessage{}, false
|
return sessionChatMessage{}, false
|
||||||
}
|
}
|
||||||
return sessionChatMessage{
|
return sessionChatMessage{
|
||||||
Role: "assistant",
|
Role: "assistant",
|
||||||
Content: reasoning,
|
Content: reasoning,
|
||||||
Kind: "thought",
|
Kind: "thought",
|
||||||
|
CreatedAt: msg.CreatedAt,
|
||||||
}, true
|
}, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func assistantToolCallsMessage(
|
func assistantToolCallsMessage(
|
||||||
toolCalls []providers.ToolCall,
|
toolCalls []providers.ToolCall,
|
||||||
toolFeedbackMaxArgsLength int,
|
toolFeedbackMaxArgsLength int,
|
||||||
|
createdAt *time.Time,
|
||||||
) (sessionChatMessage, bool) {
|
) (sessionChatMessage, bool) {
|
||||||
if len(toolCalls) == 0 {
|
if len(toolCalls) == 0 {
|
||||||
return sessionChatMessage{}, false
|
return sessionChatMessage{}, false
|
||||||
|
|
@ -707,6 +713,7 @@ func assistantToolCallsMessage(
|
||||||
return sessionChatMessage{
|
return sessionChatMessage{
|
||||||
Role: "assistant",
|
Role: "assistant",
|
||||||
Kind: "tool_calls",
|
Kind: "tool_calls",
|
||||||
|
CreatedAt: createdAt,
|
||||||
ToolCalls: visibleToolCalls,
|
ToolCalls: visibleToolCalls,
|
||||||
}, true
|
}, true
|
||||||
}
|
}
|
||||||
|
|
@ -718,7 +725,7 @@ func visibleAssistantToolArgsPreview(
|
||||||
return utils.VisibleToolCallArgumentsPreview(tc, toolFeedbackMaxArgsLength)
|
return utils.VisibleToolCallArgumentsPreview(tc, toolFeedbackMaxArgsLength)
|
||||||
}
|
}
|
||||||
|
|
||||||
func visibleAssistantToolMessages(toolCalls []providers.ToolCall) []sessionChatMessage {
|
func visibleAssistantToolMessages(toolCalls []providers.ToolCall, createdAt *time.Time) []sessionChatMessage {
|
||||||
if len(toolCalls) == 0 {
|
if len(toolCalls) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -734,8 +741,9 @@ func visibleAssistantToolMessages(toolCalls []providers.ToolCall) []sessionChatM
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
messages = append(messages, sessionChatMessage{
|
messages = append(messages, sessionChatMessage{
|
||||||
Role: "assistant",
|
Role: "assistant",
|
||||||
Content: content,
|
Content: content,
|
||||||
|
CreatedAt: createdAt,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -918,6 +926,11 @@ func (h *Handler) handleGetSession(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for i := range sess.Messages {
|
||||||
|
if sess.Messages[i].CreatedAt == nil {
|
||||||
|
sess.Messages[i].CreatedAt = &sess.Updated
|
||||||
|
}
|
||||||
|
}
|
||||||
messages := detailSessionMessages(sess.Messages, toolFeedbackMaxArgsLength)
|
messages := detailSessionMessages(sess.Messages, toolFeedbackMaxArgsLength)
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ export interface SessionDetail {
|
||||||
messages: {
|
messages: {
|
||||||
role: "user" | "assistant"
|
role: "user" | "assistant"
|
||||||
content: string
|
content: string
|
||||||
|
created_at?: string
|
||||||
kind?: "normal" | "thought" | "tool_calls"
|
kind?: "normal" | "thought" | "tool_calls"
|
||||||
media?: string[]
|
media?: string[]
|
||||||
attachments?: {
|
attachments?: {
|
||||||
|
|
|
||||||
|
|
@ -132,12 +132,17 @@ export function AssistantMessage({
|
||||||
)}
|
)}
|
||||||
<span>{collapsedLabel}</span>
|
<span>{collapsedLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
<IconChevronDown
|
<div className="flex items-center gap-2">
|
||||||
className={cn(
|
{formattedTimestamp && (
|
||||||
"size-3.5 opacity-0 transition-all duration-200 group-hover:opacity-100",
|
<span className="opacity-50">{formattedTimestamp}</span>
|
||||||
isExpanded ? "rotate-180" : "",
|
|
||||||
)}
|
)}
|
||||||
/>
|
<IconChevronDown
|
||||||
|
className={cn(
|
||||||
|
"size-3.5 opacity-0 transition-all duration-200 group-hover:opacity-100",
|
||||||
|
isExpanded ? "rotate-180" : "",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(!isCollapsedBlock || isExpanded) && isToolCalls && hasToolCalls && (
|
{(!isCollapsedBlock || isExpanded) && isToolCalls && hasToolCalls && (
|
||||||
|
|
|
||||||
|
|
@ -346,6 +346,7 @@ export function ChatPage() {
|
||||||
<UserMessage
|
<UserMessage
|
||||||
content={msg.content}
|
content={msg.content}
|
||||||
attachments={msg.attachments}
|
attachments={msg.attachments}
|
||||||
|
timestamp={msg.timestamp}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,25 @@
|
||||||
|
import { formatMessageTime } from "@/hooks/use-pico-chat"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import type { ChatAttachment } from "@/store/chat"
|
import type { ChatAttachment } from "@/store/chat"
|
||||||
|
|
||||||
interface UserMessageProps {
|
interface UserMessageProps {
|
||||||
content: string
|
content: string
|
||||||
attachments?: ChatAttachment[]
|
attachments?: ChatAttachment[]
|
||||||
|
timestamp?: string | number
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UserMessage({ content, attachments = [] }: UserMessageProps) {
|
export function UserMessage({
|
||||||
|
content,
|
||||||
|
attachments = [],
|
||||||
|
timestamp = "",
|
||||||
|
}: UserMessageProps) {
|
||||||
const hasText = content.trim().length > 0
|
const hasText = content.trim().length > 0
|
||||||
const isCommand = content.trim().startsWith("/")
|
const isCommand = content.trim().startsWith("/")
|
||||||
const imageAttachments = attachments.filter(
|
const imageAttachments = attachments.filter(
|
||||||
(attachment) => attachment.type === "image",
|
(attachment) => attachment.type === "image",
|
||||||
)
|
)
|
||||||
|
const formattedTimestamp =
|
||||||
|
timestamp !== "" ? formatMessageTime(timestamp) : ""
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col items-end gap-1.5">
|
<div className="flex w-full flex-col items-end gap-1.5">
|
||||||
|
|
@ -49,6 +57,10 @@ export function UserMessage({ content, attachments = [] }: UserMessageProps) {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{formattedTimestamp && (
|
||||||
|
<span className="px-1 text-[12px] text-zinc-400">{formattedTimestamp}</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,8 +43,6 @@ export async function loadSessionMessages(
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
): Promise<ChatMessage[]> {
|
): Promise<ChatMessage[]> {
|
||||||
const detail = await getSessionHistory(sessionId)
|
const detail = await getSessionHistory(sessionId)
|
||||||
const fallbackTime = detail.updated
|
|
||||||
|
|
||||||
return detail.messages.map((message, index) => ({
|
return detail.messages.map((message, index) => ({
|
||||||
id: `hist-${index}-${Date.now()}`,
|
id: `hist-${index}-${Date.now()}`,
|
||||||
role: message.role,
|
role: message.role,
|
||||||
|
|
@ -58,7 +56,7 @@ export async function loadSessionMessages(
|
||||||
media: message.media,
|
media: message.media,
|
||||||
attachments: message.attachments,
|
attachments: message.attachments,
|
||||||
}),
|
}),
|
||||||
timestamp: fallbackTime,
|
timestamp: message.created_at ?? detail.updated,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue