# 对话工具调用

## 使用方法

`ChatToolCalls` 用来显示大模型这一轮都调了哪些工具、函数。传一组调用进去就行：只有一条时直接排成一行；有好几条时收成一个摘要，最近调的那条露在外面。凡是 AI 助手要交代自己干了什么的地方都能用。

数组为空时什么都不渲染。多条会收成一行表头，表头上显示最近那次调用；点一下就在摘要和完整列表之间切换。`v-model:is-expanded` 控制展开状态，`default-is-expanded` 是没绑 `v-model` 时的初始状态。展开后 `label` 会替掉本地化的调用条数。某条调用带 `resultDetail`（或它的别名 `detail`），或者你给了 `detail` 插槽，它自己还能再展开。

```vue
<script setup lang="ts">
import { ChatMessage, ChatToolCalls } from '@astryx-vue/core/Chat'

const calls = [
  { name: 'bash', status: 'complete' as const, target: 'git status', duration: '1.2s' },
]
</script>

<template>
  <ChatMessage sender="assistant">
    <ChatToolCalls :calls="calls" />
  </ChatMessage>
</template>
```

### 结构

- **状态图标**: 跟着主题走的成功或失败图标；调用还在等待或运行中时换成加载动画。
- **工具名**: 函数或工具的名字，用等宽字体显示，例如 bash、edit、read、web_search。
- **环境小条**: 一个灰色小圆角条，标出这次调用跑在哪个沙箱或环境里，例如 cli:remote-server 或 workspace。
- **目标文字**: 这次动作的目标，例如文件路径、命令或搜索词，排在工具名后面。
- **增删行数**: 修改类操作的绿色新增数和红色删除数，跟在目标后面。
- **耗时**: 已完成的调用在行尾显示的执行时间。
- **组表头**: 有一条以上调用时出现，带扳手图标和条数。点一下就在摘要和完整列表之间切换。

## 建议

| 建议 | 做法 |
| --- | --- |
| 推荐 | 每次调用都带上 target，让人看得出工具动了什么：文件路径、shell 命令，或者搜索词。 |
| 推荐 | 已完成的调用显示耗时，用户才知道哪个工具慢、这次回答为什么久。 |
| 推荐 | 自动生成的条数说不清这组调用在干什么时，用 `label` 自己写一句。 |
| 推荐 | 会产出结果的调用，用 `detail` 插槽配一个代码块（改动的 diff、命令的终端输出），用户就能就地查看。 |
| 推荐 | 流式追加时给每条调用一个唯一的 `key`，Vue 才能只给新行做动画，不会把已完成的行拆掉重建。 |
| 避免 | 省掉 status。不写默认算完成，对还在跑或者已经失败的调用是误导。 |
| 避免 | 把工具调用摆在对话消息之外。它本来就该待在助手消息里，不是独立的界面。 |
| 避免 | 给每条调用自己包一层容器。一条还是多条，组件会按数组长度自己决定排版。 |

## 示例

常见的用法和不同的状态。

### 可展开

每条调用都能展开看结果，diff 和命令输出用代码块显示。点一行就展开。

```vue
<script setup lang="ts">
import type { ChatToolCallItem } from '@astryx-vue/core/Chat'
import { ChatMessage, ChatMessageList, ChatToolCalls } from '@astryx-vue/core/Chat'
import { computed } from 'vue'

const calls = computed<ChatToolCallItem[]>(() => [
  {
    name: 'edit',
    status: 'complete',
    node: 'cli:remote-server',
    target: 'src/utils/formatDate.ts',
    additions: 6,
    deletions: 3,
    duration: '85ms',
    resultDetail: 'diff',
  },
  {
    name: 'bash',
    status: 'complete',
    node: 'cli:remote-server',
    target: 'yarn test',
    duration: '1.8s',
    resultDetail: 'ok',
  },
  {
    name: 'web_search',
    status: 'complete',
    target: 'Intl.DateTimeFormat locale options',
    duration: '1.2s',
    resultDetail: 'ok',
  },
])

const diff = `function formatDate(value) {
-  return value.toLocaleDateString();
+  return new Intl.DateTimeFormat(locale).format(value);
}`
</script>

<template>
  <ChatMessageList>
    <ChatMessage sender="assistant">
      <ChatToolCalls :calls="calls" default-is-expanded>
        <template #detail="{ call }">
          <pre class="chat-preview__code">{{ call.name === 'edit' ? diff : 'passed' }}</pre>
        </template>
      </ChatToolCalls>
    </ChatMessage>
  </ChatMessageList>
</template>
```

### 简单

