---
title: AgentChat 智能体对话
description: 带权限闸门、实时计划与改动面板的编码智能体会话
category: TemplateAi
status: beta
since: 0.6.0
tags: [template, ai, agent, chat, tool-call]
syncStatus: reviewed
verified: false
---

## 用法

### 编码智能体工作台
:::TuffDemoWrapper{demo="TemplateAgentChatDemo" code-lang="vue" description="进入视口后自动演示，写文件前停在权限闸门等你确认。"}
---
code: |
  <script setup lang="ts">
  import { reactive } from 'vue'
  import TemplateFrame from './TemplateFrame.vue'

  // 会话项只存 id 和 kind，显示的内容从响应式状态里读
  const state = reactive({
    items: [{ id: 'request', kind: 'user' }, { id: 'tail', kind: 'tail' }],
    gate: 'waiting',
  })

  // 新行插在固定的 tail 前面，已有的行不会换位置、不会被重建
  function push(kind: string) {
    state.items.splice(state.items.length - 1, 0, { id: kind, kind })
  }

  // 没有任何计时器会调用它：写文件只等读者点击
  function onAllow() {
    state.gate = 'allowed'
    push('code')
  }
  </script>

  <template>
    <TemplateFrame title="AgentChat 智能体对话" @enter="run">
      <template #default="{ width }">
        <div class="agent-chat">
          <TxAgentsList v-if="width >= 1200" :agents="agents" :selected-id="agentId" @select="agentId = $event" />
          <main class="agent-chat__main">
            <header class="agent-chat__head">
              <TxThinkingOrb :state="orbState" :label="phaseLabel" :paused="!running" />
              <TxWorkingIndicator v-if="working" :label="phaseLabel" :started-at="startedAt" />
              <TxContextIndicator :used-tokens="tokens" :max-tokens="200000" />
            </header>

            <TxConversationStream :items="state.items" item-key="id" :overscan="24" :streaming="working">
              <template #item="{ item }">
                <TxChatMessage v-if="item.kind === 'user'" :message="request" :markdown="false" />
                <TxAgentTrace v-else-if="item.kind === 'plan'" :rows="planRows" :working="planning" />
                <TxApprovalCard
                  v-else-if="item.kind === 'ask'"
                  v-model="answers" v-model:index="page" :questions="questions"
                  @answer="stopAutoPick"
                />
                <TxToolCallCard v-else-if="item.kind === 'search'" :tool-call="searchCall" default-expanded />
                <TxToolConfirmation
                  v-else-if="item.kind === 'gate' && state.gate === 'waiting'"
                  tool-name="apply_patch" risk="write" :input="patchFiles"
                  @approve="onAllow" @deny="onDeny"
                />
                <TxCodeStream v-else-if="item.kind === 'code'" :code="code" :diff="diff" :revealed-lines="lines" lang="ts" />
                <TxStreamMarkdown v-else-if="item.kind === 'summary'" :content="summary" :streaming="streaming" />
              </template>
            </TxConversationStream>

            <TxPromptBar v-model="draft" :sources="sources" :commands="commands" :models="models" :submitting="running" @send="send">
              <template #actions>
                <TxIconButton v-if="running" icon="i-carbon-stop-filled-alt" label="停止" @click="stop" />
              </template>
            </TxPromptBar>
          </main>

          <aside v-if="width >= 640" class="agent-chat__side">
            <TxTaskRows :rows="tasks" done-text="" />
            <TxDiffChips :diffs="diffs" @select="jumpToCode" />
            <TxAgentScreen :state="screen" :cursor="cursor" label="插件预览 · CoreBox" />
          </aside>
        </div>
      </template>
    </TemplateFrame>
  </template>
---
:::

