@zmzai/theme 0.9.7 → 0.9.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useState } from "react";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
4
|
import { AnimatePresence, motion } from "framer-motion";
|
|
5
5
|
|
|
6
6
|
import { Icon } from "../../components/icon/Icon";
|
|
7
7
|
import "./reasoning.css";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Reasoning —
|
|
10
|
+
* Reasoning — 思考过程折叠块(v0.9.8 计时)。
|
|
11
11
|
*
|
|
12
|
+
* 标签行显示思考时长:文本持续增长时「思考中 Ns」,停止增长 1.5s 后
|
|
13
|
+
* 「已思考 Ns」——流式体验与主流(Claude / o3 的 Thought for Xs)对齐。
|
|
12
14
|
* 运行中自动展开(呼吸圆点),完成后收起为标签行,点击回看。
|
|
13
|
-
* 展开内容渐显(AnimatePresence)。
|
|
14
15
|
*
|
|
15
16
|
* @example
|
|
16
17
|
* <Reasoning text={chainOfThought} active={isStreaming} />
|
|
@@ -18,11 +19,44 @@ import "./reasoning.css";
|
|
|
18
19
|
export function Reasoning({ text, active = false }: { text: string; active?: boolean }) {
|
|
19
20
|
const [open, setOpen] = useState(false);
|
|
20
21
|
const expanded = active || open;
|
|
22
|
+
|
|
23
|
+
// 思考计时:startedAt = 首次收到文本;lastChangeAt = 最近一次文本变化。
|
|
24
|
+
// 「正在思考」= active,或 1.5s 内文本仍在变化(delta 流)。
|
|
25
|
+
const startedAtRef = useRef<number | null>(null);
|
|
26
|
+
const lastChangeRef = useRef<number>(0);
|
|
27
|
+
const [thinking, setThinking] = useState(false);
|
|
28
|
+
const [seconds, setSeconds] = useState(0);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!text) return;
|
|
32
|
+
if (startedAtRef.current === null) startedAtRef.current = Date.now();
|
|
33
|
+
lastChangeRef.current = Date.now();
|
|
34
|
+
setThinking(true);
|
|
35
|
+
}, [text]);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!thinking) return;
|
|
39
|
+
const timer = setInterval(() => {
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
const settled = now - lastChangeRef.current > 1500 && !active;
|
|
42
|
+
const secs = Math.max(1, Math.round((now - (startedAtRef.current ?? now)) / 1000));
|
|
43
|
+
setSeconds(secs);
|
|
44
|
+
if (settled) setThinking(false);
|
|
45
|
+
}, 500);
|
|
46
|
+
return () => clearInterval(timer);
|
|
47
|
+
}, [thinking, active]);
|
|
48
|
+
|
|
49
|
+
const label = thinking
|
|
50
|
+
? `思考中 ${seconds ? `${seconds}s` : "…"}`
|
|
51
|
+
: seconds > 0
|
|
52
|
+
? `已思考 ${seconds}s`
|
|
53
|
+
: "思考过程";
|
|
54
|
+
|
|
21
55
|
return (
|
|
22
56
|
<div className="zmz-reasoning">
|
|
23
57
|
<button type="button" className="zmz-reasoning-toggle" aria-expanded={expanded} onClick={() => setOpen((value) => !value)}>
|
|
24
|
-
<span className={`zmz-reasoning-dot ${active ? "live" : ""}`} aria-hidden />
|
|
25
|
-
<span
|
|
58
|
+
<span className={`zmz-reasoning-dot ${active || thinking ? "live" : ""}`} aria-hidden />
|
|
59
|
+
<span>{label}</span>
|
|
26
60
|
<Icon name="chevron-down" size={11} className={expanded ? "zmz-chevron open" : "zmz-chevron"} />
|
|
27
61
|
</button>
|
|
28
62
|
<AnimatePresence initial={false}>
|
|
@@ -4,13 +4,15 @@ import { useState } from "react";
|
|
|
4
4
|
|
|
5
5
|
import { Badge } from "../../components/badge";
|
|
6
6
|
import { Icon } from "../../components/icon/Icon";
|
|
7
|
-
import { formatToolInput, toolDuration, type ToolCall } from "./types";
|
|
7
|
+
import { formatToolInput, formatToolSummary, toolDuration, type ToolCall } from "./types";
|
|
8
8
|
import "./tool-card.css";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* ToolCard — 单次工具调用卡:状态色左边条 + 展开/收起的输入输出详情。
|
|
12
12
|
*
|
|
13
|
-
*
|
|
13
|
+
* 标题行显示人类可读摘要(v0.9.8 formatToolSummary:`Read src/foo.ts`、
|
|
14
|
+
* `Bash pnpm build`),引擎提供的 state.title 优先;JSON 结构化输入只出现在
|
|
15
|
+
* 展开区。运行中卡常开;进入终态后收起,点标题可再看输出。
|
|
14
16
|
* `sessionIdle=true` 时非终态工具渲染为「失败(中断)」——
|
|
15
17
|
* 会话空闲却停在 running 是崩溃/重启的遗留,不该永远转圈。
|
|
16
18
|
*
|
|
@@ -22,7 +24,9 @@ export function ToolCard({ call, sessionIdle = false }: { call: ToolCall; sessio
|
|
|
22
24
|
const state = call.state;
|
|
23
25
|
const running = state.status === "running" || state.status === "pending";
|
|
24
26
|
const interrupted = running && sessionIdle;
|
|
25
|
-
const
|
|
27
|
+
const summary = formatToolSummary(call.tool, state.input);
|
|
28
|
+
const title = ("title" in state && state.title) || summary || call.tool;
|
|
29
|
+
const isSummary = !("title" in state && state.title) && !!summary; // 摘要是路径/命令 → 等宽更可读
|
|
26
30
|
const output = state.status === "completed" ? state.output : state.status === "error" ? state.error : null;
|
|
27
31
|
const statusClass = state.status === "completed" ? "completed" : interrupted ? "failed" : state.status === "error" ? "failed" : "running";
|
|
28
32
|
const isOpen = running || expanded;
|
|
@@ -31,7 +35,7 @@ export function ToolCard({ call, sessionIdle = false }: { call: ToolCall; sessio
|
|
|
31
35
|
<div className={`tool-card ${statusClass}`}>
|
|
32
36
|
<button type="button" className="tool-card-trigger" aria-expanded={isOpen} onClick={() => setExpanded((value) => !value)}>
|
|
33
37
|
<span className="tool-card-glyph" aria-hidden><Icon name={state.status === "completed" ? "check" : interrupted || state.status === "error" ? "cross" : "chevron-down"} size={12} /></span>
|
|
34
|
-
<span className="tool-card-label">{title}</span>
|
|
38
|
+
<span className={isSummary ? "tool-card-label tool-card-label-mono" : "tool-card-label"}>{title}</span>
|
|
35
39
|
{(state.status === "error" || interrupted) && <Badge variant="danger" size="sm">失败</Badge>}
|
|
36
40
|
{running && !interrupted && <Badge variant="live" size="sm">运行中</Badge>}
|
|
37
41
|
{toolDuration(call) && <span className="tool-card-duration">{toolDuration(call)}</span>}
|
|
@@ -75,11 +79,9 @@ export function ToolGroup({ calls, sessionIdle = false }: { calls: ToolCall[]; s
|
|
|
75
79
|
const glyph = failed.length > 0 ? "cross" : running.length > 0 ? "chevron-down" : "check";
|
|
76
80
|
const single = calls.length === 1 ? calls[0] : undefined;
|
|
77
81
|
const singleTitle = single
|
|
78
|
-
? single.state.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
? single.state.error
|
|
82
|
-
: single.tool
|
|
82
|
+
? ("title" in single.state && single.state.title) ||
|
|
83
|
+
formatToolSummary(single.tool, single.state.input) ||
|
|
84
|
+
(single.state.status === "error" ? single.state.error : single.tool)
|
|
83
85
|
: null;
|
|
84
86
|
const summary = [
|
|
85
87
|
running.length > 0 && `${running.length} 个进行中`,
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
.tool-card.completed .tool-card-glyph { color: var(--color-success); }
|
|
13
13
|
.tool-card.failed .tool-card-glyph { color: var(--color-danger); }
|
|
14
14
|
.tool-card-label { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.75rem; }
|
|
15
|
+
/* 摘要行(formatToolSummary):路径/命令用等宽,与 JSON 展开区视觉连续 */
|
|
16
|
+
.tool-card-label-mono { font-family: var(--font-mono); font-size: 0.7rem; }
|
|
15
17
|
.tool-card-duration { flex: 0 0 auto; color: var(--color-muted); font-family: var(--font-mono); font-size: 0.6rem; }
|
|
16
18
|
.tool-card-chevron { flex: 0 0 auto; color: var(--color-muted); transition: transform 0.15s var(--ease-out); }
|
|
17
19
|
.tool-card-chevron.open { transform: rotate(180deg); }
|
|
@@ -39,3 +39,51 @@ export function formatToolInput(input: unknown): string {
|
|
|
39
39
|
return String(input);
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
+
|
|
43
|
+
/** 工具输入摘要(v0.9.8):从 input 的关键字段生成人类可读一行 ——
|
|
44
|
+
* `Read src/foo.ts`、`Edit src/bar.ts`、`Bash pnpm build`、`Grep "pat" in src/`。
|
|
45
|
+
* 字段名与 agent-framework 工具 schema 对齐(path/command/pattern/description…),
|
|
46
|
+
* 未知工具尽力提取,提取不到返回 null(调用方回落工具名)。 */
|
|
47
|
+
export function formatToolSummary(tool: string, input: unknown): string | null {
|
|
48
|
+
if (input === null || input === undefined) return null;
|
|
49
|
+
if (typeof input !== "object") return typeof input === "string" ? oneLine(input) : null;
|
|
50
|
+
const o = input as Record<string, unknown>;
|
|
51
|
+
const str = (k: string): string | undefined => (typeof o[k] === "string" && (o[k] as string).trim() ? (o[k] as string).trim() : undefined);
|
|
52
|
+
const path = str("path") ?? str("file_path") ?? str("filePath") ?? str("notebook_path");
|
|
53
|
+
const cmd = str("command") ?? str("cmd");
|
|
54
|
+
const pattern = str("pattern") ?? str("query") ?? str("q");
|
|
55
|
+
const url = str("url");
|
|
56
|
+
const desc = str("description") ?? str("prompt") ?? str("task");
|
|
57
|
+
const cap = (s: string): string => (s.length > 80 ? `${s.slice(0, 80)}…` : s);
|
|
58
|
+
const verb = tool.charAt(0).toUpperCase() + tool.slice(1);
|
|
59
|
+
switch (tool) {
|
|
60
|
+
case "read":
|
|
61
|
+
return path ? `Read ${cap(path)}` : null;
|
|
62
|
+
case "glob":
|
|
63
|
+
return pattern ? `Glob ${cap(pattern)}${path ? ` in ${cap(path)}` : ""}` : path ? `Glob ${cap(path)}` : null;
|
|
64
|
+
case "grep":
|
|
65
|
+
return pattern ? `Grep ${cap(pattern)}${path ? ` in ${cap(path)}` : ""}` : path ? `Grep ${cap(path)}` : null;
|
|
66
|
+
case "edit":
|
|
67
|
+
case "write":
|
|
68
|
+
return path ? `${tool === "edit" ? "Edit" : "Write"} ${cap(path)}` : null;
|
|
69
|
+
case "bash":
|
|
70
|
+
case "terminal":
|
|
71
|
+
return cmd ? `Bash ${cap(oneLine(cmd))}` : null;
|
|
72
|
+
case "webfetch":
|
|
73
|
+
case "web_fetch":
|
|
74
|
+
return url ? `Fetch ${cap(url)}` : null;
|
|
75
|
+
case "task":
|
|
76
|
+
return desc ? `Task ${cap(oneLine(desc))}` : null;
|
|
77
|
+
default: {
|
|
78
|
+
if (path) return `${cap(tool)} ${cap(path)}`;
|
|
79
|
+
if (cmd) return `${cap(tool)} ${cap(oneLine(cmd))}`;
|
|
80
|
+
if (url) return `${cap(tool)} ${cap(url)}`;
|
|
81
|
+
if (desc) return `${cap(tool)} ${cap(oneLine(desc))}`;
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function oneLine(s: string): string {
|
|
88
|
+
return s.replace(/\s+/g, " ").trim();
|
|
89
|
+
}
|