mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
feat(chat,seahorse): persist and display model_name across history (#2897)
* feat(chat,seahorse): persist and display model_name across history * test(seahorse): fix lint regressions in repair coverage * fix(pico): preserve model_name in live updates * fix(pico): preserve model_name through live stream wrappers
This commit is contained in:
@@ -50,6 +50,7 @@ type sessionChatMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
Kind string `json:"kind,omitempty"`
|
||||
ModelName string `json:"model_name,omitempty"`
|
||||
Media []string `json:"media,omitempty"`
|
||||
Attachments []sessionChatAttachment `json:"attachments,omitempty"`
|
||||
ToolCalls []utils.VisibleToolCall `json:"tool_calls,omitempty"`
|
||||
@@ -510,6 +511,7 @@ func sessionTranscriptMessages(
|
||||
chatMsg := sessionChatMessage{
|
||||
Role: "user",
|
||||
Content: msg.Content,
|
||||
ModelName: msg.ModelName,
|
||||
Media: append([]string(nil), msg.Media...),
|
||||
Attachments: attachments,
|
||||
}
|
||||
@@ -529,9 +531,10 @@ func sessionTranscriptMessages(
|
||||
|
||||
toolCallsMsg, hasToolCallsMsg := assistantToolCallsMessage(
|
||||
msg.ToolCalls,
|
||||
msg.ModelName,
|
||||
toolFeedbackMaxArgsLength,
|
||||
)
|
||||
visibleToolMessages := visibleAssistantToolMessages(msg.ToolCalls)
|
||||
visibleToolMessages := visibleAssistantToolMessages(msg.ToolCalls, msg.ModelName)
|
||||
|
||||
// Pico web chat can persist both visible `message` tool output and a
|
||||
// later plain assistant reply in the same turn. Hide only the fixed
|
||||
@@ -556,6 +559,7 @@ func sessionTranscriptMessages(
|
||||
chatMsg := sessionChatMessage{
|
||||
Role: "assistant",
|
||||
Content: content,
|
||||
ModelName: msg.ModelName,
|
||||
Media: append([]string(nil), msg.Media...),
|
||||
Attachments: attachments,
|
||||
}
|
||||
@@ -682,14 +686,16 @@ func assistantThoughtMessage(msg providers.Message) (sessionChatMessage, bool) {
|
||||
return sessionChatMessage{}, false
|
||||
}
|
||||
return sessionChatMessage{
|
||||
Role: "assistant",
|
||||
Content: reasoning,
|
||||
Kind: "thought",
|
||||
Role: "assistant",
|
||||
Content: reasoning,
|
||||
Kind: "thought",
|
||||
ModelName: msg.ModelName,
|
||||
}, true
|
||||
}
|
||||
|
||||
func assistantToolCallsMessage(
|
||||
toolCalls []providers.ToolCall,
|
||||
modelName string,
|
||||
toolFeedbackMaxArgsLength int,
|
||||
) (sessionChatMessage, bool) {
|
||||
if len(toolCalls) == 0 {
|
||||
@@ -707,6 +713,7 @@ func assistantToolCallsMessage(
|
||||
return sessionChatMessage{
|
||||
Role: "assistant",
|
||||
Kind: "tool_calls",
|
||||
ModelName: modelName,
|
||||
ToolCalls: visibleToolCalls,
|
||||
}, true
|
||||
}
|
||||
@@ -718,7 +725,7 @@ func visibleAssistantToolArgsPreview(
|
||||
return utils.VisibleToolCallArgumentsPreview(tc, toolFeedbackMaxArgsLength)
|
||||
}
|
||||
|
||||
func visibleAssistantToolMessages(toolCalls []providers.ToolCall) []sessionChatMessage {
|
||||
func visibleAssistantToolMessages(toolCalls []providers.ToolCall, modelName string) []sessionChatMessage {
|
||||
if len(toolCalls) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -734,8 +741,9 @@ func visibleAssistantToolMessages(toolCalls []providers.ToolCall) []sessionChatM
|
||||
continue
|
||||
}
|
||||
messages = append(messages, sessionChatMessage{
|
||||
Role: "assistant",
|
||||
Content: content,
|
||||
Role: "assistant",
|
||||
Content: content,
|
||||
ModelName: modelName,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -564,7 +564,7 @@ func TestHandleGetSession_ReconstructsThoughtFromAssistantReasoningContent(t *te
|
||||
sessionKey := picoSessionPrefix + "detail-reasoning-content"
|
||||
for _, msg := range []providers.Message{
|
||||
{Role: "user", Content: "hello"},
|
||||
{Role: "assistant", Content: "final visible answer", ReasoningContent: "internal chain of thought"},
|
||||
{Role: "assistant", Content: "final visible answer", ModelName: "gpt-5.4", ReasoningContent: "internal chain of thought"},
|
||||
} {
|
||||
if err := store.AddFullMessage(nil, sessionKey, msg); err != nil {
|
||||
t.Fatalf("AddFullMessage() error = %v", err)
|
||||
@@ -597,9 +597,15 @@ func TestHandleGetSession_ReconstructsThoughtFromAssistantReasoningContent(t *te
|
||||
resp.Messages[1].Kind != "thought" {
|
||||
t.Fatalf("thought message = %#v, want assistant thought/internal chain of thought", resp.Messages[1])
|
||||
}
|
||||
if resp.Messages[1].ModelName != "gpt-5.4" {
|
||||
t.Fatalf("thought model_name = %q, want %q", resp.Messages[1].ModelName, "gpt-5.4")
|
||||
}
|
||||
if resp.Messages[2].Role != "assistant" || resp.Messages[2].Content != "final visible answer" {
|
||||
t.Fatalf("final message = %#v, want assistant/final visible answer", resp.Messages[2])
|
||||
}
|
||||
if resp.Messages[2].ModelName != "gpt-5.4" {
|
||||
t.Fatalf("final model_name = %q, want %q", resp.Messages[2].ModelName, "gpt-5.4")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleGetSession_ReconstructsRefreshMatrixForThoughtAndToolSummary(t *testing.T) {
|
||||
@@ -725,8 +731,9 @@ func TestHandleGetSession_ReconstructsVisibleMessageToolOutputWithoutDuplicateSu
|
||||
for _, msg := range []providers.Message{
|
||||
{Role: "user", Content: "test"},
|
||||
{
|
||||
Role: "assistant",
|
||||
Content: "",
|
||||
Role: "assistant",
|
||||
Content: "",
|
||||
ModelName: "gpt-5.4-mini",
|
||||
ToolCalls: []providers.ToolCall{
|
||||
{
|
||||
ID: "call_1",
|
||||
@@ -771,9 +778,15 @@ func TestHandleGetSession_ReconstructsVisibleMessageToolOutputWithoutDuplicateSu
|
||||
t.Fatalf("first message = %#v, want user/test", resp.Messages[0])
|
||||
}
|
||||
assertVisibleToolCallMessage(t, resp.Messages[1], "message")
|
||||
if resp.Messages[1].ModelName != "gpt-5.4-mini" {
|
||||
t.Fatalf("tool_calls model_name = %q, want %q", resp.Messages[1].ModelName, "gpt-5.4-mini")
|
||||
}
|
||||
if resp.Messages[2].Role != "assistant" || resp.Messages[2].Content != "visible tool output" {
|
||||
t.Fatalf("assistant message = %#v, want visible tool output", resp.Messages[2])
|
||||
}
|
||||
if resp.Messages[2].ModelName != "gpt-5.4-mini" {
|
||||
t.Fatalf("visible tool output model_name = %q, want %q", resp.Messages[2].ModelName, "gpt-5.4-mini")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleGetSession_PreservesFinalAssistantReplyAfterMessageToolOutput(t *testing.T) {
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface SessionDetail {
|
||||
role: "user" | "assistant"
|
||||
content: string
|
||||
kind?: "normal" | "thought" | "tool_calls"
|
||||
model_name?: string
|
||||
media?: string[]
|
||||
attachments?: {
|
||||
type?: "image" | "audio" | "video" | "file"
|
||||
|
||||
@@ -33,6 +33,7 @@ interface AssistantMessageProps {
|
||||
content: string
|
||||
attachments?: ChatAttachment[]
|
||||
kind?: AssistantMessageKind
|
||||
modelName?: string
|
||||
toolCalls?: ChatToolCall[]
|
||||
timestamp?: string | number
|
||||
}
|
||||
@@ -41,6 +42,7 @@ export function AssistantMessage({
|
||||
content,
|
||||
attachments = [],
|
||||
kind = "normal",
|
||||
modelName,
|
||||
toolCalls = [],
|
||||
timestamp = "",
|
||||
}: AssistantMessageProps) {
|
||||
@@ -66,13 +68,20 @@ export function AssistantMessage({
|
||||
const copyMessageLabel = isCopied
|
||||
? t("chat.copiedLabel")
|
||||
: t("chat.copyMessage")
|
||||
const trimmedModelName = modelName?.trim() ?? ""
|
||||
|
||||
return (
|
||||
<div className="group flex w-full flex-col gap-1.5">
|
||||
{!isCollapsedBlock && (
|
||||
<div className="text-muted-foreground/60 flex items-center justify-between gap-2 px-1 text-xs opacity-70">
|
||||
<div className="text-muted-foreground/60 flex items-center justify-between gap-2 px-1 text-xs opacity-70">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>PicoClaw</span>
|
||||
{trimmedModelName && (
|
||||
<>
|
||||
<span className="opacity-50">•</span>
|
||||
<span>{trimmedModelName}</span>
|
||||
</>
|
||||
)}
|
||||
{formattedTimestamp && (
|
||||
<>
|
||||
<span className="opacity-50">•</span>
|
||||
@@ -104,6 +113,9 @@ export function AssistantMessage({
|
||||
<IconTool className="size-3.5" />
|
||||
)}
|
||||
<span>{collapsedLabel}</span>
|
||||
{trimmedModelName && (
|
||||
<span className="text-muted-foreground/45">{trimmedModelName}</span>
|
||||
)}
|
||||
</div>
|
||||
<IconChevronDown
|
||||
className={cn(
|
||||
|
||||
@@ -376,6 +376,7 @@ export function ChatPage() {
|
||||
content={msg.content}
|
||||
attachments={msg.attachments}
|
||||
kind={msg.kind}
|
||||
modelName={msg.modelName}
|
||||
toolCalls={msg.toolCalls}
|
||||
timestamp={msg.timestamp}
|
||||
/>
|
||||
|
||||
@@ -50,6 +50,7 @@ export async function loadSessionMessages(
|
||||
role: message.role,
|
||||
content: message.content,
|
||||
kind: message.role === "assistant" ? (message.kind ?? "normal") : undefined,
|
||||
modelName: message.model_name,
|
||||
toolCalls:
|
||||
message.role === "assistant"
|
||||
? parseToolCallsValue(message.tool_calls)
|
||||
@@ -86,7 +87,7 @@ function messageSignature(message: ChatMessage): string {
|
||||
|
||||
return `${message.role}\u0000${message.content}\u0000${normalizeMessageTimestamp(
|
||||
message.timestamp,
|
||||
)}\u0000${message.kind ?? ""}\u0000${attachmentSignature}\u0000${toolCallsSignature(
|
||||
)}\u0000${message.kind ?? ""}\u0000${message.modelName ?? ""}\u0000${attachmentSignature}\u0000${toolCallsSignature(
|
||||
message.toolCalls,
|
||||
)}`
|
||||
}
|
||||
|
||||
@@ -83,6 +83,14 @@ function parseContextUsage(
|
||||
}
|
||||
}
|
||||
|
||||
function parseModelName(payload: Record<string, unknown>): string | undefined {
|
||||
if (typeof payload.model_name !== "string") {
|
||||
return undefined
|
||||
}
|
||||
const modelName = payload.model_name.trim()
|
||||
return modelName || undefined
|
||||
}
|
||||
|
||||
export function handlePicoMessage(
|
||||
message: PicoMessage,
|
||||
expectedSessionId: string,
|
||||
@@ -102,6 +110,7 @@ export function handlePicoMessage(
|
||||
const attachments = parseAttachments(payload)
|
||||
const contextUsage = parseContextUsage(payload)
|
||||
const isPlaceholder = payload.placeholder === true
|
||||
const modelName = parseModelName(payload)
|
||||
const timestamp =
|
||||
message.timestamp !== undefined &&
|
||||
Number.isFinite(Number(message.timestamp))
|
||||
@@ -116,6 +125,7 @@ export function handlePicoMessage(
|
||||
role: "assistant",
|
||||
content,
|
||||
kind,
|
||||
...(modelName ? { modelName } : {}),
|
||||
...(toolCalls ? { toolCalls } : {}),
|
||||
attachments,
|
||||
timestamp,
|
||||
@@ -135,6 +145,7 @@ export function handlePicoMessage(
|
||||
const messageId = payload.message_id as string
|
||||
const attachments = parseAttachments(payload)
|
||||
const contextUsage = parseContextUsage(payload)
|
||||
const modelName = parseModelName(payload)
|
||||
const timestamp =
|
||||
message.timestamp !== undefined &&
|
||||
Number.isFinite(Number(message.timestamp))
|
||||
@@ -160,6 +171,7 @@ export function handlePicoMessage(
|
||||
content,
|
||||
kind,
|
||||
toolCalls,
|
||||
...(modelName ? { modelName } : {}),
|
||||
...(attachments ? { attachments } : {}),
|
||||
}
|
||||
})
|
||||
@@ -178,6 +190,7 @@ export function handlePicoMessage(
|
||||
content,
|
||||
kind,
|
||||
toolCalls,
|
||||
...(modelName ? { modelName } : {}),
|
||||
...(attachments ? { attachments } : {}),
|
||||
timestamp,
|
||||
},
|
||||
|
||||
@@ -44,6 +44,7 @@ export interface ChatMessage {
|
||||
content: string
|
||||
timestamp: number | string
|
||||
kind?: AssistantMessageKind
|
||||
modelName?: string
|
||||
attachments?: ChatAttachment[]
|
||||
toolCalls?: ChatToolCall[]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user