上面是一条单独的工具调用，下面是一组可收起的多条调用，带增删行数。两种排版放在一起对比。

```vue
<script setup lang="ts">
import type { ChatToolCallItem } from '@astryx-vue/core/Chat'
import { ChatMessage, ChatMessageList, ChatToolCalls } from '@astryx-vue/core/Chat'
import { computed } from 'vue'

const single = computed<ChatToolCallItem[]>(() => [
  { name: 'bash', status: 'complete', target: 'git status', duration: '1.2s' },
])

const group = computed<ChatToolCallItem[]>(() => [
  { name: 'read', status: 'complete', target: 'src/components/DataGrid.tsx', duration: '30ms' },
  { name: 'edit', status: 'complete', target: 'src/components/DataGrid.tsx', additions: 24, deletions: 8, duration: '85ms' },
  { name: 'edit', status: 'complete', target: 'src/components/DataGrid.test.tsx', additions: 45, duration: '60ms' },
])
</script>

<template>
  <ChatMessageList>
    <ChatMessage sender="assistant">
      <ChatToolCalls :calls="single" />
      <ChatToolCalls :calls="group" default-is-expanded />
    </ChatMessage>
  </ChatMessageList>
</template>
```

### 状态

四种状态（等待中、运行中、已完成、出错）放在同一组里一起看。

```vue
<script setup lang="ts">
import type { ChatToolCallItem } from '@astryx-vue/core/Chat'
import { ChatMessage, ChatMessageList, ChatToolCalls } from '@astryx-vue/core/Chat'
import { computed } from 'vue'

const calls = computed<ChatToolCallItem[]>(() => [
  { name: 'bash', status: 'pending', target: 'yarn build' },
  { name: 'bash', status: 'running', target: 'yarn test' },
  { name: 'edit', status: 'complete', target: 'src/App.tsx', additions: 8, deletions: 2, duration: '120ms' },
  {
    name: 'bash',
    status: 'error',
    target: 'yarn lint',
    errorMessage: '3 lint errors found',
  },
])
</script>

<template>
  <ChatMessageList>
    <ChatMessage sender="assistant">
      <ChatToolCalls :calls="calls" default-is-expanded />
    </ChatMessage>
  </ChatMessageList>
</template>
```

## 属性

| 属性 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `calls` | `ChatToolCallItem[]` | — | 助手这一轮的全部工具调用，必填。空数组什么都不渲染。每项可以有 `name`、`status`、`target`、`duration`、`node`、`additions`、`deletions`、`stats`、`errorMessage`、`key`、`resultDetail`、`data`。 |
| `label` | `string` | 本地化的条数 | 一组展开后，表头上显示的文字。 |
| `isExpanded` | `boolean` | — | 受控的展开状态，用 `v-model:is-expanded` 绑定。 |
| `defaultIsExpanded` | `boolean` | `false` | 没绑 `isExpanded` 时用的初始展开状态。 |

## 事件

- `expandedChange(isExpanded: boolean)`：表头请求切换到的状态。用 `v-model:is-expanded` 时还会发 `update:isExpanded`。

## 插槽

- `detail`：给某一条调用画更丰富的结果，插槽参数是 `{ call }`。不传就用 `call.resultDetail` 的纯文本。

## 暴露

- `element`：根元素。

## 类型

`ChatToolCallItem`：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `name` | `string` | 工具或函数名，用等宽字体显示。必填。 |
| `status` | `'pending' \| 'running' \| 'complete' \| 'error'` | 执行状态。不写就当已完成。 |
| `target` | `string` | 工具动的是什么，例如文件路径、命令或者搜索词。 |
| `duration` | `string` | 耗时，排在行尾，例如 `1.2s`。 |
| `node` | `string` | 沙箱或 worker 的名字，显示成一个灰色小条。 |
| `additions` | `number` | 新增的行数或字符数，显示成 `+12`。 |
| `deletions` | `number` | 删除的行数或字符数，显示成 `-3`。 |
| `stats` | `string` | 跟在后面的一段纯文本。 |
| `errorMessage` | `string` | `status` 是 `error` 时的错误说明。会读给读屏，也用作状态图标的提示。 |
| `key` | `string` | 稳定标识。不写就按其它字段推出来。 |
| `resultDetail` | `string` | 展开这一行后显示的细节，例如 diff 或命令输出。有这个字段的行才能展开。要放富文本请用 `detail` 插槽。 |
| `detail` | `string` | `resultDetail` 的别名。两个都写时以 `resultDetail` 为准。 |
| `data` | `unknown` | 你自己挂在调用上的任意数据，不会渲染出来。 |
