# Think 块与正文分离、可折叠展示 — 实施计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 识别 assistant 消息中 `//` 标签,分离为可折叠思考块展示,不破坏历史数据。 **Architecture:** 新增纯函数解析器位于 `utils/`;chat store 用运行时 Map 记录流式观察到的时间戳;`MessageItem.vue` 用两条独立响应链(parsed computed + duration interval)渲染折叠区。 **Tech Stack:** Vue 3 Composition API, TypeScript (strict), Pinia, Naive UI, Vitest, SCSS。 **Spec:** `docs/superpowers/specs/2026-04-23-thinking-block-collapse-design.md` --- ## 文件结构 | 路径 | 角色 | |---|---| | `packages/client/src/utils/thinking-parser.ts` | **新建** 纯函数:`parseThinking`、`detectThinkingBoundary`、`countThinkingChars` | | `packages/client/src/stores/hermes/chat.ts` | **修改** 新增 `thinkingObservation` Map + getter + `switchSession` 清理 + `message.delta` 边界写入 | | `packages/client/src/components/hermes/chat/MessageItem.vue` | **修改** 新增 `.thinking-block` 渲染区 + 两条 computed/interval | | `packages/client/src/i18n/locales/{en,zh,de,es,fr,ja,ko,pt}.ts` | **修改** 新增 6 条 `chat.thinking*` key | | `tests/client/thinking-parser.test.ts` | **新建** 解析器单测 | | `tests/client/chat-store-thinking.test.ts` | **新建** store 观察态单测 | --- ## Task 1: 解析器骨架 + 第一个闭合标签测试 **Files:** - Create: `tests/client/thinking-parser.test.ts` - Create: `packages/client/src/utils/thinking-parser.ts` - [ ] **Step 1.1: 写首个失败测试(单个闭合 think)** ```ts // tests/client/thinking-parser.test.ts import { describe, it, expect } from 'vitest' import { parseThinking } from '@/utils/thinking-parser' describe('parseThinking', () => { it('splits a single closed block from body', () => { const r = parseThinking('innerbody', { streaming: false }) expect(r.segments).toEqual(['inner']) expect(r.body).toBe('body') expect(r.pending).toBeNull() expect(r.hasThinking).toBe(true) }) }) ``` - [ ] **Step 1.2: 运行测试确认失败** Run: `npx vitest run tests/client/thinking-parser.test.ts` Expected: FAIL — `Cannot find module '@/utils/thinking-parser'` - [ ] **Step 1.3: 实现最小骨架** ```ts // packages/client/src/utils/thinking-parser.ts export interface ParsedThinking { segments: string[] pending: string | null body: string hasThinking: boolean } export interface ParseOptions { streaming: boolean } const TAG_RE = /<(think|thinking|reasoning)>([\s\S]*?)<\/\1>/gi export function parseThinking(content: string, opts: ParseOptions): ParsedThinking { const segments: string[] = [] let pending: string | null = null let body = '' let lastIndex = 0 TAG_RE.lastIndex = 0 let m: RegExpExecArray | null while ((m = TAG_RE.exec(content)) !== null) { body += content.slice(lastIndex, m.index) segments.push(m[2]) lastIndex = m.index + m[0].length } const rest = content.slice(lastIndex) const openRe = /<(think|thinking|reasoning)>([\s\S]*)$/i const openMatch = rest.match(openRe) if (openMatch) { body += rest.slice(0, openMatch.index) if (opts.streaming) { pending = openMatch[2] } else { body += rest.slice(openMatch.index!) } } else { body += rest } return { segments, pending, body, hasThinking: segments.length > 0 || pending !== null, } } ``` - [ ] **Step 1.4: 运行测试确认通过** Run: `npx vitest run tests/client/thinking-parser.test.ts` Expected: PASS (1 test) - [ ] **Step 1.5: 提交** ```bash git add tests/client/thinking-parser.test.ts packages/client/src/utils/thinking-parser.ts git commit -m "feat(thinking-parser): 首个闭合 标签拆分 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>" ``` --- ## Task 2: 解析器:多段、变体标签、大小写、空输入 **Files:** - Modify: `tests/client/thinking-parser.test.ts` - [ ] **Step 2.1: 追加测试** ```ts it('collects multiple closed blocks in order', () => { const r = parseThinking('amidbend', { streaming: false }) expect(r.segments).toEqual(['a', 'b']) expect(r.body).toBe('midend') }) it('supports and variants', () => { const r = parseThinking('rbody', { streaming: false }) expect(r.segments).toEqual(['r']) expect(r.body).toBe('body') }) it('is case-insensitive on tag names', () => { const r = parseThinking('xyz', { streaming: false }) expect(r.segments).toEqual(['x', 'y']) expect(r.body).toBe('z') }) it('returns hasThinking=false and body unchanged for plain text', () => { const r = parseThinking('hello world', { streaming: false }) expect(r.hasThinking).toBe(false) expect(r.body).toBe('hello world') expect(r.segments).toEqual([]) }) it('returns hasThinking=false for empty content', () => { const r = parseThinking('', { streaming: false }) expect(r.hasThinking).toBe(false) expect(r.body).toBe('') }) ``` - [ ] **Step 2.2: 运行** Run: `npx vitest run tests/client/thinking-parser.test.ts` Expected: PASS (6 tests) - [ ] **Step 2.3: 提交** ```bash git add tests/client/thinking-parser.test.ts git commit -m "test(thinking-parser): 覆盖多段/变体标签/大小写/空输入 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>" ``` --- ## Task 3: 流式未闭合 + 终止态降级 **Files:** - Modify: `tests/client/thinking-parser.test.ts` - [ ] **Step 3.1: 追加测试** ```ts it('treats trailing unclosed tag as pending when streaming', () => { const r = parseThinking('bodyin-progress', { streaming: true }) expect(r.pending).toBe('in-progress') expect(r.body).toBe('body') expect(r.segments).toEqual([]) expect(r.hasThinking).toBe(true) }) it('degrades trailing unclosed tag to body when NOT streaming (terminal state)', () => { const r = parseThinking('bodyorphan', { streaming: false }) expect(r.pending).toBeNull() expect(r.body).toBe('bodyorphan') expect(r.segments).toEqual([]) expect(r.hasThinking).toBe(false) }) it('combines closed segments with trailing pending (streaming)', () => { const r = parseThinking('donemidnow', { streaming: true }) expect(r.segments).toEqual(['done']) expect(r.pending).toBe('now') expect(r.body).toBe('mid') }) ``` - [ ] **Step 3.2: 运行 + 提交** Run: `npx vitest run tests/client/thinking-parser.test.ts` Expected: PASS (9 tests) ```bash git add tests/client/thinking-parser.test.ts git commit -m "test(thinking-parser): 流式 pending 与终止态降级 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>" ``` --- ## Task 4: 代码块保护(fenced + inline) **Files:** - Modify: `tests/client/thinking-parser.test.ts` - Modify: `packages/client/src/utils/thinking-parser.ts` - [ ] **Step 4.1: 写失败测试** ```ts it('does NOT recognize inside fenced code block', () => { const src = 'before\n```\nfake\n```\nafter' const r = parseThinking(src, { streaming: false }) expect(r.hasThinking).toBe(false) expect(r.body).toBe(src) }) it('does NOT recognize inside tilde-fenced code block', () => { const src = '~~~\nfake\n~~~' const r = parseThinking(src, { streaming: false }) expect(r.hasThinking).toBe(false) expect(r.body).toBe(src) }) it('does NOT recognize inside inline code', () => { const src = 'the tag `x` is a literal' const r = parseThinking(src, { streaming: false }) expect(r.hasThinking).toBe(false) expect(r.body).toBe(src) }) it('parses real outside code blocks even when code blocks contain fake ones', () => { const src = 'realtext\n```\nfake\n```' const r = parseThinking(src, { streaming: false }) expect(r.segments).toEqual(['real']) expect(r.body).toBe('text\n```\nfake\n```') }) ``` - [ ] **Step 4.2: 运行确认失败** Run: `npx vitest run tests/client/thinking-parser.test.ts` Expected: FAIL on 4 new tests - [ ] **Step 4.3: 重构实现加入代码块保护** 替换 `packages/client/src/utils/thinking-parser.ts` 整个 `parseThinking` 相关部分为: ```ts const PLACEHOLDER_PREFIX = '\u0000THKCODE' const PLACEHOLDER_SUFFIX = '\u0000' const FENCED_RE = /(```|~~~)([\s\S]*?)\1/g const INLINE_CODE_RE = /`[^`\n]*`/g function protectCodeBlocks(input: string): { masked: string; blocks: string[] } { const blocks: string[] = [] let masked = input.replace(FENCED_RE, (m) => { blocks.push(m) return `${PLACEHOLDER_PREFIX}${blocks.length - 1}${PLACEHOLDER_SUFFIX}` }) masked = masked.replace(INLINE_CODE_RE, (m) => { blocks.push(m) return `${PLACEHOLDER_PREFIX}${blocks.length - 1}${PLACEHOLDER_SUFFIX}` }) return { masked, blocks } } function restoreCodeBlocks(text: string, blocks: string[]): string { if (blocks.length === 0) return text return text.replace( new RegExp(`${PLACEHOLDER_PREFIX}(\\d+)${PLACEHOLDER_SUFFIX}`, 'g'), (_, idx) => blocks[Number(idx)] ?? '', ) } export function parseThinking(content: string, opts: ParseOptions): ParsedThinking { const { masked, blocks } = protectCodeBlocks(content) const segments: string[] = [] let pending: string | null = null let body = '' let lastIndex = 0 TAG_RE.lastIndex = 0 let m: RegExpExecArray | null while ((m = TAG_RE.exec(masked)) !== null) { body += masked.slice(lastIndex, m.index) segments.push(m[2]) lastIndex = m.index + m[0].length } const rest = masked.slice(lastIndex) const openRe = /<(think|thinking|reasoning)>([\s\S]*)$/i const openMatch = rest.match(openRe) if (openMatch) { body += rest.slice(0, openMatch.index) if (opts.streaming) { pending = openMatch[2] } else { body += rest.slice(openMatch.index!) } } else { body += rest } return { segments: segments.map(s => restoreCodeBlocks(s, blocks)), pending: pending === null ? null : restoreCodeBlocks(pending, blocks), body: restoreCodeBlocks(body, blocks), hasThinking: segments.length > 0 || pending !== null, } } ``` - [ ] **Step 4.4: 运行确认全部通过** Run: `npx vitest run tests/client/thinking-parser.test.ts` Expected: PASS (13 tests) - [ ] **Step 4.5: 提交** ```bash git add tests/client/thinking-parser.test.ts packages/client/src/utils/thinking-parser.ts git commit -m "feat(thinking-parser): 代码块保护避免误识别伪标签 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>" ``` --- ## Task 5: 同名嵌套 & chunk 边界 行为文档化 **Files:** - Modify: `tests/client/thinking-parser.test.ts` - [ ] **Step 5.1: 追加行为说明测试** ```ts it('same-name nesting: inner tag absorbed into first segment (documented limitation)', () => { const r = parseThinking('abc', { streaming: false }) expect(r.segments).toEqual(['ab']) expect(r.body).toBe('c') }) it('handles chunk boundary: partial opening tag not yet identified', () => { const mid = parseThinking('hidone', { streaming: true }) expect(after.segments).toEqual(['hi']) expect(after.body).toBe('done') }) ``` - [ ] **Step 5.2: 运行 + 提交** Run: `npx vitest run tests/client/thinking-parser.test.ts` Expected: PASS (15 tests) ```bash git add tests/client/thinking-parser.test.ts git commit -m "test(thinking-parser): 同名嵌套与 chunk 边界行为 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>" ``` --- ## Task 6: 字数计数 `countThinkingChars` **Files:** - Modify: `tests/client/thinking-parser.test.ts` - Modify: `packages/client/src/utils/thinking-parser.ts` - [ ] **Step 6.1: 写测试** ```ts import { countThinkingChars } from '@/utils/thinking-parser' describe('countThinkingChars', () => { it('counts all segments + pending as Unicode chars', () => { const n = countThinkingChars({ segments: ['abc', '你好'], pending: '🎉!', body: '', hasThinking: true, }) expect(n).toBe(7) }) it('returns 0 when no thinking', () => { expect(countThinkingChars({ segments: [], pending: null, body: 'x', hasThinking: false })).toBe(0) }) }) ``` - [ ] **Step 6.2: 实现** 在 `packages/client/src/utils/thinking-parser.ts` 末尾追加: ```ts export function countThinkingChars(parsed: ParsedThinking): number { const len = (s: string) => [...s].length return parsed.segments.reduce((a, s) => a + len(s), 0) + len(parsed.pending || '') } ``` - [ ] **Step 6.3: 运行 + 提交** Run: `npx vitest run tests/client/thinking-parser.test.ts` Expected: PASS (17 tests) ```bash git add tests/client/thinking-parser.test.ts packages/client/src/utils/thinking-parser.ts git commit -m "feat(thinking-parser): countThinkingChars 辅助函数 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>" ``` --- ## Task 7: 边界检测 `detectThinkingBoundary` **Files:** - Modify: `tests/client/thinking-parser.test.ts` - Modify: `packages/client/src/utils/thinking-parser.ts` - [ ] **Step 7.1: 测试** ```ts import { detectThinkingBoundary } from '@/utils/thinking-parser' describe('detectThinkingBoundary', () => { it('detects first appearance of opening tag', () => { const r = detectThinkingBoundary('', 'x') expect(r.startedAtBoundary).toBe(true) expect(r.endedAtBoundary).toBe(false) }) it('detects first appearance of closing tag', () => { const r = detectThinkingBoundary('hi', 'hi') expect(r.startedAtBoundary).toBe(false) expect(r.endedAtBoundary).toBe(true) }) it('detects both when both emerge in one delta', () => { const r = detectThinkingBoundary('', 'x') expect(r.startedAtBoundary).toBe(true) expect(r.endedAtBoundary).toBe(true) }) it('reports no boundary when neither crossed', () => { const r = detectThinkingBoundary('abc', 'abcdef') expect(r.startedAtBoundary).toBe(false) expect(r.endedAtBoundary).toBe(false) }) it('ignores fake tags inside code blocks', () => { const r = detectThinkingBoundary('', '```\nfake\n```') expect(r.startedAtBoundary).toBe(false) expect(r.endedAtBoundary).toBe(false) }) it('is idempotent for repeated open/close after initial', () => { const r = detectThinkingBoundary( 'ab', 'ab', ) expect(r.startedAtBoundary).toBe(false) expect(r.endedAtBoundary).toBe(false) }) }) ``` - [ ] **Step 7.2: 实现** 在 `packages/client/src/utils/thinking-parser.ts` 末尾追加: ```ts export interface ThinkingBoundary { startedAtBoundary: boolean endedAtBoundary: boolean } const ANY_OPEN_RE = /<(think|thinking|reasoning)>/i const ANY_CLOSE_RE = /<\/(think|thinking|reasoning)>/i export function detectThinkingBoundary(prev: string, next: string): ThinkingBoundary { const prevMasked = protectCodeBlocks(prev).masked const nextMasked = protectCodeBlocks(next).masked return { startedAtBoundary: !ANY_OPEN_RE.test(prevMasked) && ANY_OPEN_RE.test(nextMasked), endedAtBoundary: !ANY_CLOSE_RE.test(prevMasked) && ANY_CLOSE_RE.test(nextMasked), } } ``` - [ ] **Step 7.3: 运行 + 提交** Run: `npx vitest run tests/client/thinking-parser.test.ts` Expected: PASS (23 tests) ```bash git add tests/client/thinking-parser.test.ts packages/client/src/utils/thinking-parser.ts git commit -m "feat(thinking-parser): detectThinkingBoundary 边界检测 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>" ``` --- ## Task 8: chat store 集成 `thinkingObservation` **Files:** - Create: `tests/client/chat-store-thinking.test.ts` - Modify: `packages/client/src/stores/hermes/chat.ts` - [ ] **Step 8.1: 写 store 单测** ```ts // tests/client/chat-store-thinking.test.ts import { describe, it, expect, beforeEach } from 'vitest' import { setActivePinia, createPinia } from 'pinia' import { useChatStore } from '@/stores/hermes/chat' describe('chat store thinkingObservation', () => { beforeEach(() => { setActivePinia(createPinia()) }) it('starts empty', () => { const store = useChatStore() expect(store.getThinkingObservation('any-id')).toBeUndefined() }) it('records startedAt when delta first introduces an opening tag', () => { const store = useChatStore() store.noteThinkingDelta('msg-1', '', 'hi') const ob = store.getThinkingObservation('msg-1') expect(ob).toBeDefined() expect(typeof ob!.startedAt).toBe('number') expect(ob!.endedAt).toBeUndefined() }) it('records endedAt when delta first introduces closing tag', () => { const store = useChatStore() store.noteThinkingDelta('msg-1', '', 'hi') store.noteThinkingDelta('msg-1', 'hi', 'hidone') const ob = store.getThinkingObservation('msg-1') expect(ob!.startedAt).toBeDefined() expect(typeof ob!.endedAt).toBe('number') }) it('is idempotent for subsequent openings/closings', () => { const store = useChatStore() store.noteThinkingDelta('m', '', 'a') const first = store.getThinkingObservation('m')! const firstStarted = first.startedAt const firstEnded = first.endedAt store.noteThinkingDelta( 'm', 'a', 'ab', ) const second = store.getThinkingObservation('m')! expect(second.startedAt).toBe(firstStarted) expect(second.endedAt).toBe(firstEnded) }) it('is ignored when delta is inside a code block', () => { const store = useChatStore() store.noteThinkingDelta('m', '', '```\nfake\n```') expect(store.getThinkingObservation('m')).toBeUndefined() }) it('clears observations on clearThinkingObservationFor', () => { const store = useChatStore() store.noteThinkingDelta('m', '', 'hi') expect(store.getThinkingObservation('m')).toBeDefined() store.clearThinkingObservationFor('any-session') expect(store.getThinkingObservation('m')).toBeUndefined() }) }) ``` - [ ] **Step 8.2: 运行确认失败** Run: `npx vitest run tests/client/chat-store-thinking.test.ts` Expected: FAIL — 方法未定义 - [ ] **Step 8.3: 修改 chat.ts 导入 detectThinkingBoundary** 在 `packages/client/src/stores/hermes/chat.ts` import 区域追加一行: ```ts import { detectThinkingBoundary } from '@/utils/thinking-parser' ``` - [ ] **Step 8.4: 在 store setup 函数内新增状态与方法** 定位 `defineStore('chat', () => { ... })` 内部(建议在已有 `const streamStates = ...` 等 ref 声明附近),追加: ```ts // Transient observation of boundaries during active streaming. // Not persisted; cleared on session switch. See spec §5.3. const thinkingObservation = new Map() function getThinkingObservation(messageId: string) { return thinkingObservation.get(messageId) } function noteThinkingDelta(messageId: string, prevContent: string, nextContent: string) { const { startedAtBoundary, endedAtBoundary } = detectThinkingBoundary(prevContent, nextContent) if (!startedAtBoundary && !endedAtBoundary) return const existing = thinkingObservation.get(messageId) || {} if (startedAtBoundary && existing.startedAt === undefined) { existing.startedAt = Date.now() } if (endedAtBoundary && existing.endedAt === undefined) { existing.endedAt = Date.now() } thinkingObservation.set(messageId, existing) } function clearThinkingObservationFor(_sessionId: string) { // messageId 与 sessionId 的关联未单独持有;方案是切会话时一律清空。 // 这符合 spec 定义:observation 是"当前会话范围内"的 transient 状态。 thinkingObservation.clear() } ``` 在 store `return { ... }` 块末尾新增导出: ```ts getThinkingObservation, noteThinkingDelta, clearThinkingObservationFor, ``` - [ ] **Step 8.5: 运行测试确认通过** Run: `npx vitest run tests/client/chat-store-thinking.test.ts` Expected: PASS (6 tests) - [ ] **Step 8.6: 提交** ```bash git add packages/client/src/stores/hermes/chat.ts tests/client/chat-store-thinking.test.ts git commit -m "feat(chat-store): 新增 thinkingObservation 运行时 Map Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>" ``` --- ## Task 9: 接入 `message.delta` 与 `switchSession` **Files:** - Modify: `packages/client/src/stores/hermes/chat.ts` - [ ] **Step 9.1: 定位 switchSession** Run: `grep -n "async function switchSession\|function switchSession\|switchSession =\|function selectSession" packages/client/src/stores/hermes/chat.ts` 记下函数起始行号。 - [ ] **Step 9.2: 修改 message.delta 分支** 把 `packages/client/src/stores/hermes/chat.ts` 中 `case 'message.delta':` 分支(约 817-833 行)整体替换为: ```ts case 'message.delta': { const msgs = getSessionMsgs(sid) const last = msgs[msgs.length - 1] if (last?.role === 'assistant' && last.isStreaming) { const prev = last.content const next = prev + (evt.delta || '') noteThinkingDelta(last.id, prev, next) last.content = next } else { const newId = uid() const nextContent = evt.delta || '' noteThinkingDelta(newId, '', nextContent) addMessage(sid, { id: newId, role: 'assistant', content: nextContent, timestamp: Date.now(), isStreaming: true, }) } schedulePersist() break } ``` - [ ] **Step 9.3: 在 switchSession 函数最开头加一行清理** 根据 Step 9.1 找到的 switchSession 函数入口(形如 `async function switchSession(sessionId: string) {`),在函数体第一行加入: ```ts clearThinkingObservationFor(sessionId) ``` (参数名以实际函数签名为准。) - [ ] **Step 9.4: 运行所有测试** Run: `npm run test -- --run` Expected: 全部通过(新增 + 原有) Run: `npx vue-tsc -b --noEmit` Expected: 通过 - [ ] **Step 9.5: 提交** ```bash git add packages/client/src/stores/hermes/chat.ts git commit -m "feat(chat-store): message.delta 写入 thinking 边界 + switchSession 清理 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>" ``` --- ## Task 10: i18n 8 语言新增 thinking key **Files:** - Modify: `packages/client/src/i18n/locales/{en,zh,de,es,fr,ja,ko,pt}.ts` 在每个 locale 的 `chat: { ... }` 对象**末尾**(闭合 `},` 前)追加 6 条 key。各语言内容如下: **zh.ts** ```ts thinkingLabel: '思考过程', thinkingInProgress: '思考中…', thinkingShow: '展开思考过程', thinkingHide: '收起思考过程', thinkingDuration: '已观察 {duration}', thinkingChars: '{count} 字', ``` **en.ts** ```ts thinkingLabel: 'Thinking', thinkingInProgress: 'Thinking…', thinkingShow: 'Show thinking', thinkingHide: 'Hide thinking', thinkingDuration: 'Observed {duration}', thinkingChars: '{count} chars', ``` **de.ts** ```ts thinkingLabel: 'Denkprozess', thinkingInProgress: 'Denkt…', thinkingShow: 'Denkprozess anzeigen', thinkingHide: 'Denkprozess ausblenden', thinkingDuration: 'Beobachtet {duration}', thinkingChars: '{count} Zeichen', ``` **es.ts** ```ts thinkingLabel: 'Pensamiento', thinkingInProgress: 'Pensando…', thinkingShow: 'Mostrar pensamiento', thinkingHide: 'Ocultar pensamiento', thinkingDuration: 'Observado {duration}', thinkingChars: '{count} caracteres', ``` **fr.ts** ```ts thinkingLabel: 'Raisonnement', thinkingInProgress: 'En réflexion…', thinkingShow: 'Afficher le raisonnement', thinkingHide: 'Masquer le raisonnement', thinkingDuration: 'Observé {duration}', thinkingChars: '{count} caractères', ``` **ja.ts** ```ts thinkingLabel: '思考過程', thinkingInProgress: '思考中…', thinkingShow: '思考過程を表示', thinkingHide: '思考過程を隠す', thinkingDuration: '観測 {duration}', thinkingChars: '{count} 文字', ``` **ko.ts** ```ts thinkingLabel: '사고 과정', thinkingInProgress: '사고 중…', thinkingShow: '사고 과정 펼치기', thinkingHide: '사고 과정 접기', thinkingDuration: '관측 {duration}', thinkingChars: '{count}자', ``` **pt.ts** ```ts thinkingLabel: 'Raciocínio', thinkingInProgress: 'Pensando…', thinkingShow: 'Mostrar raciocínio', thinkingHide: 'Ocultar raciocínio', thinkingDuration: 'Observado {duration}', thinkingChars: '{count} caracteres', ``` - [ ] **Step 10.1: 追加 8 个 locale 文件中的 key**(如上) - [ ] **Step 10.2: Type-check** Run: `npx vue-tsc -b --noEmit` Expected: 通过 - [ ] **Step 10.3: 提交** ```bash git add packages/client/src/i18n/locales/ git commit -m "i18n: 新增 thinking 块 6 条 key(8 语言) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>" ``` --- ## Task 11: `MessageItem.vue` 渲染 thinking 折叠区 **Files:** - Modify: `packages/client/src/components/hermes/chat/MessageItem.vue` - [ ] **Step 11.1: 补充 `