### 侧边 Copilot
给已有页面（文档编辑器、CMS 正文、工单回复）配的停靠助手，只给出由你决定是否应用的建议。
:::TuffDemoWrapper{demo="TemplateAgentChatCopilotDemo" code-lang="vue" description="自动演示一次润色并停在建议上，也可在文章里划词。回答均为预写。"}
---
code: |
  <script setup lang="ts">
  import type { SelectionPayload } from '@talex-touch/tuffex/selection-actions'
  import { useSelectionAnchor } from '@talex-touch/tuffex/selection-actions'
  import { markRaw, nextTick, onMounted, ref } from 'vue'
  import TemplateFrame from './TemplateFrame.vue'

  const articleRef = ref<HTMLElement | null>(null)
  const barRef = ref()
  const { selection } = useSelectionAnchor({ root: articleRef, ignore: () => [barRef.value?.el] })

  // 浮条每次重新定位都会读 rects：从 Range 现量，页面或文章滚动时它才贴着文字
  function livePayload(text: string, range: Range): SelectionPayload {
    return markRaw({ text, range, get rects() { return Array.from(range.getClientRects()) } })
  }

  // 没有滚动事件的布局变化（收起侧栏、展开舞台、文字重排）要请它重新定位
  onMounted(() => {
    new ResizeObserver(() => nextTick(() => barRef.value?.updatePosition())).observe(articleRef.value!)
  })

  // 只有读者点「应用」才会改动页面
  function apply(card: Card) {
    texts[card.blockId] = card.body
    card.phase = 'applied'
  }
  </script>

  <template>
    <TemplateFrame title="侧边 Copilot" :height="560" @enter="run">
      <template #default="{ width }">
        <article ref="articleRef" class="copilot__page">
          <p v-for="block in blocks" :key="block.id" :data-block="block.id">{{ texts[block.id] }}</p>
        </article>

        <aside v-if="width >= 640" class="copilot__side">
          <TxThinkingOrb :state="orbState" :paused="!working" />
          <TxModeChip :label="readonly ? '只读 · 仅解释' : '编辑需确认'" @click="readonly = !readonly" />
          <TxTag v-if="target" label="选区" closable @close="dismiss" />
          <div v-for="card in cards" :key="card.id">
            <TxChatMessage :message="echo(card)" :markdown="false" />
            <TxTypingIndicator v-if="card.phase === 'thinking'" text="正在改写…" />
            <template v-else>
              <del>{{ card.original }}</del>
              <ins>{{ card.body.slice(0, card.shown) }}</ins>
              <TxButton variant="primary" size="sm" @click="apply(card)">应用到页面</TxButton>
              <TxDiffChips :diffs="[{ file: 'README.md', add: 1, del: 1 }]" @select="reveal(card.blockId)" />
            </template>
          </div>
          <TxSuggestionChips :suggestions="next" @select="runSuggestion" />
          <TxPromptBar v-model="draft" :sources="sources" :commands="commands" @send="send" />
        </aside>

        <TxSelectionActions
          ref="barRef" :selection="payload" :state="barState" :actions="actions" keep-label="应用"
          @action="run($event.id)" @keep="apply(activeCard)" @discard="discard(activeCard)" @retry="retry(activeCard)"
        >
          <template #action-icon="{ action }">
            <span :class="icons[action.id]" />
          </template>
        </TxSelectionActions>
      </template>
    </TemplateFrame>
  </template>
---
:::

## 概述

编码智能体处理一条需求的完整会话：澄清问题由人回答，写文件由人放行。

- 进入视口后自动演示，停在写权限闸门；减少动态效果时直接显示到闸门为止的状态。
- 澄清问题闲置 3 秒后按推荐项作答；点任意选项则改为等你提交。
- 写文件闸门从不自动放行：「允许」后写码、测试并总结；「拒绝」则跳过写入与测试，改为导出补丁。
- 「停止」把进行中的工具卡片标为已停止；点改动文件时，会话滚到对应代码，整页不动。
- 列内（640–959px）侧栏用「任务 / 变更 / 预览」切换，≥ 1200px 加智能体列表；窄屏（< 640px）收起侧栏。
- 侧边 Copilot：只有点「应用」才改动页面，可撤销；「只读 · 仅解释」下浮条只剩「解释」。
- 侧边 Copilot：划词浮条随选区滚动与重排；窄屏（< 640px）助手收进顶栏按钮，打开为底部面板。

