@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,6 +1,6 @@
1
1
  {
2
2
  "name": "@zmzai/theme",
3
- "version": "0.9.7",
3
+ "version": "0.9.8",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "zmzai 全品牌设计系统 — Radix UI + framer-motion + Tailwind v4 + MiSans",
@@ -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>思考过程</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 title = state.status === "completed" ? state.title : state.status === "running" ? (state.title ?? call.tool) : call.tool;
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.status === "completed"
79
- ? (single.state.title ?? single.tool)
80
- : single.state.status === "error"
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
+ }