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:
lxowalle
2026-04-23 10:35:50 +08:00
committed by GitHub
parent 68ceb54b36
commit 451db2f5d8
44 changed files with 4569 additions and 188 deletions
@@ -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
+107 -10
View File
@@ -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
}
+3 -3
View File
@@ -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",
+3 -3
View File
@@ -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": "允许远程命令执行",