From 544c73879bbd7530f4fd072c8fd3a80eb80a4f0d Mon Sep 17 00:00:00 2001 From: ekko <152005280+EKKOLearnAI@users.noreply.github.com> Date: Tue, 5 May 2026 08:55:36 +0800 Subject: [PATCH] chore: remove outdated planning documents (#452) Remove obsolete planning and spec documents: - docs/plans/2026-04-22-chat-live-monitor-direction.md - docs/superpowers/plans/2026-04-23-thinking-block-collapse.md - docs/superpowers/specs/2026-04-23-thinking-block-collapse-design.md These documents are no longer relevant as the features have been implemented. Co-authored-by: Claude Sonnet 4.6 --- .../2026-04-22-chat-live-monitor-direction.md | 131 -- .../2026-04-23-thinking-block-collapse.md | 1138 ----------------- ...26-04-23-thinking-block-collapse-design.md | 310 ----- 3 files changed, 1579 deletions(-) delete mode 100644 docs/plans/2026-04-22-chat-live-monitor-direction.md delete mode 100644 docs/superpowers/plans/2026-04-23-thinking-block-collapse.md delete mode 100644 docs/superpowers/specs/2026-04-23-thinking-block-collapse-design.md diff --git a/docs/plans/2026-04-22-chat-live-monitor-direction.md b/docs/plans/2026-04-22-chat-live-monitor-direction.md deleted file mode 100644 index 05391ec8..00000000 --- a/docs/plans/2026-04-22-chat-live-monitor-direction.md +++ /dev/null @@ -1,131 +0,0 @@ -# Hermes Web UI Chat / Live Monitor Direction Plan - -> For Hermes: use subagent-driven-development only after Han explicitly approves execution. - -Goal: clarify whether Chat and Live should both exist, and record the current product recommendation while shipping the bundled live-badge PR. - -Architecture: keep the interactive chat write path and any read-only monitor path conceptually separate. In the current product, the immediate user need is best served by direct Live badges in the Chat session list. A separate Live surface is justified only if it becomes a real monitor with distinct observability and triage value. - -Tech stack: Vue 3, Pinia, Naive UI, Koa, Hermes session DB. - ---- - -## Current findings - -1. Original reason for Live -- Live was introduced as a read-only monitoring surface inside the Chat page. -- The intent was to avoid a separate route/page while still allowing users to inspect conversations without sending messages there. - -2. Current product problem -- In practice, Live is too close to a second session browser. -- Chat already contains the main session list and now supports direct Live badges on active rows. -- Without stronger monitor-specific affordances, the Chat/Live toggle weakens the information architecture. - -3. External dashboard pattern check -- Useful live monitors are observability surfaces, not duplicate navigators. -- Common differentiators: - - search - - source/status filters - - active vs recent grouping - - read-only drilldown across many runs - - monitoring metadata such as live state, last active, errors, counts, source/model, stuck state - -4. Decision -- Keep direct Live badges in Chat session rows. -- Do not keep the current Chat/Live toggle long-term unless we rebuild it as a real monitor surface. -- Preferred direction right now: remove the current Live toggle after the bundled PR lands, unless Han wants an explicit monitor rebuild. - ---- - -## Recommended roadmap - -### Phase 0: ship the bundled Live badge PR - -Objective: land the immediate UX improvement and backend fix already implemented on `feat/chat-session-live-badge`. - -Scope: -- direct `Live` badge in normal Chat session rows -- stronger but on-brand badge styling -- DB-backed fix for the current Live monitor backend so the existing surface stops failing on large histories -- tests for both client and server changes - -Done when: -- PR is open against `upstream/main` -- branch includes the implementation commits plus this plan doc -- targeted tests and build pass - -### Phase 1: product simplification decision - -Objective: decide whether to keep or remove the current Chat/Live toggle. - -Recommended default: -- remove the current `Chat / Live` toggle -- keep only Chat + row-level Live badges - -Why: -- this solves the real user need: show active chats directly where users already work -- it avoids maintaining a half-monitor that duplicates Chat semantics - -Done when: -- product decision is explicit: `remove-live-toggle` or `rebuild-monitor` - -### Phase 2A: if simplifying, remove the current Live surface - -Objective: cleanly remove the current in-Chat Live mode. - -Files likely involved: -- `packages/client/src/components/hermes/chat/ChatPanel.vue` -- `packages/client/src/components/hermes/chat/ConversationMonitorPane.vue` -- `packages/client/src/components/hermes/settings/SessionSettings.vue` -- `packages/client/src/stores/hermes/session-browser-prefs.ts` -- related i18n keys and tests - -Expected effect: -- Chat remains the only session interaction surface -- active work is indicated directly by row-level `Live` badges -- no duplicate list/detail workflow inside Chat - -### Phase 2B: if keeping a monitor, rebuild it as a true monitor - -Objective: keep a separate read-only surface only if it becomes clearly distinct from Chat. - -Required monitor traits: -- read-only only -- search -- source/type/status filters -- active vs recent grouping -- conversation-chain aggregation rather than raw session browsing -- metadata useful for triage: last active, live/running, visible message count, linked session count, source/model, errors/stuck state - -Preferred naming: -- `Monitor` or `Conversations`, not `Live` - -Preferred surface: -- a dedicated page/route rather than a peer toggle inside Chat - ---- - -## Review inputs - -Independent review summary: -- Branch implementation for the bundled PR is PR-ready; no blocker/major findings. -- Product review recommendation: remove the current Live toggle now unless we commit to rebuilding it as a distinct monitor surface. - ---- - -## Validation commands - -Run from repo root: - -`npm test -- tests/server/conversations-db.test.ts tests/server/sessions-controller.test.ts tests/client/chat-store.test.ts tests/client/chat-panel.test.ts` - -`npm run build` - ---- - -## Artifact note - -Canonical plan path: -- `docs/plans/2026-04-22-chat-live-monitor-direction.md` - -This file is the source of truth for the current Chat-vs-Live recommendation tied to the bundled live-badge PR. diff --git a/docs/superpowers/plans/2026-04-23-thinking-block-collapse.md b/docs/superpowers/plans/2026-04-23-thinking-block-collapse.md deleted file mode 100644 index 67026436..00000000 --- a/docs/superpowers/plans/2026-04-23-thinking-block-collapse.md +++ /dev/null @@ -1,1138 +0,0 @@ -# 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: 补充 `