forked from EduCraft/curriculum-project-hub
feat(hub): live TodoWrite checklist on Feishu agent cards
Always expose Claude Agent SDK TodoWrite (todoFeatureEnabled) so multi-step runs can plan in the open. Parse TodoWrite payloads into a progress panel on the streaming Feishu card (completed/in_progress/pending) and filter raw TodoWrite noise out of the tool-use list.
This commit is contained in:
@@ -2,9 +2,10 @@
|
||||
* Feishu interactive card builder for agent run output.
|
||||
*
|
||||
* Produces card JSON with:
|
||||
* 1. A collapsible tool-use panel (tool steps with status, params, results)
|
||||
* 2. A collapsible reasoning panel (thinking text)
|
||||
* 3. The streaming/final answer text (markdown)
|
||||
* 1. A live todo checklist when the agent uses TodoWrite (Manus-style progress)
|
||||
* 2. A collapsible tool-use panel (tool steps with status, params, results)
|
||||
* 3. A collapsible reasoning panel (thinking text)
|
||||
* 4. The streaming/final answer text (markdown)
|
||||
*
|
||||
* Adapted from openclaw-lark's builder.ts, simplified for our
|
||||
* message.patch-based approach (no CardKit 2.0 streaming_mode).
|
||||
@@ -12,6 +13,8 @@
|
||||
*/
|
||||
|
||||
import type { ToolUseTraceStep } from "./trace-store.js";
|
||||
import type { AgentTodoItem } from "../../agent/todoList.js";
|
||||
import { todoProgressSummary } from "../../agent/todoList.js";
|
||||
import { maskMarkdownImagesForStreaming, type CardContentSegment } from "../outboundImages.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -39,6 +42,7 @@ const TOOL_ICONS: Record<string, string> = {
|
||||
glob: "search-filled",
|
||||
grep: "search-filled",
|
||||
edit: "edit-filled",
|
||||
todowrite: "todo-filled",
|
||||
send_file: "send-filled",
|
||||
request_approval: "thumb-up-filled",
|
||||
feishu_read_context: "search-filled",
|
||||
@@ -61,20 +65,32 @@ export function buildAgentCard(params: {
|
||||
text: string;
|
||||
contentSegments?: readonly CardContentSegment[] | undefined;
|
||||
reasoningText: string | undefined;
|
||||
todos: readonly AgentTodoItem[] | undefined;
|
||||
toolUseSteps: ToolUseTraceStep[];
|
||||
toolUseElapsedMs: number | undefined;
|
||||
isError: boolean | undefined;
|
||||
interrupted: boolean | undefined;
|
||||
runId: string | undefined;
|
||||
}): Record<string, unknown> {
|
||||
const { phase, text, contentSegments, reasoningText, toolUseSteps, toolUseElapsedMs, isError, interrupted } = params;
|
||||
const { phase, text, contentSegments, reasoningText, todos, toolUseSteps, toolUseElapsedMs, isError, interrupted } = params;
|
||||
const elements: unknown[] = [];
|
||||
|
||||
// Tool-use panel (always present if there are steps)
|
||||
if (toolUseSteps.length > 0) {
|
||||
elements.push(buildToolUsePanel(toolUseSteps, toolUseElapsedMs, phase !== "complete"));
|
||||
} else if (phase === "thinking" || (phase === "streaming" && text === "" && (contentSegments === undefined || contentSegments.length === 0))) {
|
||||
elements.push(buildPendingToolUsePanel());
|
||||
// Todo checklist panel — primary progress signal; hide bare TodoWrite noise below.
|
||||
if (todos !== undefined && todos.length > 0) {
|
||||
elements.push(buildTodoPanel(todos, phase !== "complete"));
|
||||
}
|
||||
|
||||
// Tool-use panel (exclude TodoWrite itself — already shown as checklist)
|
||||
const visibleToolSteps = toolUseSteps.filter((step) => step.toolName.toLowerCase() !== "todowrite");
|
||||
if (visibleToolSteps.length > 0) {
|
||||
elements.push(buildToolUsePanel(visibleToolSteps, toolUseElapsedMs, phase !== "complete"));
|
||||
} else if (
|
||||
todos === undefined ||
|
||||
todos.length === 0
|
||||
) {
|
||||
if (phase === "thinking" || (phase === "streaming" && text === "" && (contentSegments === undefined || contentSegments.length === 0))) {
|
||||
elements.push(buildPendingToolUsePanel());
|
||||
}
|
||||
}
|
||||
|
||||
// Reasoning panel
|
||||
@@ -161,6 +177,62 @@ function buildInterruptAction(runId: string): unknown {
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Todo checklist panel
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function buildTodoPanel(todos: readonly AgentTodoItem[], expanded: boolean): unknown {
|
||||
const { completed, total, inProgress } = todoProgressSummary(todos);
|
||||
const titleParts = [`\u{1F4CB} \u4EFB\u52A1\u8FDB\u5EA6 ${completed}/${total}`];
|
||||
if (inProgress > 0 && completed < total) titleParts.push(`(\u8FDB\u884C\u4E2D ${inProgress})`);
|
||||
const lines = todos.map((todo) => formatTodoLine(todo));
|
||||
return {
|
||||
tag: "collapsible_panel",
|
||||
expanded,
|
||||
header: {
|
||||
title: {
|
||||
tag: "plain_text",
|
||||
content: titleParts.join(" "),
|
||||
text_color: completed === total && total > 0 ? "green" : "grey",
|
||||
text_size: "notation",
|
||||
},
|
||||
vertical_align: "center",
|
||||
icon: {
|
||||
tag: "standard_icon",
|
||||
token: "down-small-ccm_outlined",
|
||||
color: "grey",
|
||||
size: "16px 16px",
|
||||
},
|
||||
icon_position: "right",
|
||||
icon_expanded_angle: -180,
|
||||
},
|
||||
border: { color: "grey", corner_radius: "5px" },
|
||||
vertical_spacing: "4px",
|
||||
padding: "8px 8px 8px 8px",
|
||||
elements: [
|
||||
{
|
||||
tag: "markdown",
|
||||
content: lines.join("\n"),
|
||||
text_size: "notation",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function formatTodoLine(todo: AgentTodoItem): string {
|
||||
const label =
|
||||
todo.status === "in_progress" && todo.activeForm !== undefined && todo.activeForm !== ""
|
||||
? todo.activeForm
|
||||
: todo.content;
|
||||
if (todo.status === "completed") return `- [\u2713] ~~${escapeMd(todo.content)}~~`;
|
||||
if (todo.status === "in_progress") return `- [\u25B6] **${escapeMd(label)}**`;
|
||||
return `- [ ] ${escapeMd(todo.content)}`;
|
||||
}
|
||||
|
||||
function escapeMd(text: string): string {
|
||||
return text.replace(/([\\`*_{}\[\]()#+\-.!>])/g, "\\$1");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tool-use panel
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user