💄 style(builtin-tool): switch Task inspector copy by phase (#15104)

Inspector chips stay in chat history, so a settled TaskCreate row that still reads "Creating task" looks like the call is still running. Split lobe-claude-code task labels into .loading / .completed pairs and pick based on isArgumentsStreaming || isLoading. Documented the rule in the builtin-tool ui skill so new tools follow the same convention.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Arvin Xu
2026-05-22 15:12:21 +08:00
committed by GitHub
parent 875e2ffb87
commit a27ea18dfb
5 changed files with 58 additions and 41 deletions
+13 -11
View File
@@ -25,17 +25,18 @@ The two reference tools to read end-to-end:
1. **先保证折叠态可读。** 每个 API 都必须有 Inspector;用户不展开也应该能看懂 “正在做什么 / 对什么做 / 当前结果是什么”。Inspector 不应该只展示函数名和原始参数。
2. **Inspector 是一句话,不是详情页。** 优先表达动作、关键对象、数量、状态,例如 “分析图片 3 张”“搜索 12 个结果”“读取 config.json”。长文本、列表和结构化结果放到 Render 或 Portal。
3. **Inspector 要覆盖执行生命周期。** `args` 还在 streaming、工具执行中、执行完成、执行失败时都应该有稳定展示;必要时同时读取 `args``partialArgs``pluginState`,避免出现空白、跳变或只显示半截参数。
4. **只有结构化结果才需要 Render。** 如果工具结果只是自然语言总结,通常不需要 Render;如果结果包含列表、媒体、文件、表格、代码、diff、地图、时间线、权限请求等结构,就应该提供 Render
5. **Render 要帮助用户检查结果,而不是复述参数。** Render 的主体应该围绕工具产物组织:可预览、可比较、可筛选、可定位。参数只作为上下文辅助出现,不要把 Render 做成一块更大的 args dump
6. **参数和结果要一起参与渲染。** 好的 Tool UI 通常同时用 `args` 解释意图,用 `pluginState` 展示真实执行结果;但 `pluginState` 只放结果域数据,不要反向塞入可以从 `args` 推导出的内容
7. **慢操作要有 Placeholder。** 如果工具通常需要等待网络、文件系统、模型或外部进程,Placeholder 应该先占住最终 Render 的版式,让用户知道即将看到什么,而不是只显示一个泛化 loading
8. **Streaming 只用于连续产物。** 搜索列表、日志、长文本、文件分析、分阶段计划适合 Streaming;一次性小结果不需要强行做 Streaming。Streaming UI 要能渐进追加,并且完成后自然过渡到最终 Render
9. **有风险的动作必须 Intervention。** 写文件、删除、发送、安装、执行命令、外部可见操作、权限敏感操作,都应该在执行前给出可理解的确认界面;确认文案要说明影响范围,而不是只问 “是否继续”
10. **错误、空态和截断都是正式状态。** Render 不能在失败、无结果、超长结果时退化成空白。错误要说明发生在哪一步;空态要告诉用户没有产物;超长内容要明确 “展示前 N 项 / 还有 N 项”。
11. **信息密度要克制。** 默认展示最有判断价值的部分:标题、来源、状态、摘要、少量关键字段。大对象、长列表、原文、调试数据放进可展开区域或 Portal,避免把聊天流撑成后台管理页
12. **视觉上融入聊天流。** Tool UI 应该使用 `@lobehub/ui` / base-ui、`Flexbox``createStaticStyles``cssVar.*`,遵循现有间距、圆角、颜色、字号;不要为单个工具发明一套独立视觉语言
13. **Devtools fixture 是验收入口** 新增或修改 Tool UI 时,应在 `/devtools` 里准备覆盖典型态、loading/streaming、空态、错误态、长内容态的 fixture;一个 API 如果在真实聊天里会出现,就不应该在 devtools 中缺席
14. **先做用户会看的 UI,再做调试 UI。** Raw JSON、trace、schema、内部 id 可以存在,但应默认收起或放到调试区;主界面先回答用户最关心的问题:工具做了什么,结果值不值得信任,下一步能做什么
4. **文案要随状态切换时态。** 同一个动作在 loading 与 completed 两个阶段必须用不同的措辞:执行中用现在进行时(“正在创建任务 / Creating task / 正在搜索”),执行完成后切到完成态(“已创建任务 / Task created / 已找到 N 条”)。Inspector chip 会一直留在聊天记录里——如果一直挂着 “正在 xxx”,几小时后回看历史时会读起来像还在跑。约定的 i18n 形式是 `<api>.loading` / `<api>.completed` 一对键(见 `lobe-agent.apiName.callSubAgent.{loading,completed}``lobe-claude-code.task.{create,list,update,get}.{loading,completed}`),渲染时按 `isArgumentsStreaming || isLoading` 决定取哪一个。只读 / 查询类(“查看任务”这种本来就是名词性的)可以共用一个键
5. **只有结构化结果才需要 Render。** 如果工具结果只是自然语言总结,通常不需要 Render;如果结果包含列表、媒体、文件、表格、代码、diff、地图、时间线、权限请求等结构,就应该提供 Render
6. **Render 要帮助用户检查结果,而不是复述参数。** Render 的主体应该围绕工具产物组织:可预览、可比较、可筛选、可定位。参数只作为上下文辅助出现,不要把 Render 做成一块更大的 args dump
7. **参数和结果要一起参与渲染。** 好的 Tool UI 通常同时用 `args` 解释意图,用 `pluginState` 展示真实执行结果;但 `pluginState` 只放结果域数据,不要反向塞入可以从 `args` 推导出的内容
8. **慢操作要有 Placeholder。** 如果工具通常需要等待网络、文件系统、模型或外部进程,Placeholder 应该先占住最终 Render 的版式,让用户知道即将看到什么,而不是只显示一个泛化 loading
9. **Streaming 只用于连续产物。** 搜索列表、日志、长文本、文件分析、分阶段计划适合 Streaming;一次性小结果不需要强行做 Streaming。Streaming UI 要能渐进追加,并且完成后自然过渡到最终 Render
10. **有风险的动作必须 Intervention。** 写文件、删除、发送、安装、执行命令、外部可见操作、权限敏感操作,都应该在执行前给出可理解的确认界面;确认文案要说明影响范围,而不是只问 “是否继续”。
11. **错误、空态和截断都是正式状态。** Render 不能在失败、无结果、超长结果时退化成空白。错误要说明发生在哪一步;空态要告诉用户没有产物;超长内容要明确 “展示前 N 项 / 还有 N 项”
12. **信息密度要克制。** 默认展示最有判断价值的部分:标题、来源、状态、摘要、少量关键字段。大对象、长列表、原文、调试数据放进可展开区域或 Portal,避免把聊天流撑成后台管理页
13. **视觉上融入聊天流** Tool UI 应该使用 `@lobehub/ui` / base-ui、`Flexbox``createStaticStyles``cssVar.*`,遵循现有间距、圆角、颜色、字号;不要为单个工具发明一套独立视觉语言
14. **Devtools fixture 是验收入口。** 新增或修改 Tool UI 时,应在 `/devtools` 里准备覆盖典型态、loading/streaming、空态、错误态、长内容态的 fixture;一个 API 如果在真实聊天里会出现,就不应该在 devtools 中缺席
15. **先做用户会看的 UI,再做调试 UI。** Raw JSON、trace、schema、内部 id 可以存在,但应默认收起或放到调试区;主界面先回答用户最关心的问题:工具做了什么,结果值不值得信任,下一步能做什么。
---
@@ -200,6 +201,7 @@ export default SearchInspector;
- Read both `args?.X` and `partialArgs?.X` together — `args` is final, `partialArgs` is in-stream.
- Use chips/tags for distinct facets (identifier, name, parent, status, count). Each chip should clip with `text-overflow: ellipsis` and have a `max-width` so long values don't blow out the chat bubble.
- Append `pluginState`-derived suffixes only **after** loading finishes — count or "(no results)" should not appear while still searching.
- **Switch copy by phase.** If the verb implies an ongoing action ("Creating", "Searching", "Listing"), define `<api>.loading` and `<api>.completed` keys and select via `isArgumentsStreaming || isLoading ? loadingKey : completedKey`. Inspector chips persist in chat history — leaving "Creating task" frozen on a finished call reads as if the tool is still running. Read-only labels that are already noun-form ("View task") can keep a single key. See `CallSubAgentInspector` for the canonical two-key pattern.
### Inspector registry — `client/Inspector/index.ts`
+7 -4
View File
@@ -90,13 +90,16 @@
"builtins.lobe-agent.title": "Lobe Agent",
"builtins.lobe-claude-code.agent.instruction": "Instruction",
"builtins.lobe-claude-code.agent.result": "Result",
"builtins.lobe-claude-code.task.createLabel": "Creating task: ",
"builtins.lobe-claude-code.task.getLabel": "Inspecting task #{{taskId}}",
"builtins.lobe-claude-code.task.listLabel": "Listing tasks",
"builtins.lobe-claude-code.task.create.completed": "Task created: ",
"builtins.lobe-claude-code.task.create.loading": "Creating task: ",
"builtins.lobe-claude-code.task.getLabel": "View task #{{taskId}}",
"builtins.lobe-claude-code.task.list.completed": "Tasks listed",
"builtins.lobe-claude-code.task.list.loading": "Listing tasks",
"builtins.lobe-claude-code.task.update.completed": "Task #{{taskId}} updated",
"builtins.lobe-claude-code.task.update.loading": "Updating task #{{taskId}}",
"builtins.lobe-claude-code.task.updateCompleted": "Completed",
"builtins.lobe-claude-code.task.updateDeleted": "Deleted",
"builtins.lobe-claude-code.task.updateInProgress": "Started",
"builtins.lobe-claude-code.task.updateLabel": "Updating task #{{taskId}}",
"builtins.lobe-claude-code.task.updatePending": "Reset",
"builtins.lobe-claude-code.todoWrite.allDone": "All tasks completed",
"builtins.lobe-claude-code.todoWrite.currentStep": "Current step",
+6 -3
View File
@@ -90,13 +90,16 @@
"builtins.lobe-agent.title": "Lobe Agent",
"builtins.lobe-claude-code.agent.instruction": "指令",
"builtins.lobe-claude-code.agent.result": "结果",
"builtins.lobe-claude-code.task.createLabel": "正在创建任务:",
"builtins.lobe-claude-code.task.create.completed": "创建任务:",
"builtins.lobe-claude-code.task.create.loading": "正在创建任务:",
"builtins.lobe-claude-code.task.getLabel": "查看任务 #{{taskId}}",
"builtins.lobe-claude-code.task.listLabel": "正在列出任务",
"builtins.lobe-claude-code.task.list.completed": "列出任务",
"builtins.lobe-claude-code.task.list.loading": "正在列出任务",
"builtins.lobe-claude-code.task.update.completed": "已更新任务 #{{taskId}}",
"builtins.lobe-claude-code.task.update.loading": "正在更新任务 #{{taskId}}",
"builtins.lobe-claude-code.task.updateCompleted": "已完成",
"builtins.lobe-claude-code.task.updateDeleted": "已删除",
"builtins.lobe-claude-code.task.updateInProgress": "开始执行",
"builtins.lobe-claude-code.task.updateLabel": "正在更新任务 #{{taskId}}",
"builtins.lobe-claude-code.task.updatePending": "重置为待办",
"builtins.lobe-claude-code.todoWrite.allDone": "全部任务已完成",
"builtins.lobe-claude-code.todoWrite.currentStep": "当前步骤",
@@ -176,18 +176,20 @@ export const TaskInspector = memo<BuiltinInspectorProps<TaskInspectorArgs, TaskP
// pluginState snapshot; the trailing `completed/total` chip makes the
// accumulation visible across rows (the ring alone stays empty while
// every new task is still `todo`, so total wouldn't otherwise show).
// Verb flips to past tense once the call finishes — the chip persists
// in chat history and "Creating task" frozen on a settled row reads as
// if it's still running (see ui.md principle 4).
if (apiName === ClaudeCodeApiName.TaskCreate) {
const subject = ((args || partialArgs) as TaskCreateArgs | undefined)?.subject;
const text = subject
? `${t('builtins.lobe-claude-code.task.createLabel')}${subject}`
: t('builtins.lobe-claude-code.task.createLabel');
const inFlight = isArgumentsStreaming || isLoading;
const label = t(
inFlight
? 'builtins.lobe-claude-code.task.create.loading'
: 'builtins.lobe-claude-code.task.create.completed',
);
const text = subject ? `${label}${subject}` : label;
return (
<div
className={cx(
inspectorTextStyles.root,
(isArgumentsStreaming || isLoading) && shinyTextStyles.shinyText,
)}
>
<div className={cx(inspectorTextStyles.root, inFlight && shinyTextStyles.shinyText)}>
<ProgressRing stats={stats} />
{stats.total > 0 && (
<span className={styles.countChip}>
@@ -253,22 +255,26 @@ export const TaskInspector = memo<BuiltinInspectorProps<TaskInspectorArgs, TaskP
// TodoWriteInspector's `isArgumentsStreaming && stats.total === 0` branch.
if (stats.total === 0) {
const resolvedArgs = (args || partialArgs) as TaskInspectorArgs | undefined;
const inFlight = isArgumentsStreaming || isLoading;
const fallback = (() => {
if (apiName === ClaudeCodeApiName.TaskUpdate) {
const taskId = (resolvedArgs as TaskUpdateArgs | undefined)?.taskId;
return taskId
? t('builtins.lobe-claude-code.task.updateLabel', { taskId })
: t('builtins.lobe-claude-code.todoWrite.todos');
if (!taskId) return t('builtins.lobe-claude-code.todoWrite.todos');
return t(
inFlight
? 'builtins.lobe-claude-code.task.update.loading'
: 'builtins.lobe-claude-code.task.update.completed',
{ taskId },
);
}
return t('builtins.lobe-claude-code.task.listLabel');
return t(
inFlight
? 'builtins.lobe-claude-code.task.list.loading'
: 'builtins.lobe-claude-code.task.list.completed',
);
})();
return (
<div
className={cx(
inspectorTextStyles.root,
(isArgumentsStreaming || isLoading) && shinyTextStyles.shinyText,
)}
>
<div className={cx(inspectorTextStyles.root, inFlight && shinyTextStyles.shinyText)}>
{fallback}
</div>
);
+7 -4
View File
@@ -62,13 +62,16 @@ export default {
'builtins.lobe-agent.title': 'Lobe Agent',
'builtins.lobe-claude-code.agent.instruction': 'Instruction',
'builtins.lobe-claude-code.agent.result': 'Result',
'builtins.lobe-claude-code.task.createLabel': 'Creating task: ',
'builtins.lobe-claude-code.task.getLabel': 'Inspecting task #{{taskId}}',
'builtins.lobe-claude-code.task.listLabel': 'Listing tasks',
'builtins.lobe-claude-code.task.create.completed': 'Task created: ',
'builtins.lobe-claude-code.task.create.loading': 'Creating task: ',
'builtins.lobe-claude-code.task.getLabel': 'View task #{{taskId}}',
'builtins.lobe-claude-code.task.list.completed': 'Tasks listed',
'builtins.lobe-claude-code.task.list.loading': 'Listing tasks',
'builtins.lobe-claude-code.task.update.completed': 'Task #{{taskId}} updated',
'builtins.lobe-claude-code.task.update.loading': 'Updating task #{{taskId}}',
'builtins.lobe-claude-code.task.updateCompleted': 'Completed',
'builtins.lobe-claude-code.task.updateDeleted': 'Deleted',
'builtins.lobe-claude-code.task.updateInProgress': 'Started',
'builtins.lobe-claude-code.task.updateLabel': 'Updating task #{{taskId}}',
'builtins.lobe-claude-code.task.updatePending': 'Reset',
'builtins.lobe-claude-code.todoWrite.allDone': 'All tasks completed',
'builtins.lobe-claude-code.todoWrite.currentStep': 'Current step',