mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
Feat(channels): unify animated tool feedback across chat channels and Pico (#2622)
* feat(channels): unify tool feedback animation across discord telegram and feishu * fix(tool-feedback): unify fallback and single-message delivery * fix(channels): finalize tool feedback in place * fix ci * feat: improve tool feedback * fix review blockers in pico token cache and tool feedback fix(provider): preserve function thought signatures fix(feishu): recover tool feedback after edit fallback * * delete dead code * fix(pico): clean up tool feedback progress state * fix ci * fix(web): preserve tool feedback line breaks in chat * fix(channels): preserve tool feedback progress state fix(pico): preserve context usage when finalizing tool feedback chore: record branch review pass fix: preserve tool feedback finalization state fix(web): handle pico history update fallback * fix ci
This commit is contained in:
@@ -100,8 +100,8 @@ export function AssistantMessage({
|
||||
className={cn(
|
||||
"prose dark:prose-invert prose-pre:my-2 prose-pre:overflow-x-auto prose-pre:rounded-lg prose-pre:border prose-pre:bg-zinc-100 prose-pre:p-0 prose-pre:text-zinc-900 dark:prose-pre:bg-zinc-950 dark:prose-pre:text-zinc-100 max-w-none [overflow-wrap:anywhere] break-words",
|
||||
isThought
|
||||
? "prose-p:my-1.5 px-3 pt-0 pb-3 text-[13px] leading-relaxed opacity-70"
|
||||
: "prose-p:my-2 p-4 text-[15px] leading-relaxed",
|
||||
? "prose-p:my-1.5 prose-p:whitespace-pre-wrap px-3 pt-0 pb-3 text-[13px] leading-relaxed opacity-70"
|
||||
: "prose-p:my-2 prose-p:whitespace-pre-wrap p-4 text-[15px] leading-relaxed",
|
||||
)}
|
||||
>
|
||||
<ReactMarkdown
|
||||
|
||||
@@ -4,6 +4,7 @@ import { normalizeUnixTimestamp } from "@/features/chat/state"
|
||||
import {
|
||||
type AssistantMessageKind,
|
||||
type ChatAttachment,
|
||||
type ChatMessage,
|
||||
type ContextUsage,
|
||||
updateChatStore,
|
||||
} from "@/store/chat"
|
||||
@@ -90,6 +91,35 @@ function parseContextUsage(
|
||||
}
|
||||
}
|
||||
|
||||
function isToolFeedbackMessage(message: ChatMessage): boolean {
|
||||
if (message.role !== "assistant") {
|
||||
return false
|
||||
}
|
||||
|
||||
const firstLine = message.content.split("\n", 1)[0]?.trim() ?? ""
|
||||
return /^🔧\s+`[^`]+`/.test(firstLine)
|
||||
}
|
||||
|
||||
function findToolFeedbackMessageIndex(messages: ChatMessage[]): number {
|
||||
let lastUserIndex = -1
|
||||
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
||||
if (messages[i].role === "user") {
|
||||
lastUserIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
||||
if (i <= lastUserIndex) {
|
||||
break
|
||||
}
|
||||
if (isToolFeedbackMessage(messages[i])) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
export function handlePicoMessage(
|
||||
message: PicoMessage,
|
||||
expectedSessionId: string,
|
||||
@@ -138,21 +168,88 @@ export function handlePicoMessage(
|
||||
const hasKind = hasAssistantKindPayload(payload)
|
||||
const kind = parseAssistantMessageKind(payload)
|
||||
const attachments = parseAttachments(payload)
|
||||
const contextUsage = parseContextUsage(payload)
|
||||
const timestamp =
|
||||
message.timestamp !== undefined &&
|
||||
Number.isFinite(Number(message.timestamp))
|
||||
? normalizeUnixTimestamp(Number(message.timestamp))
|
||||
: Date.now()
|
||||
if (!messageId) {
|
||||
break
|
||||
}
|
||||
|
||||
updateChatStore((prev) => ({
|
||||
messages: prev.messages.map((msg) =>
|
||||
msg.id === messageId
|
||||
? {
|
||||
...msg,
|
||||
content,
|
||||
...(hasKind ? { kind } : {}),
|
||||
...(attachments ? { attachments } : {}),
|
||||
}
|
||||
: msg,
|
||||
),
|
||||
messages: (() => {
|
||||
let found = false
|
||||
const messages = prev.messages.map((msg) => {
|
||||
if (msg.id !== messageId) {
|
||||
return msg
|
||||
}
|
||||
found = true
|
||||
return {
|
||||
...msg,
|
||||
id: messageId,
|
||||
content,
|
||||
...(hasKind ? { kind } : {}),
|
||||
...(attachments ? { attachments } : {}),
|
||||
}
|
||||
})
|
||||
if (found) {
|
||||
return messages
|
||||
}
|
||||
|
||||
const fallbackIndex = findToolFeedbackMessageIndex(messages)
|
||||
if (fallbackIndex >= 0) {
|
||||
return messages.map((msg, index) =>
|
||||
index === fallbackIndex
|
||||
? {
|
||||
...msg,
|
||||
id: messageId,
|
||||
content,
|
||||
...(hasKind ? { kind } : {}),
|
||||
...(attachments ? { attachments } : {}),
|
||||
}
|
||||
: msg,
|
||||
)
|
||||
}
|
||||
|
||||
return [
|
||||
...messages,
|
||||
{
|
||||
id: messageId,
|
||||
role: "assistant" as const,
|
||||
content,
|
||||
...(hasKind ? { kind } : {}),
|
||||
...(attachments ? { attachments } : {}),
|
||||
timestamp,
|
||||
},
|
||||
]
|
||||
})(),
|
||||
...(contextUsage ? { contextUsage } : {}),
|
||||
}))
|
||||
break
|
||||
}
|
||||
|
||||
case "message.delete": {
|
||||
const messageId = payload.message_id as string
|
||||
if (!messageId) {
|
||||
break
|
||||
}
|
||||
|
||||
updateChatStore((prev) => ({
|
||||
messages: (() => {
|
||||
const exactMessages = prev.messages.filter((msg) => msg.id !== messageId)
|
||||
if (exactMessages.length !== prev.messages.length) {
|
||||
return exactMessages
|
||||
}
|
||||
|
||||
const fallbackIndex = findToolFeedbackMessageIndex(prev.messages)
|
||||
if (fallbackIndex < 0) {
|
||||
return prev.messages
|
||||
}
|
||||
|
||||
return prev.messages.filter((_, index) => index !== fallbackIndex)
|
||||
})(),
|
||||
}))
|
||||
break
|
||||
}
|
||||
|
||||
@@ -605,9 +605,9 @@
|
||||
"split_on_marker": "Chatty Mode",
|
||||
"split_on_marker_hint": "Split long messages into short ones like real human chatting.",
|
||||
"tool_feedback_enabled": "Tool Feedback",
|
||||
"tool_feedback_enabled_hint": "Send a short tool-call preview into the current chat before each tool execution.",
|
||||
"tool_feedback_max_args_length": "Tool Feedback Args Preview Length",
|
||||
"tool_feedback_max_args_length_hint": "Maximum number of argument characters shown in each tool feedback message. Set to 0 to use the default.",
|
||||
"tool_feedback_enabled_hint": "Send a short execution note into the current chat before each tool runs.",
|
||||
"tool_feedback_max_args_length": "Tool Feedback Length",
|
||||
"tool_feedback_max_args_length_hint": "Maximum number of characters shown in each tool feedback message. Set to 0 to use the default.",
|
||||
"exec_enabled": "Allow Commands",
|
||||
"exec_enabled_hint": "Enable or disable command execution for the app. When disabled, no command requests will run.",
|
||||
"allow_remote": "Allow Remote Commands",
|
||||
|
||||
@@ -605,9 +605,9 @@
|
||||
"split_on_marker": "连续短消息",
|
||||
"split_on_marker_hint": "像真人聊天一样,把长难句拆成多条短消息快速发出",
|
||||
"tool_feedback_enabled": "工具反馈",
|
||||
"tool_feedback_enabled_hint": "在每次执行工具前,先向当前会话发送一条简短的工具调用预览",
|
||||
"tool_feedback_max_args_length": "工具反馈参数预览长度",
|
||||
"tool_feedback_max_args_length_hint": "每条工具反馈消息中展示的参数字符上限。设为 0 时使用默认值",
|
||||
"tool_feedback_enabled_hint": "在每次执行工具前,先向当前会话发送一条简短的执行说明",
|
||||
"tool_feedback_max_args_length": "工具反馈长度",
|
||||
"tool_feedback_max_args_length_hint": "每条工具反馈消息中展示的字符上限。设为 0 时使用默认值",
|
||||
"exec_enabled": "允许命令执行",
|
||||
"exec_enabled_hint": "控制应用是否允许执行命令。关闭后,所有命令请求都不会执行",
|
||||
"allow_remote": "允许远程命令执行",
|
||||
|
||||
Reference in New Issue
Block a user