## 自定义

- 接真实智能体时，用事件流替换演示时间轴；会话项只存 `id` 和 `kind`，流式事件只更新状态。
- 在 TxConversationStream 末尾保留固定的 tail 项，每条消息只挂载一次，卡片状态与入场动画不会重放。
- 权限按风险分级：`risk="read"` 直接执行并留痕，`write` / `execute` 一律等人确认。
- 澄清答案用受控的 `v-model` 与 `v-model:index` 持有；读者动过卡片后不再自动选推荐项。
- 侧边 Copilot：`selection.rects` 写成从 Range 现量的 getter，布局变化后调用 `updatePosition()`。

## 相关组件

| 区域 | 组件 | 作用 |
|---|---|---|
| 顶栏 | [TxThinkingOrb](/docs/dev/components/thinking-orb)、[TxWorkingIndicator](/docs/dev/components/working-indicator)、[TxStatusBadge](/docs/dev/components/status-badge)、[TxContextIndicator](/docs/dev/components/context-indicator) | 阶段、耗时与上下文用量 |
| 会话 | [TxConversationStream](/docs/dev/components/conversation-stream)、[TxChatMessage](/docs/dev/components/chat) | 虚拟化会话流与需求气泡 |
| 规划 | [TxAgentTrace](/docs/dev/components/agent-trace) | 计划逐行出现，完成后折叠 |
| 澄清 | [TxApprovalCard](/docs/dev/components/approval-card) | 两道澄清问题 |
| 工具调用 | [TxToolCallCard](/docs/dev/components/tool-call-card) | 输入、流式日志与结果 |
| 权限闸门 | [TxToolConfirmation](/docs/dev/components/tool-confirmation) | 写文件前的允许 / 拒绝 |
| 改动 | [TxCodeStream](/docs/dev/components/code-stream) | 逐行写出的 diff |
| 总结 | [TxTypingIndicator](/docs/dev/components/typing-indicator)、[TxStreamMarkdown](/docs/dev/components/stream-markdown)、[TxToolChips](/docs/dev/components/tool-chips)、[TxMessageActions](/docs/dev/components/message-actions) | 流式总结、工具摘要与操作 |
| 侧栏 | [TxFlatRadio](/docs/dev/components/flat-radio)、[TxTaskRows](/docs/dev/components/task-rows)、[TxDiffChips](/docs/dev/components/tool-chips)、[TxAgentScreen](/docs/dev/components/agent-screen)、[TxSkeleton](/docs/dev/components/skeleton) | 进度、改动文件与预览 |
| 智能体列表（≥ 1200px） | [TxAgentsList](/docs/dev/components/agents)、[TxSuggestionChips](/docs/dev/components/suggestion-chips) | 切换智能体与推荐提问 |
| 输入 | [TxPromptBar](/docs/dev/components/prompt-bar) | 引用、命令、模型与停止 |
| 宿主页面（侧边 Copilot） | 普通 DOM 段落 | 可划词的 README 正文 |
| 划词浮条（侧边 Copilot） | [TxSelectionActions](/docs/dev/components/selection-actions) | 解释、润色、翻译与应用 |
| 助手面板（侧边 Copilot） | [TxModeChip](/docs/dev/components/mode-chip)、[TxTag](/docs/dev/components/tag) | 编辑模式与可见范围 |
| 建议卡（侧边 Copilot） | [TxButton](/docs/dev/components/button)、[TxDiffChips](/docs/dev/components/tool-chips) | 删改对照、应用与放弃 |
| 改动提示（侧边 Copilot） | [TxToastPanel](/docs/dev/components/toast-panel) | 应用后的撤销提示 |
