mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-09 17:19:02 +00:00
docs: consolidate design docs and plans under docs/ (#6417)
Design docs and implementation plans were scattered across .qwen/design, .qwen/plans, and docs/superpowers. The .qwen/ locations are git-ignored, so docs written there never got tracked, while docs/design already held the richer, version-controlled set. Consolidate everything under docs/design and docs/plans, relocate two stray root docs into docs/design, and repoint the references left dangling by the move (moved-doc cross-links and a few source comments). Also update AGENTS.md and the feat-dev skill so the documented workflow writes new design docs and plans to the tracked docs/ locations. Co-authored-by: DragonnZhang <dragonzhang1024@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
4c884e47bd
commit
067cfbba62
42 changed files with 324 additions and 224 deletions
|
|
@ -1,42 +0,0 @@
|
|||
# Channel Lifecycle Status Umbrella
|
||||
|
||||
Date: 2026-07-01
|
||||
|
||||
## Goal
|
||||
|
||||
Provide one review surface that summarizes the lifecycle-status behavior across
|
||||
the supported channel adapters and calls out what remains intentionally out of
|
||||
scope.
|
||||
|
||||
## Scope
|
||||
|
||||
- Telegram
|
||||
- Weixin
|
||||
- DingTalk
|
||||
- Feishu
|
||||
|
||||
## Explicit Non-Goals
|
||||
|
||||
- Slack remains out of scope.
|
||||
- QQ Bot remains out of scope for lifecycle status UI.
|
||||
- The plugin example remains out of scope for lifecycle status UI.
|
||||
- DingTalk terminal emoji remains out of scope.
|
||||
|
||||
## Reviewer Matrix
|
||||
|
||||
| Channel | Supported lifecycle events | Native surface | `started` behavior | `text_chunk` behavior | Terminal behavior | Unsupported / no-op reason | Exact test files |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| Telegram | `started`, `completed`, `cancelled`, `failed` | Typing indicator | Starts the existing per-chat typing loop once. Duplicate `started` events do not add another loop. | Ignored by the lifecycle hook. Response content continues through the normal reply path. | Stops the typing loop on any terminal event and leaves no stale interval behind. | `tool_call` has no native status surface and does not need adapter UI. | `packages/channels/telegram/src/TelegramAdapter.test.ts` |
|
||||
| Weixin | `started`, `completed`, `cancelled`, `failed` | Typing indicator | Calls `setTyping(chatId, true)` once for the active chat. Duplicate `started` events do not restack typing state. | Ignored by the lifecycle hook. Response content continues through the normal send path. | Calls `setTyping(chatId, false)` on terminal events. Failed start attempts clear local state so a later `started` can retry. | `tool_call` has no separate status surface and no extra message should be sent. | `packages/channels/weixin/src/WeixinAdapter.test.ts` |
|
||||
| DingTalk | `started`, `completed`, `cancelled`, `failed` | Eye reaction on the inbound message | Attaches the existing eye reaction once when a conversation id is available. | Ignored by the lifecycle hook. Response content continues through the normal send path. | Recalls the eye reaction on terminal events, including late-resolving attach races after cancellation. | Direct robot webhook chats do not expose the conversation id needed for reactions, so lifecycle status is a no-op there. `tool_call` also has no UI in scope. | `packages/channels/dingtalk/src/DingtalkAdapter.test.ts` |
|
||||
| Feishu | `started`, `completed`, `cancelled`, `failed` | Streaming card status label | Keeps the card in its running state and reserves space for the running label while the existing card stream is active. | Not consumed directly by the lifecycle hook. Content streaming remains owned by the existing response/card stream hook. | Finalizes the card status label as completed, cancelled, or failed without overwriting the streamed answer body. | `tool_call` stays hidden because the card already uses the answer stream plus terminal status labels only. | `packages/channels/feishu/src/adapter.test.ts`, `packages/channels/feishu/src/markdown.test.ts` |
|
||||
| QQ Bot | None | None | No-op. | No-op. QQ Bot still streams reply chunks through outbound message sends, but not through lifecycle status updates. | No-op. | The channel has no typing or task-status endpoint, and `QQChannel` leaves `onPromptStart`, `onPromptEnd`, and `onTaskLifecycle` empty by design. | `packages/channels/qqbot/src/send.test.ts`, `packages/channels/qqbot/src/api.test.ts` |
|
||||
| Plugin example | None | WebSocket protocol messages only | No-op for lifecycle status. | Streams response chunks over the mock protocol's `chunk` message type from `onResponseChunk`, outside lifecycle status handling. | Sends the final outbound message on response completion, outside lifecycle status handling. | The mock channel demonstrates transport wiring only; it has no native typing, reaction, or status surface. | `integration-tests/channel-plugin.test.ts` |
|
||||
|
||||
## Review Notes
|
||||
|
||||
- Feishu lifecycle `text_chunk` remains a no-op in the lifecycle hook. It does
|
||||
not append or update answer content there.
|
||||
- Slack is intentionally excluded from this matrix because it is out of scope.
|
||||
- DingTalk terminal events only recall the existing eye reaction in this scope.
|
||||
No terminal emoji is added.
|
||||
|
|
@ -14,9 +14,9 @@ feeds the next.
|
|||
|
||||
## Artifact Paths
|
||||
|
||||
Use `.qwen/` paths for planning artifacts:
|
||||
Use these paths for planning artifacts:
|
||||
|
||||
- `.qwen/design/<feature>.md`
|
||||
- `docs/design/<feature>.md`
|
||||
- `.qwen/e2e-tests/<feature>.md`
|
||||
|
||||
## Phase 1: Investigate
|
||||
|
|
|
|||
13
AGENTS.md
13
AGENTS.md
|
|
@ -160,7 +160,7 @@ npm run preflight # Full check: clean → install → format → lint → build
|
|||
|
||||
### General workflow
|
||||
|
||||
1. **Design doc for non-trivial work** — write one in `.qwen/design/` if the
|
||||
1. **Design doc for non-trivial work** — write one in `docs/design/` if the
|
||||
change touches multiple files or involves design decisions. Skip for small
|
||||
bugfixes.
|
||||
2. **Test plan for behavioral changes** — write an E2E test plan in
|
||||
|
|
@ -220,11 +220,18 @@ applicable.
|
|||
|
||||
## Project Directories
|
||||
|
||||
Project artifacts live under `.qwen/`:
|
||||
Design docs and implementation plans are committed under `docs/` so they are
|
||||
tracked in version control:
|
||||
|
||||
| Directory | Purpose |
|
||||
| -------------- | -------------------------------- |
|
||||
| `docs/design/` | Design docs for planned features |
|
||||
| `docs/plans/` | Implementation plans |
|
||||
|
||||
Other working artifacts live under `.qwen/` (git-ignored):
|
||||
|
||||
| Directory | Purpose |
|
||||
| ----------------------- | ------------------------------------ |
|
||||
| `.qwen/design/` | Design docs for planned features |
|
||||
| `.qwen/e2e-tests/` | E2E test plans and results |
|
||||
| `.qwen/issues/` | Issue drafts before filing on GitHub |
|
||||
| `.qwen/pr-drafts/` | PR drafts before submitting |
|
||||
|
|
|
|||
|
|
@ -73,12 +73,12 @@ reasoning?: false | { effort?: 'low' | 'medium' | 'high' | 'max'; budget_tokens?
|
|||
|
||||
Existing per-provider translators:
|
||||
|
||||
| Provider | File | Behavior |
|
||||
| --- | --- | --- |
|
||||
| DeepSeek | `provider/deepseek.ts:176-218` | nested → flat `reasoning_effort`; `low/medium→high`, `xhigh→max` |
|
||||
| Anthropic | `anthropicContentGenerator.ts:521-593`, clamp `665-693`, beta hdr `393-431` | `output_config.effort` + thinking; `max`→`high` clamp + one-time warn; `effort-2025-11-24` beta |
|
||||
| Gemini | `geminiContentGenerator.ts:107-146` | `thinkingConfig`/`thinkingLevel`; `low→LOW`, `high/max→HIGH` |
|
||||
| OpenAI/GLM/DashScope | `openaiContentGenerator/pipeline.ts:689-717` (`buildReasoningConfig`), strip `597-602` | forwards/strips `reasoning_effort`; DashScope adds `preserve_thinking` |
|
||||
| Provider | File | Behavior |
|
||||
| -------------------- | -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
|
||||
| DeepSeek | `provider/deepseek.ts:176-218` | nested → flat `reasoning_effort`; `low/medium→high`, `xhigh→max` |
|
||||
| Anthropic | `anthropicContentGenerator.ts:521-593`, clamp `665-693`, beta hdr `393-431` | `output_config.effort` + thinking; `max`→`high` clamp + one-time warn; `effort-2025-11-24` beta |
|
||||
| Gemini | `geminiContentGenerator.ts:107-146` | `thinkingConfig`/`thinkingLevel`; `low→LOW`, `high/max→HIGH` |
|
||||
| OpenAI/GLM/DashScope | `openaiContentGenerator/pipeline.ts:689-717` (`buildReasoningConfig`), strip `597-602` | forwards/strips `reasoning_effort`; DashScope adds `preserve_thinking` |
|
||||
|
||||
Gaps: the union lacks `xhigh`; Gemini lacks `medium` and an `xhigh→high` rule;
|
||||
the generic pipeline must be confirmed to emit `reasoning_effort` for plain
|
||||
|
|
@ -119,7 +119,7 @@ borrow from (studied at `~/Documents/openclaw`):
|
|||
What we take: the **rank-based central clamp**, **per-model capability
|
||||
declaration**, the **three shape mappers**, and the **exact Gemini 2.5 budget
|
||||
buckets**. What we drop for v1: `minimal`/`adaptive` user tiers (decision = 5
|
||||
tiers) — they stay valid *internal* normalization targets so a model catalog can
|
||||
tiers) — they stay valid _internal_ normalization targets so a model catalog can
|
||||
still declare them.
|
||||
|
||||
## Design
|
||||
|
|
@ -132,13 +132,13 @@ Each provider declares a supported subset; the translator clamps a requested
|
|||
tier **down** the ladder to the nearest supported tier. Mapping (canonical →
|
||||
wire value), with `↓` marking a clamp:
|
||||
|
||||
| Tier | OpenAI `reasoning_effort` | DeepSeek `reasoning_effort` | GLM-5.2+ `reasoning_effort` | Anthropic `output_config.effort` | Gemini 3 `thinking_level` | Qwen DashScope |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| low | low | high¹ | low | low | low | enable_thinking:true |
|
||||
| medium | medium | high¹ | medium | medium | medium | true |
|
||||
| high | high | high | high | high (default) | high | true |
|
||||
| xhigh | xhigh | max¹ | xhigh | xhigh ↓high² | high ↓² | true |
|
||||
| max | xhigh ↓ (no `max`) | max | max | max ↓high² | high ↓² | true |
|
||||
| Tier | OpenAI `reasoning_effort` | DeepSeek `reasoning_effort` | GLM-5.2+ `reasoning_effort` | Anthropic `output_config.effort` | Gemini 3 `thinking_level` | Qwen DashScope |
|
||||
| ------ | ------------------------- | --------------------------- | --------------------------- | -------------------------------- | ------------------------- | -------------------- |
|
||||
| low | low | high¹ | low | low | low | enable_thinking:true |
|
||||
| medium | medium | high¹ | medium | medium | medium | true |
|
||||
| high | high | high | high | high (default) | high | true |
|
||||
| xhigh | xhigh | max¹ | xhigh | xhigh ↓high² | high ↓² | true |
|
||||
| max | xhigh ↓ (no `max`) | max | max | max ↓high² | high ↓² | true |
|
||||
|
||||
¹ DeepSeek/GLM documented internal grouping (low/medium ≡ high, xhigh ≡ max).
|
||||
² Clamped to the model's documented ceiling (varies by Anthropic model; Gemini 3
|
||||
|
|
@ -183,12 +183,12 @@ nested `reasoning: { effort }` object; `buildReasoningConfig()`
|
|||
provider whose wire field differs must reshape it in its `buildRequest` hook.
|
||||
Known shapes:
|
||||
|
||||
| Wire shape | Providers | qwen-code handling |
|
||||
| --- | --- | --- |
|
||||
| nested `reasoning: { effort }` | OpenAI Responses, OpenRouter, gpt-5.x | passthrough (default) ✅ |
|
||||
| flat top-level `reasoning_effort` | DeepSeek, **GLM/z.ai**, OpenAI Chat Completions, Groq | DeepSeek adapter flattens ✅; **GLM has no adapter → currently ships the nested shape, likely wrong ❌** |
|
||||
| `enable_thinking` bool | qwen3 / DashScope | adapter emits bool (disable only); no effort tiers yet |
|
||||
| `extra_body.thinking.enabled` toggle | GLM | separate on/off knob from the effort value |
|
||||
| Wire shape | Providers | qwen-code handling |
|
||||
| ------------------------------------ | ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
||||
| nested `reasoning: { effort }` | OpenAI Responses, OpenRouter, gpt-5.x | passthrough (default) ✅ |
|
||||
| flat top-level `reasoning_effort` | DeepSeek, **GLM/z.ai**, OpenAI Chat Completions, Groq | DeepSeek adapter flattens ✅; **GLM has no adapter → currently ships the nested shape, likely wrong ❌** |
|
||||
| `enable_thinking` bool | qwen3 / DashScope | adapter emits bool (disable only); no effort tiers yet |
|
||||
| `extra_body.thinking.enabled` toggle | GLM | separate on/off knob from the effort value |
|
||||
|
||||
Implication: pure passthrough only "just works" for providers that accept the
|
||||
nested shape. **PR1 must add GLM/z.ai flattening** (mirror `deepseek.ts`) and,
|
||||
|
|
@ -34,26 +34,26 @@ scope because each channel already has a clear native surface for these states.
|
|||
|
||||
## Current State
|
||||
|
||||
| Channel | Existing status surface | Current behavior |
|
||||
| --- | --- | --- |
|
||||
| Telegram | Typing indicator | Starts typing on prompt start and stops on prompt end. |
|
||||
| Weixin | Typing indicator | Starts typing on prompt start and stops on prompt end. |
|
||||
| DingTalk | Message reaction | Adds the eye reaction on prompt start and recalls it on prompt end. |
|
||||
| Feishu | Streaming card | Shows and updates a streaming card, with completion and error paths. |
|
||||
| Channel | Existing status surface | Current behavior |
|
||||
| -------- | ----------------------- | -------------------------------------------------------------------- |
|
||||
| Telegram | Typing indicator | Starts typing on prompt start and stops on prompt end. |
|
||||
| Weixin | Typing indicator | Starts typing on prompt start and stops on prompt end. |
|
||||
| DingTalk | Message reaction | Adds the eye reaction on prompt start and recalls it on prompt end. |
|
||||
| Feishu | Streaming card | Shows and updates a streaming card, with completion and error paths. |
|
||||
|
||||
## Proposed Design
|
||||
|
||||
Keep the implementation adapter-local. Each adapter consumes the lifecycle event
|
||||
hook and maps the event into the platform's existing native status surface.
|
||||
|
||||
| Lifecycle event | Telegram | Weixin | DingTalk | Feishu |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `started` | Start typing. | Start typing. | Add eye reaction. | Show/update card as running. |
|
||||
| `text_chunk` | Ignore. | Ignore. | Ignore. | Ignore in the lifecycle hook. Content streaming stays on the existing response/card stream path. |
|
||||
| `tool_call` | Ignore. | Ignore. | Ignore. | Ignore for UI. |
|
||||
| `completed` | Stop typing. | Stop typing. | Recall eye reaction. | Mark card completed. |
|
||||
| `cancelled` | Stop typing. | Stop typing. | Recall eye reaction. | Mark card cancelled. |
|
||||
| `failed` | Stop typing. | Stop typing. | Recall eye reaction. | Mark card failed. |
|
||||
| Lifecycle event | Telegram | Weixin | DingTalk | Feishu |
|
||||
| --------------- | ------------- | ------------- | -------------------- | ------------------------------------------------------------------------------------------------ |
|
||||
| `started` | Start typing. | Start typing. | Add eye reaction. | Show/update card as running. |
|
||||
| `text_chunk` | Ignore. | Ignore. | Ignore. | Ignore in the lifecycle hook. Content streaming stays on the existing response/card stream path. |
|
||||
| `tool_call` | Ignore. | Ignore. | Ignore. | Ignore for UI. |
|
||||
| `completed` | Stop typing. | Stop typing. | Recall eye reaction. | Mark card completed. |
|
||||
| `cancelled` | Stop typing. | Stop typing. | Recall eye reaction. | Mark card cancelled. |
|
||||
| `failed` | Stop typing. | Stop typing. | Recall eye reaction. | Mark card failed. |
|
||||
|
||||
### Telegram
|
||||
|
||||
|
|
@ -85,12 +85,12 @@ send extra status messages unless an existing error path already does so.
|
|||
Feishu keeps the streaming card as the status surface and makes the terminal
|
||||
state explicit in card content:
|
||||
|
||||
| State | Card label |
|
||||
| --- | --- |
|
||||
| Running | `运行中...` |
|
||||
| Completed | `已完成` |
|
||||
| Cancelled | `已取消` |
|
||||
| Failed | `已失败,请重试` |
|
||||
| State | Card label |
|
||||
| --------- | ---------------- |
|
||||
| Running | `运行中...` |
|
||||
| Completed | `已完成` |
|
||||
| Cancelled | `已取消` |
|
||||
| Failed | `已失败,请重试` |
|
||||
|
||||
The card still streams answer content as it does today through the existing
|
||||
response/card stream hook. Lifecycle `text_chunk` is not consumed directly by
|
||||
42
docs/design/2026-07-01-channel-lifecycle-status-umbrella.md
Normal file
42
docs/design/2026-07-01-channel-lifecycle-status-umbrella.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# Channel Lifecycle Status Umbrella
|
||||
|
||||
Date: 2026-07-01
|
||||
|
||||
## Goal
|
||||
|
||||
Provide one review surface that summarizes the lifecycle-status behavior across
|
||||
the supported channel adapters and calls out what remains intentionally out of
|
||||
scope.
|
||||
|
||||
## Scope
|
||||
|
||||
- Telegram
|
||||
- Weixin
|
||||
- DingTalk
|
||||
- Feishu
|
||||
|
||||
## Explicit Non-Goals
|
||||
|
||||
- Slack remains out of scope.
|
||||
- QQ Bot remains out of scope for lifecycle status UI.
|
||||
- The plugin example remains out of scope for lifecycle status UI.
|
||||
- DingTalk terminal emoji remains out of scope.
|
||||
|
||||
## Reviewer Matrix
|
||||
|
||||
| Channel | Supported lifecycle events | Native surface | `started` behavior | `text_chunk` behavior | Terminal behavior | Unsupported / no-op reason | Exact test files |
|
||||
| -------------- | --------------------------------------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
|
||||
| Telegram | `started`, `completed`, `cancelled`, `failed` | Typing indicator | Starts the existing per-chat typing loop once. Duplicate `started` events do not add another loop. | Ignored by the lifecycle hook. Response content continues through the normal reply path. | Stops the typing loop on any terminal event and leaves no stale interval behind. | `tool_call` has no native status surface and does not need adapter UI. | `packages/channels/telegram/src/TelegramAdapter.test.ts` |
|
||||
| Weixin | `started`, `completed`, `cancelled`, `failed` | Typing indicator | Calls `setTyping(chatId, true)` once for the active chat. Duplicate `started` events do not restack typing state. | Ignored by the lifecycle hook. Response content continues through the normal send path. | Calls `setTyping(chatId, false)` on terminal events. Failed start attempts clear local state so a later `started` can retry. | `tool_call` has no separate status surface and no extra message should be sent. | `packages/channels/weixin/src/WeixinAdapter.test.ts` |
|
||||
| DingTalk | `started`, `completed`, `cancelled`, `failed` | Eye reaction on the inbound message | Attaches the existing eye reaction once when a conversation id is available. | Ignored by the lifecycle hook. Response content continues through the normal send path. | Recalls the eye reaction on terminal events, including late-resolving attach races after cancellation. | Direct robot webhook chats do not expose the conversation id needed for reactions, so lifecycle status is a no-op there. `tool_call` also has no UI in scope. | `packages/channels/dingtalk/src/DingtalkAdapter.test.ts` |
|
||||
| Feishu | `started`, `completed`, `cancelled`, `failed` | Streaming card status label | Keeps the card in its running state and reserves space for the running label while the existing card stream is active. | Not consumed directly by the lifecycle hook. Content streaming remains owned by the existing response/card stream hook. | Finalizes the card status label as completed, cancelled, or failed without overwriting the streamed answer body. | `tool_call` stays hidden because the card already uses the answer stream plus terminal status labels only. | `packages/channels/feishu/src/adapter.test.ts`, `packages/channels/feishu/src/markdown.test.ts` |
|
||||
| QQ Bot | None | None | No-op. | No-op. QQ Bot still streams reply chunks through outbound message sends, but not through lifecycle status updates. | No-op. | The channel has no typing or task-status endpoint, and `QQChannel` leaves `onPromptStart`, `onPromptEnd`, and `onTaskLifecycle` empty by design. | `packages/channels/qqbot/src/send.test.ts`, `packages/channels/qqbot/src/api.test.ts` |
|
||||
| Plugin example | None | WebSocket protocol messages only | No-op for lifecycle status. | Streams response chunks over the mock protocol's `chunk` message type from `onResponseChunk`, outside lifecycle status handling. | Sends the final outbound message on response completion, outside lifecycle status handling. | The mock channel demonstrates transport wiring only; it has no native typing, reaction, or status surface. | `integration-tests/channel-plugin.test.ts` |
|
||||
|
||||
## Review Notes
|
||||
|
||||
- Feishu lifecycle `text_chunk` remains a no-op in the lifecycle hook. It does
|
||||
not append or update answer content there.
|
||||
- Slack is intentionally excluded from this matrix because it is out of scope.
|
||||
- DingTalk terminal events only recall the existing eye reaction in this scope.
|
||||
No terminal emoji is added.
|
||||
|
|
@ -14,7 +14,7 @@ PR [#4842][p4842] shipped the fields with an end-to-end runtime path at the
|
|||
time. PR [#4870][p4870] then replaced the YAML parser to support block
|
||||
scalars. This follow-up PR builds on both: it replaces the YAML
|
||||
**stringifier** (PR #4870 left it hand-rolled — see
|
||||
`docs/yaml-parser-replacement.md`), surfaces `mcpServers` + `hooks` on
|
||||
`docs/design/yaml-parser-replacement.md`), surfaces `mcpServers` + `hooks` on
|
||||
`SubagentConfig`, and wires them to the runtime so per-agent MCP servers
|
||||
and hooks actually fire when a subagent runs.
|
||||
|
||||
|
|
@ -58,12 +58,12 @@ use fewer visible rows:
|
|||
The automated spacing assertions and terminal evidence use 100-column fixtures
|
||||
for the changed rules:
|
||||
|
||||
| Scenario | Width | Baseline rows | PR1 rows | Delta | Evidence |
|
||||
| --- | ---: | ---: | ---: | ---: | --- |
|
||||
| Simple assistant reply | 100 | 2 | 1 | -1 | leading history spacer removed |
|
||||
| Tool header with one-line result | 100 | 3 | 2 | -1 | header and result are adjacent |
|
||||
| Three-tool expanded group with rendered results | 100 | 16 | 11 | -5 | one header/result spacer removed per tool result and one inter-tool separator removed between adjacent tools |
|
||||
| Full representative fixture | 100 | 26 | 19 | -7 | same rendered content captured in tmux |
|
||||
| Scenario | Width | Baseline rows | PR1 rows | Delta | Evidence |
|
||||
| ----------------------------------------------- | ----: | ------------: | -------: | ----: | ------------------------------------------------------------------------------------------------------------ |
|
||||
| Simple assistant reply | 100 | 2 | 1 | -1 | leading history spacer removed |
|
||||
| Tool header with one-line result | 100 | 3 | 2 | -1 | header and result are adjacent |
|
||||
| Three-tool expanded group with rendered results | 100 | 16 | 11 | -5 | one header/result spacer removed per tool result and one inter-tool separator removed between adjacent tools |
|
||||
| Full representative fixture | 100 | 26 | 19 | -7 | same rendered content captured in tmux |
|
||||
|
||||
The snapshot diffs also cover the existing 80-column fixtures to confirm the
|
||||
same row-count deltas in the current component test harness.
|
||||
|
|
@ -25,18 +25,19 @@ PR1 通过去除工具组内部多余空行,初步收紧了 TUI 垂直间距
|
|||
|
||||
### 2. 收紧问答间距
|
||||
|
||||
| 位置 | 改动前 | 改动后 |
|
||||
|------|--------|--------|
|
||||
| 用户消息上方 | 1 行空白 | 0(由色带提供视觉分隔;降级时保留 marginTop=1) |
|
||||
| 模型输出上方 | 1 行空白 | 1 行空白(保留,区分思考过程和最终输出) |
|
||||
| 工具调用/状态消息上方 | 1 行空白 | 0 |
|
||||
| 思考文本末尾 | 可能有多余换行 | trimEnd() 避免双空行 |
|
||||
| 位置 | 改动前 | 改动后 |
|
||||
| --------------------- | -------------- | ----------------------------------------------- |
|
||||
| 用户消息上方 | 1 行空白 | 0(由色带提供视觉分隔;降级时保留 marginTop=1) |
|
||||
| 模型输出上方 | 1 行空白 | 1 行空白(保留,区分思考过程和最终输出) |
|
||||
| 工具调用/状态消息上方 | 1 行空白 | 0 |
|
||||
| 思考文本末尾 | 可能有多余换行 | trimEnd() 避免双空行 |
|
||||
|
||||
同一轮对话内的"回复 → 工具调用 → 回复"序列不再有多余空行,信息更紧凑连贯。
|
||||
|
||||
## 效果对比
|
||||
|
||||
**改动前:**
|
||||
|
||||
```
|
||||
(1 行空白)
|
||||
> 帮我读取 package.json
|
||||
|
|
@ -54,6 +55,7 @@ PR1 通过去除工具组内部多余空行,初步收紧了 TUI 垂直间距
|
|||
```
|
||||
|
||||
**改动后:**
|
||||
|
||||
```
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
> 帮我读取 package.json
|
||||
|
|
@ -5,7 +5,7 @@ Internal design document for replacing the hand-rolled 192-line YAML parser at
|
|||
`mcpServers` and `hooks` fields from Claude Code's declarative-agent schema can
|
||||
round-trip safely through subagent / skill / converter code paths.
|
||||
|
||||
Companion to [`docs/declarative-agents-port.md`](./declarative-agents-port.md).
|
||||
Companion to [`docs/design/declarative-agents-port.md`](./declarative-agents-port.md).
|
||||
Issue: [#4821](https://github.com/QwenLM/qwen-code/issues/4821). Prereq for
|
||||
the follow-up to [PR #4842](https://github.com/QwenLM/qwen-code/pull/4842).
|
||||
|
||||
|
|
@ -46,7 +46,7 @@ export function parseYaml(input: string): unknown {
|
|||
(we don't target Bun runtime).
|
||||
- **Schema mode**: NOT explicitly set anywhere in CC. Relies on `yaml`
|
||||
package's default behavior, plus zod validation at the consumer layer
|
||||
(`DL7`, `gS8`, `TKO`/`_u` per `docs/declarative-agents-port.md`). **C**
|
||||
(`DL7`, `gS8`, `TKO`/`_u` per `docs/design/declarative-agents-port.md`). **C**
|
||||
|
||||
### Why `yaml` rather than `js-yaml`
|
||||
|
||||
|
|
@ -87,13 +87,13 @@ Track that decision separately if it comes up.
|
|||
`~/code/claude-code/src/utils/frontmatterParser.ts` is 370 lines. Key
|
||||
findings:
|
||||
|
||||
| Step | Logic | Source |
|
||||
| ------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
|
||||
| Delimiter match | Regex `/^---\s*\n([\s\S]*?)\n---\s*\n?/` — opens at column 0, body is non-greedy, closing `---` must be on its own line | `frontmatterParser.ts:~123` (line numbers from old snapshot; treat as approximate) **C** |
|
||||
| Pass 1 parse | Call `parseYaml(body)`. If success → return parsed object + content remainder. | same file, top of try block **C** |
|
||||
| Pass 2 recovery | On `YAMLException`, walk lines, auto-quote values that look like dates/colons/specials, retry `parseYaml` once. | lines ~85–121 in old snapshot **C** (`tab → 2 spaces` normalisation, ISO-date heuristic, colon-trap) |
|
||||
| Failure fallthrough | Both passes failed → log via `logForDebugging`, return `{ data: {}, content: text }`. Agent loads with empty frontmatter. | end of function **C** |
|
||||
| Telemetry | Wrapped further upstream — `tengu_frontmatter_shadow_unknown_key` / `_mismatch` events fire from `ug5.agent` (Ig5 schema) | `claude.strings:308120`, `309074`, `309076` (cross-cited in `docs/declarative-agents-port.md` Phase 1) |
|
||||
| Step | Logic | Source |
|
||||
| ------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
||||
| Delimiter match | Regex `/^---\s*\n([\s\S]*?)\n---\s*\n?/` — opens at column 0, body is non-greedy, closing `---` must be on its own line | `frontmatterParser.ts:~123` (line numbers from old snapshot; treat as approximate) **C** |
|
||||
| Pass 1 parse | Call `parseYaml(body)`. If success → return parsed object + content remainder. | same file, top of try block **C** |
|
||||
| Pass 2 recovery | On `YAMLException`, walk lines, auto-quote values that look like dates/colons/specials, retry `parseYaml` once. | lines ~85–121 in old snapshot **C** (`tab → 2 spaces` normalisation, ISO-date heuristic, colon-trap) |
|
||||
| Failure fallthrough | Both passes failed → log via `logForDebugging`, return `{ data: {}, content: text }`. Agent loads with empty frontmatter. | end of function **C** |
|
||||
| Telemetry | Wrapped further upstream — `tengu_frontmatter_shadow_unknown_key` / `_mismatch` events fire from `ug5.agent` (Ig5 schema) | `claude.strings:308120`, `309074`, `309076` (cross-cited in `docs/design/declarative-agents-port.md` Phase 1) |
|
||||
|
||||
**Implication for qwen-code**: we do NOT need to clone the 2-pass recovery.
|
||||
qwen-code's `subagent-manager.ts` already enforces stricter "throw on malformed
|
||||
|
|
@ -105,7 +105,7 @@ warn-and-drop posture.
|
|||
|
||||
## Phase 3 — Nested validation via zod (CC)
|
||||
|
||||
The relevant CC validators per `docs/declarative-agents-port.md` Phase 1 +
|
||||
The relevant CC validators per `docs/design/declarative-agents-port.md` Phase 1 +
|
||||
binary strings cross-check:
|
||||
|
||||
### `mcpServers` (CC symbol `gS8` / JSON-shadow `jL7`)
|
||||
|
|
@ -134,7 +134,7 @@ DL7-style), and let the downstream merge into `Config.getMcpServers()` do the
|
|||
shape coercion. `qwen-code` already has `MCPServerConfig` class with
|
||||
`type` discrimination — we reuse that converter instead of duplicating the
|
||||
zod schema. See Phase 4 of the runtime-wiring plan in
|
||||
`docs/declarative-agents-port.md`.
|
||||
`docs/design/declarative-agents-port.md`.
|
||||
|
||||
### `hooks` (CC symbol `TKO` / `_u`)
|
||||
|
||||
|
|
@ -477,7 +477,7 @@ from the existing test suites in `packages/core/src/subagents/`,
|
|||
| --- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Q1 | Does `yaml.parse` need an explicit logger to redirect `YAMLWarning` (e.g., `Unresolved tag`) to qwen-code's logger instead of `process.emitWarning`? | No — defer | If logs get noisy in CI, plumb `{ logLevel: 'silent' }` or a custom `onWarning` callback. Not load-bearing for v1. |
|
||||
| Q2 | Should `parse()` continue to return `{}` for empty-string / null-document YAML, or throw? | No — preserve current behavior | Current hand-rolled returns `{}`; we keep that. Add a regression test pinning the choice. |
|
||||
| Q3 | When `mcpServers` is malformed at the top level (e.g., `mcpServers: "string"`), should the whole agent fail to load, or load with that field dropped? | Yes — drives the warn-and-drop posture in Phase 3 of the implementation | **Resolution**: drop the field, emit a console warning (parity with CC `DL7` per Phase 3 of `docs/declarative-agents-port.md`). |
|
||||
| Q3 | When `mcpServers` is malformed at the top level (e.g., `mcpServers: "string"`), should the whole agent fail to load, or load with that field dropped? | Yes — drives the warn-and-drop posture in Phase 3 of the implementation | **Resolution**: drop the field, emit a console warning (parity with CC `DL7` per Phase 3 of `docs/design/declarative-agents-port.md`). |
|
||||
| Q4 | Same as Q3 but for `hooks`: drop the field, the event, or just the individual matcher? | Yes — drives the warn-and-drop posture | **Resolution**: drop the whole `hooks` field on top-level shape failure. Per-event / per-matcher granularity is deferred to a future PR if a real user surfaces a need. |
|
||||
| Q5 | Does the `Bun.YAML.parse` shortcut from CC's helper apply to qwen-code? | No | qwen-code does not target Bun runtime. Skip. |
|
||||
|
||||
|
|
@ -485,4 +485,4 @@ from the existing test suites in `packages/core/src/subagents/`,
|
|||
|
||||
**Status**: research complete, ready to implement Phase 2 (replace
|
||||
`yaml-parser.ts`) and Phase 3 (re-surface `mcpServers` + `hooks` on
|
||||
`SubagentConfig`) per `docs/declarative-agents-port.md`.
|
||||
`SubagentConfig`) per `docs/design/declarative-agents-port.md`.
|
||||
|
|
@ -13,6 +13,7 @@
|
|||
### Task 1: Extend UsageSummaryRecord with latency and tool duration
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `packages/core/src/services/usageHistoryService.ts:16-44`
|
||||
- Modify: `packages/core/src/services/usageHistoryService.ts:111-158` (metricsToUsageRecord)
|
||||
- Test: `packages/core/src/services/usageHistoryService.test.ts` (create)
|
||||
|
|
@ -32,7 +33,13 @@ function makeMetrics(): SessionMetrics {
|
|||
models: {
|
||||
'qwen-max': {
|
||||
api: { totalRequests: 5, totalErrors: 0, totalLatencyMs: 9500 },
|
||||
tokens: { prompt: 1000, candidates: 500, total: 1500, cached: 800, thoughts: 0 },
|
||||
tokens: {
|
||||
prompt: 1000,
|
||||
candidates: 500,
|
||||
total: 1500,
|
||||
cached: 800,
|
||||
thoughts: 0,
|
||||
},
|
||||
bySource: {},
|
||||
},
|
||||
},
|
||||
|
|
@ -48,8 +55,30 @@ function makeMetrics(): SessionMetrics {
|
|||
[ToolCallDecision.AUTO_ACCEPT]: 4,
|
||||
},
|
||||
byName: {
|
||||
edit: { count: 6, success: 6, fail: 0, durationMs: 3000, decisions: { [ToolCallDecision.ACCEPT]: 3, [ToolCallDecision.REJECT]: 0, [ToolCallDecision.MODIFY]: 0, [ToolCallDecision.AUTO_ACCEPT]: 3 } },
|
||||
bash: { count: 4, success: 3, fail: 1, durationMs: 2000, decisions: { [ToolCallDecision.ACCEPT]: 2, [ToolCallDecision.REJECT]: 1, [ToolCallDecision.MODIFY]: 0, [ToolCallDecision.AUTO_ACCEPT]: 1 } },
|
||||
edit: {
|
||||
count: 6,
|
||||
success: 6,
|
||||
fail: 0,
|
||||
durationMs: 3000,
|
||||
decisions: {
|
||||
[ToolCallDecision.ACCEPT]: 3,
|
||||
[ToolCallDecision.REJECT]: 0,
|
||||
[ToolCallDecision.MODIFY]: 0,
|
||||
[ToolCallDecision.AUTO_ACCEPT]: 3,
|
||||
},
|
||||
},
|
||||
bash: {
|
||||
count: 4,
|
||||
success: 3,
|
||||
fail: 1,
|
||||
durationMs: 2000,
|
||||
decisions: {
|
||||
[ToolCallDecision.ACCEPT]: 2,
|
||||
[ToolCallDecision.REJECT]: 1,
|
||||
[ToolCallDecision.MODIFY]: 0,
|
||||
[ToolCallDecision.AUTO_ACCEPT]: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
files: { totalLinesAdded: 50, totalLinesRemoved: 10 },
|
||||
|
|
@ -58,12 +87,24 @@ function makeMetrics(): SessionMetrics {
|
|||
|
||||
describe('metricsToUsageRecord', () => {
|
||||
it('includes totalLatencyMs from all models', () => {
|
||||
const record = metricsToUsageRecord('s1', '/proj', 1000, 2000, makeMetrics());
|
||||
const record = metricsToUsageRecord(
|
||||
's1',
|
||||
'/proj',
|
||||
1000,
|
||||
2000,
|
||||
makeMetrics(),
|
||||
);
|
||||
expect(record.totalLatencyMs).toBe(9500);
|
||||
});
|
||||
|
||||
it('includes per-tool totalDurationMs in byName', () => {
|
||||
const record = metricsToUsageRecord('s1', '/proj', 1000, 2000, makeMetrics());
|
||||
const record = metricsToUsageRecord(
|
||||
's1',
|
||||
'/proj',
|
||||
1000,
|
||||
2000,
|
||||
makeMetrics(),
|
||||
);
|
||||
expect(record.tools.byName['edit']!.totalDurationMs).toBe(3000);
|
||||
expect(record.tools.byName['bash']!.totalDurationMs).toBe(2000);
|
||||
});
|
||||
|
|
@ -103,7 +144,10 @@ export interface UsageSummaryRecord {
|
|||
totalCalls: number;
|
||||
totalSuccess: number;
|
||||
totalFail: number;
|
||||
byName: Record<string, { count: number; success: number; fail: number; totalDurationMs?: number }>;
|
||||
byName: Record<
|
||||
string,
|
||||
{ count: number; success: number; fail: number; totalDurationMs?: number }
|
||||
>;
|
||||
};
|
||||
files: {
|
||||
linesAdded: number;
|
||||
|
|
@ -186,6 +230,7 @@ git commit -m "feat(stats): extend UsageSummaryRecord with latency and tool dura
|
|||
### Task 2: Add delta calculation and aggregation extensions
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `packages/core/src/services/usageHistoryService.ts:283-394` (aggregateUsage)
|
||||
- Test: `packages/core/src/services/usageHistoryService.test.ts` (extend)
|
||||
|
||||
|
|
@ -194,9 +239,15 @@ git commit -m "feat(stats): extend UsageSummaryRecord with latency and tool dura
|
|||
Add to `packages/core/src/services/usageHistoryService.test.ts`:
|
||||
|
||||
```typescript
|
||||
import { aggregateUsage, type UsageSummaryRecord, type TimeRange } from './usageHistoryService.js';
|
||||
import {
|
||||
aggregateUsage,
|
||||
type UsageSummaryRecord,
|
||||
type TimeRange,
|
||||
} from './usageHistoryService.js';
|
||||
|
||||
function makeRecord(overrides: Partial<UsageSummaryRecord> = {}): UsageSummaryRecord {
|
||||
function makeRecord(
|
||||
overrides: Partial<UsageSummaryRecord> = {},
|
||||
): UsageSummaryRecord {
|
||||
return {
|
||||
version: 1,
|
||||
sessionId: 's1',
|
||||
|
|
@ -231,7 +282,10 @@ function makeRecord(overrides: Partial<UsageSummaryRecord> = {}): UsageSummaryRe
|
|||
|
||||
describe('aggregateUsage', () => {
|
||||
it('includes totalLatencyMs in aggregated result', () => {
|
||||
const records = [makeRecord({ totalLatencyMs: 2000 }), makeRecord({ totalLatencyMs: 3000 })];
|
||||
const records = [
|
||||
makeRecord({ totalLatencyMs: 2000 }),
|
||||
makeRecord({ totalLatencyMs: 3000 }),
|
||||
];
|
||||
const report = aggregateUsage(records, 'all');
|
||||
expect(report.totalLatencyMs).toBe(5000);
|
||||
});
|
||||
|
|
@ -452,6 +506,7 @@ git commit -m "feat(stats): add latency/duration/requests to aggregated report"
|
|||
### Task 3: Add delta calculation to statsDataService
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `packages/cli/src/ui/utils/statsDataService.ts`
|
||||
- Test: `packages/cli/src/ui/utils/statsDataService.test.ts` (create)
|
||||
|
||||
|
|
@ -465,7 +520,8 @@ import type { UsageSummaryRecord } from '@qwen-code/qwen-code-core';
|
|||
|
||||
// Mock loadUsageHistory to return controlled data
|
||||
vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
|
||||
const orig = await importOriginal<typeof import('@qwen-code/qwen-code-core')>();
|
||||
const orig =
|
||||
await importOriginal<typeof import('@qwen-code/qwen-code-core')>();
|
||||
return {
|
||||
...orig,
|
||||
loadUsageHistory: vi.fn(),
|
||||
|
|
@ -500,7 +556,9 @@ function makeRecord(ts: number, tokens: number): UsageSummaryRecord {
|
|||
totalCalls: 5,
|
||||
totalSuccess: 4,
|
||||
totalFail: 1,
|
||||
byName: { edit: { count: 5, success: 4, fail: 1, totalDurationMs: 1000 } },
|
||||
byName: {
|
||||
edit: { count: 5, success: 4, fail: 1, totalDurationMs: 1000 },
|
||||
},
|
||||
},
|
||||
files: { linesAdded: 10, linesRemoved: 5 },
|
||||
};
|
||||
|
|
@ -585,9 +643,12 @@ function computeDelta(
|
|||
return ((cur - prev) / prev) * 100;
|
||||
};
|
||||
|
||||
let curTokens = 0, prevTokens = 0;
|
||||
let curInput = 0, prevInput = 0;
|
||||
let curCached = 0, prevCached = 0;
|
||||
let curTokens = 0,
|
||||
prevTokens = 0;
|
||||
let curInput = 0,
|
||||
prevInput = 0;
|
||||
let curCached = 0,
|
||||
prevCached = 0;
|
||||
for (const m of Object.values(current.models)) {
|
||||
curTokens += m.totalTokens;
|
||||
curInput += m.inputTokens;
|
||||
|
|
@ -601,14 +662,22 @@ function computeDelta(
|
|||
|
||||
const curCacheRate = curInput > 0 ? (curCached / curInput) * 100 : 0;
|
||||
const prevCacheRate = prevInput > 0 ? (prevCached / prevInput) * 100 : 0;
|
||||
const curToolSuccess = current.tools.totalCalls > 0
|
||||
? (current.tools.totalSuccess / current.tools.totalCalls) * 100 : 0;
|
||||
const prevToolSuccess = previous.tools.totalCalls > 0
|
||||
? (previous.tools.totalSuccess / previous.tools.totalCalls) * 100 : 0;
|
||||
const curLatency = current.totalRequests > 0
|
||||
? current.totalLatencyMs / current.totalRequests : null;
|
||||
const prevLatency = previous.totalRequests > 0
|
||||
? previous.totalLatencyMs / previous.totalRequests : null;
|
||||
const curToolSuccess =
|
||||
current.tools.totalCalls > 0
|
||||
? (current.tools.totalSuccess / current.tools.totalCalls) * 100
|
||||
: 0;
|
||||
const prevToolSuccess =
|
||||
previous.tools.totalCalls > 0
|
||||
? (previous.tools.totalSuccess / previous.tools.totalCalls) * 100
|
||||
: 0;
|
||||
const curLatency =
|
||||
current.totalRequests > 0
|
||||
? current.totalLatencyMs / current.totalRequests
|
||||
: null;
|
||||
const prevLatency =
|
||||
previous.totalRequests > 0
|
||||
? previous.totalLatencyMs / previous.totalRequests
|
||||
: null;
|
||||
|
||||
return {
|
||||
sessions: pctChange(current.sessionCount, previous.sessionCount),
|
||||
|
|
@ -616,8 +685,10 @@ function computeDelta(
|
|||
tokens: pctChange(curTokens, prevTokens),
|
||||
cacheRate: curCacheRate - prevCacheRate,
|
||||
toolSuccess: curToolSuccess - prevToolSuccess,
|
||||
avgLatency: curLatency !== null && prevLatency !== null
|
||||
? curLatency - prevLatency : null,
|
||||
avgLatency:
|
||||
curLatency !== null && prevLatency !== null
|
||||
? curLatency - prevLatency
|
||||
: null,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
|
@ -625,7 +696,9 @@ function computeDelta(
|
|||
Add a helper to get previous range bounds:
|
||||
|
||||
```typescript
|
||||
function getPreviousRangeBounds(range: TimeRange): { start: Date; end: Date } | null {
|
||||
function getPreviousRangeBounds(
|
||||
range: TimeRange,
|
||||
): { start: Date; end: Date } | null {
|
||||
if (range === 'all') return null;
|
||||
const { start, end } = getTimeRangeBounds(range);
|
||||
const durationMs = end.getTime() - start.getTime();
|
||||
|
|
@ -653,24 +726,31 @@ export async function loadStatsData(
|
|||
const prevBounds = getPreviousRangeBounds(range);
|
||||
if (prevBounds) {
|
||||
const prevFiltered = records.filter(
|
||||
(r) => r.timestamp >= prevBounds.start.getTime() && r.timestamp < prevBounds.end.getTime(),
|
||||
(r) =>
|
||||
r.timestamp >= prevBounds.start.getTime() &&
|
||||
r.timestamp < prevBounds.end.getTime(),
|
||||
);
|
||||
const prevReport = aggregateUsage(prevFiltered, 'all');
|
||||
delta = computeDelta(report, prevReport);
|
||||
}
|
||||
|
||||
// Efficiency
|
||||
let totalInput = 0, totalCached = 0;
|
||||
let totalInput = 0,
|
||||
totalCached = 0;
|
||||
for (const m of Object.values(report.models)) {
|
||||
totalInput += m.inputTokens;
|
||||
totalCached += m.cachedTokens;
|
||||
}
|
||||
const efficiency: StatsData['efficiency'] = {
|
||||
cacheHitRate: totalInput > 0 ? (totalCached / totalInput) * 100 : 0,
|
||||
toolSuccessRate: report.tools.totalCalls > 0
|
||||
? (report.tools.totalSuccess / report.tools.totalCalls) * 100 : 0,
|
||||
avgLatencyMs: report.totalRequests > 0
|
||||
? report.totalLatencyMs / report.totalRequests : null,
|
||||
toolSuccessRate:
|
||||
report.tools.totalCalls > 0
|
||||
? (report.tools.totalSuccess / report.tools.totalCalls) * 100
|
||||
: 0,
|
||||
avgLatencyMs:
|
||||
report.totalRequests > 0
|
||||
? report.totalLatencyMs / report.totalRequests
|
||||
: null,
|
||||
};
|
||||
|
||||
// Tool leaderboard
|
||||
|
|
@ -765,6 +845,7 @@ git commit -m "feat(stats): add delta calculation, efficiency metrics, tool lead
|
|||
### Task 4: Change heatmap to token-based with today highlight
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `packages/cli/src/ui/utils/statsDataService.ts:69-82` (buildHeatmap)
|
||||
- Modify: `packages/cli/src/ui/utils/asciiCharts.ts` (HeatmapCell interface + buildHeatmapData)
|
||||
|
||||
|
|
@ -849,6 +930,7 @@ git commit -m "feat(stats): token-based heatmap with today highlight"
|
|||
### Task 5: Add 'today' to TimeRange and update range cycle
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `packages/core/src/services/usageHistoryService.ts:46,253-281`
|
||||
- Modify: `packages/cli/src/ui/components/StatsDialog.tsx:34`
|
||||
|
||||
|
|
@ -888,6 +970,7 @@ git commit -m "feat(stats): add 'today' to range cycle"
|
|||
### Task 6: Implement ActivityTab component
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `packages/cli/src/ui/components/StatsDialog.tsx`
|
||||
|
||||
- [ ] **Step 1: Replace OverviewTab with ActivityTab**
|
||||
|
|
@ -1085,6 +1168,7 @@ git commit -m "feat(stats): implement ActivityTab with KPI deltas, heatmap, tren
|
|||
### Task 7: Implement EfficiencyTab component
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `packages/cli/src/ui/components/StatsDialog.tsx`
|
||||
|
||||
- [ ] **Step 1: Replace ModelsTab with EfficiencyTab**
|
||||
|
|
@ -1243,9 +1327,11 @@ Remove the `chartFilter` state and the `e` key handler (no longer needed).
|
|||
Update the hints text:
|
||||
|
||||
```typescript
|
||||
{activeTab === 'session'
|
||||
? t('tab · esc')
|
||||
: t('tab · r dates · ←→ month · esc')}
|
||||
{
|
||||
activeTab === 'session'
|
||||
? t('tab · esc')
|
||||
: t('tab · r dates · ←→ month · esc');
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
|
@ -1260,6 +1346,7 @@ git commit -m "feat(stats): implement EfficiencyTab with perf cards, tool leader
|
|||
### Task 8: Add i18n keys
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `packages/cli/src/i18n/mustTranslateKeys.ts`
|
||||
|
||||
- [ ] **Step 1: Add new translation keys**
|
||||
|
|
@ -1304,6 +1391,7 @@ git commit -m "feat(stats): add i18n keys for new dashboard tabs"
|
|||
### Task 9: Clean up unused code and verify
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `packages/cli/src/ui/components/StatsDialog.tsx`
|
||||
|
||||
- [ ] **Step 1: Remove dead code**
|
||||
|
|
@ -1323,6 +1411,7 @@ Expected: All pass (fix any snapshot updates with `--update` if needed).
|
|||
- [ ] **Step 4: Visual verification**
|
||||
|
||||
Run: `npm run dev`, then type `/stats`:
|
||||
|
||||
- Verify Session tab unchanged
|
||||
- Verify Activity tab shows KPI row with deltas, token heatmap with today highlight, sparkline, projects
|
||||
- Verify Efficiency tab shows performance cards, tool leaderboard with bars, model table, code impact
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
**Tech Stack:** TypeScript, Vitest, Node `fs.promises`, existing `Storage.getGlobalDebugDir()`, existing `updateSymlink` helper.
|
||||
|
||||
**Reference spec:** `docs/superpowers/specs/2026-05-26-daemon-logger-design.md`
|
||||
**Reference spec:** `docs/design/2026-05-26-daemon-logger-design.md`
|
||||
|
||||
**Test harness:** `vitest run` from each package; for a single file: `cd packages/<pkg> && npx vitest run <relative-path>`.
|
||||
|
||||
|
|
@ -47,7 +47,7 @@ Expected: all pass. (If not, baseline is broken — stop and report.)
|
|||
|
||||
- [ ] **Step 3: Skim the spec**
|
||||
|
||||
Read `docs/superpowers/specs/2026-05-26-daemon-logger-design.md` end-to-end. Key sections to internalize: §3 (modules), §4 (path), §5 (API), §6 (format + tee semantics), §7 (boot/shutdown), §11 (error handling).
|
||||
Read `docs/design/2026-05-26-daemon-logger-design.md` end-to-end. Key sections to internalize: §3 (modules), §4 (path), §5 (API), §6 (format + tee semantics), §7 (boot/shutdown), §11 (error handling).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
**Tech Stack:** TypeScript, Vitest, Express (REST routes), JSON-RPC (ACP), supertest (integration)
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-05-27-daemon-workspace-service-design.md`
|
||||
**Spec:** `docs/design/2026-05-27-daemon-workspace-service-design.md`
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -61,7 +61,7 @@ helpers must be idempotent and must not double-append streamed Feishu content.
|
|||
|
||||
**Files:**
|
||||
|
||||
- Read: `.qwen/design/2026-07-01-channel-lifecycle-status-adapters.md`
|
||||
- Read: `docs/design/2026-07-01-channel-lifecycle-status-adapters.md`
|
||||
- Read: `packages/channels/base/src/types.ts`
|
||||
- Read: `packages/channels/base/src/ChannelBase.ts`
|
||||
|
||||
|
|
@ -308,8 +308,7 @@ Add the behavior test:
|
|||
it('maps lifecycle start and terminal events to typing state', () => {
|
||||
const channel = createChannel();
|
||||
const setTyping = vi.fn().mockResolvedValue(undefined);
|
||||
(channel as unknown as { setTyping: typeof setTyping }).setTyping =
|
||||
setTyping;
|
||||
(channel as unknown as { setTyping: typeof setTyping }).setTyping = setTyping;
|
||||
|
||||
const baseEvent = {
|
||||
channelName: 'weixin',
|
||||
|
|
@ -485,8 +484,9 @@ it('maps lifecycle start and terminal events to the eye reaction', () => {
|
|||
it('does not attach lifecycle reactions without a conversation id', () => {
|
||||
const channel = createChannel();
|
||||
const attachReaction = vi.fn().mockResolvedValue(undefined);
|
||||
(channel as unknown as { attachReaction: typeof attachReaction })
|
||||
.attachReaction = attachReaction;
|
||||
(
|
||||
channel as unknown as { attachReaction: typeof attachReaction }
|
||||
).attachReaction = attachReaction;
|
||||
|
||||
getLifecycleHook(channel)({
|
||||
type: 'started',
|
||||
|
|
@ -973,9 +973,7 @@ state:
|
|||
const terminalStatus = cs.terminalStatus || 'failed';
|
||||
const terminalLabel = this.statusLabelFor(terminalStatus);
|
||||
const text = cs.accumulatedText
|
||||
? (atPrefix
|
||||
? `${atPrefix}\n\n${cs.accumulatedText}`
|
||||
: cs.accumulatedText) +
|
||||
? (atPrefix ? `${atPrefix}\n\n${cs.accumulatedText}` : cs.accumulatedText) +
|
||||
'\n\n---\n' +
|
||||
`*${terminalLabel}*`
|
||||
: (atPrefix ? `${atPrefix}\n\n` : '') + `*${terminalLabel}*`;
|
||||
|
|
@ -20,6 +20,7 @@
|
|||
## Task 1: Add Channel Metadata And Lifecycle Types
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `packages/channels/base/src/types.ts`
|
||||
- Test: `packages/channels/base/src/ChannelBase.test.ts`
|
||||
|
||||
|
|
@ -199,8 +200,8 @@ Add private fields after `protected name: string;`:
|
|||
Set them in the constructor after `this.proxy = options?.proxy;`:
|
||||
|
||||
```ts
|
||||
this.identity = this.resolveIdentity(name, config);
|
||||
this.memoryScope = this.resolveMemoryScope(name, config);
|
||||
this.identity = this.resolveIdentity(name, config);
|
||||
this.memoryScope = this.resolveMemoryScope(name, config);
|
||||
```
|
||||
|
||||
Add methods near other protected hooks:
|
||||
|
|
@ -247,15 +248,15 @@ Add methods near other protected hooks:
|
|||
Emit `started` after `this.activePrompts.set(sessionId, promptState);`:
|
||||
|
||||
```ts
|
||||
this.emitTaskLifecycle({
|
||||
type: 'started',
|
||||
channelName: this.name,
|
||||
chatId: envelope.chatId,
|
||||
sessionId,
|
||||
messageId: envelope.messageId,
|
||||
identity: this.identity,
|
||||
memoryScope: this.memoryScope,
|
||||
});
|
||||
this.emitTaskLifecycle({
|
||||
type: 'started',
|
||||
channelName: this.name,
|
||||
chatId: envelope.chatId,
|
||||
sessionId,
|
||||
messageId: envelope.messageId,
|
||||
identity: this.identity,
|
||||
memoryScope: this.memoryScope,
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run test to verify Task 1 passes**
|
||||
|
|
@ -272,6 +273,7 @@ Expected: both new metadata tests pass; existing tests still pass.
|
|||
## Task 2: Add Prompt Boundary And Status Visibility
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `packages/channels/base/src/ChannelBase.ts`
|
||||
- Test: `packages/channels/base/src/ChannelBase.test.ts`
|
||||
|
||||
|
|
@ -301,13 +303,13 @@ it('prepends channel boundary metadata before custom instructions once per sessi
|
|||
expect(prompt).toContain('Channel identity:');
|
||||
expect(prompt).toContain('- id: ops-agent');
|
||||
expect(prompt).toContain('- display name: Ops Agent');
|
||||
expect(prompt).toContain(
|
||||
'- description: Coordinates repository operations.',
|
||||
);
|
||||
expect(prompt).toContain('- description: Coordinates repository operations.');
|
||||
expect(prompt).toContain('Memory scope:');
|
||||
expect(prompt).toContain('- namespace: qwen-tag:ops');
|
||||
expect(prompt).toContain('- mode: metadata-only');
|
||||
expect(prompt).toContain('- storage isolation: not enforced by this version.');
|
||||
expect(prompt).toContain(
|
||||
'- storage isolation: not enforced by this version.',
|
||||
);
|
||||
expect(prompt.indexOf('Channel identity:')).toBeLessThan(
|
||||
prompt.indexOf('Be concise.'),
|
||||
);
|
||||
|
|
@ -378,25 +380,25 @@ Add private method near metadata resolvers:
|
|||
Replace the existing instruction block:
|
||||
|
||||
```ts
|
||||
if (this.config.instructions && !this.instructedSessions.has(sessionId)) {
|
||||
promptText = `${this.config.instructions}\n\n${promptText}`;
|
||||
this.instructedSessions.add(sessionId);
|
||||
}
|
||||
if (this.config.instructions && !this.instructedSessions.has(sessionId)) {
|
||||
promptText = `${this.config.instructions}\n\n${promptText}`;
|
||||
this.instructedSessions.add(sessionId);
|
||||
}
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```ts
|
||||
if (
|
||||
this.shouldPrependChannelBoundaryPrompt() &&
|
||||
!this.instructedSessions.has(sessionId)
|
||||
) {
|
||||
const prefix = this.config.instructions
|
||||
? `${this.channelBoundaryPrompt()}\n\n${this.config.instructions}`
|
||||
: this.channelBoundaryPrompt();
|
||||
promptText = `${prefix}\n\n${promptText}`;
|
||||
this.instructedSessions.add(sessionId);
|
||||
}
|
||||
if (
|
||||
this.shouldPrependChannelBoundaryPrompt() &&
|
||||
!this.instructedSessions.has(sessionId)
|
||||
) {
|
||||
const prefix = this.config.instructions
|
||||
? `${this.channelBoundaryPrompt()}\n\n${this.config.instructions}`
|
||||
: this.channelBoundaryPrompt();
|
||||
promptText = `${prefix}\n\n${promptText}`;
|
||||
this.instructedSessions.add(sessionId);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add status lines**
|
||||
|
|
@ -429,6 +431,7 @@ Expected: prompt boundary and status tests pass; existing instruction tests are
|
|||
## Task 3: Emit Full Task Lifecycle Events
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `packages/channels/base/src/ChannelBase.ts`
|
||||
- Test: `packages/channels/base/src/ChannelBase.test.ts`
|
||||
|
||||
|
|
@ -538,25 +541,25 @@ Update `started` to spread `this.lifecycleBase(...)`.
|
|||
In `bridgeToolCallListener`, after `this.onToolCall(target.chatId, event);`, add:
|
||||
|
||||
```ts
|
||||
this.emitTaskLifecycle({
|
||||
type: 'tool_call',
|
||||
channelName: this.name,
|
||||
chatId: target.chatId,
|
||||
sessionId: event.sessionId,
|
||||
toolCall: event,
|
||||
identity: this.identity,
|
||||
memoryScope: this.memoryScope,
|
||||
});
|
||||
this.emitTaskLifecycle({
|
||||
type: 'tool_call',
|
||||
channelName: this.name,
|
||||
chatId: target.chatId,
|
||||
sessionId: event.sessionId,
|
||||
toolCall: event,
|
||||
identity: this.identity,
|
||||
memoryScope: this.memoryScope,
|
||||
});
|
||||
```
|
||||
|
||||
In `onChunk`, after `this.onResponseChunk(...)`, add:
|
||||
|
||||
```ts
|
||||
this.emitTaskLifecycle({
|
||||
type: 'text_chunk',
|
||||
...this.lifecycleBase(envelope.chatId, sessionId, envelope.messageId),
|
||||
chunk,
|
||||
});
|
||||
this.emitTaskLifecycle({
|
||||
type: 'text_chunk',
|
||||
...this.lifecycleBase(envelope.chatId, sessionId, envelope.messageId),
|
||||
chunk,
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Emit cancellation events**
|
||||
|
|
@ -564,31 +567,31 @@ In `onChunk`, after `this.onResponseChunk(...)`, add:
|
|||
In `/cancel`, after `active.cancelled = true;`, add:
|
||||
|
||||
```ts
|
||||
this.emitTaskLifecycle({
|
||||
type: 'cancelled',
|
||||
...this.lifecycleBase(active.chatId, activeSessionId, active.messageId),
|
||||
reason: 'cancel_command',
|
||||
});
|
||||
this.emitTaskLifecycle({
|
||||
type: 'cancelled',
|
||||
...this.lifecycleBase(active.chatId, activeSessionId, active.messageId),
|
||||
reason: 'cancel_command',
|
||||
});
|
||||
```
|
||||
|
||||
In `/clear`, when `active` exists before waiting, add:
|
||||
|
||||
```ts
|
||||
this.emitTaskLifecycle({
|
||||
type: 'cancelled',
|
||||
...this.lifecycleBase(active.chatId, id, active.messageId),
|
||||
reason: 'clear',
|
||||
});
|
||||
this.emitTaskLifecycle({
|
||||
type: 'cancelled',
|
||||
...this.lifecycleBase(active.chatId, id, active.messageId),
|
||||
reason: 'clear',
|
||||
});
|
||||
```
|
||||
|
||||
In `steer`, after `active.cancelled = true;`, add:
|
||||
|
||||
```ts
|
||||
this.emitTaskLifecycle({
|
||||
type: 'cancelled',
|
||||
...this.lifecycleBase(active.chatId, sessionId, active.messageId),
|
||||
reason: 'steer',
|
||||
});
|
||||
this.emitTaskLifecycle({
|
||||
type: 'cancelled',
|
||||
...this.lifecycleBase(active.chatId, sessionId, active.messageId),
|
||||
reason: 'steer',
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Emit completed and failed events**
|
||||
|
|
@ -596,10 +599,10 @@ In `steer`, after `active.cancelled = true;`, add:
|
|||
In the prompt `try` block, after response delivery finishes and only when `!promptState.cancelled`, add:
|
||||
|
||||
```ts
|
||||
this.emitTaskLifecycle({
|
||||
type: 'completed',
|
||||
...this.lifecycleBase(envelope.chatId, sessionId, envelope.messageId),
|
||||
});
|
||||
this.emitTaskLifecycle({
|
||||
type: 'completed',
|
||||
...this.lifecycleBase(envelope.chatId, sessionId, envelope.messageId),
|
||||
});
|
||||
```
|
||||
|
||||
Convert the `try/finally` into `try/catch/finally`:
|
||||
|
|
@ -629,6 +632,7 @@ Expected: all `ChannelBase` tests pass.
|
|||
## Task 4: Config Parsing, Exports, And Verification
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `packages/cli/src/commands/channel/config-utils.ts`
|
||||
- Modify: `packages/cli/src/commands/channel/config-utils.test.ts`
|
||||
- Modify: `packages/channels/base/src/index.ts`
|
||||
|
|
@ -620,7 +620,7 @@ describe('qwen serve — POST /session/:id/continue', () => {
|
|||
describe('qwen serve — prompt clientId admission', () => {
|
||||
// Validates the three real-daemon behaviors that DaemonSessionClient's
|
||||
// clientId self-heal relies on (see
|
||||
// docs/superpowers/specs/2026-06-24-daemon-clientid-self-heal-design.md).
|
||||
// docs/design/2026-06-24-daemon-clientid-self-heal-design.md).
|
||||
// Model-free: prompt admission (where invalid_client_id is decided) runs
|
||||
// before any model call, so promptNonBlocking returns 202 on acceptance
|
||||
// without reaching the (unreachable, fake) model.
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
* Mirrors Claude Code 2.1.168's `.claude/agents/<name>.md` schema verbatim so
|
||||
* a user can drop a Claude Code agent file into `.qwen/agents/` and have it
|
||||
* parse identically. The internal verification source (DL7 / Ig5 / GN / kc /
|
||||
* P37 / _Y) is documented in `docs/declarative-agents-port.md`.
|
||||
* P37 / _Y) is documented in `docs/design/declarative-agents-port.md`.
|
||||
*
|
||||
* Parsing follows DL7's "lenient" posture: invalid optional fields are dropped
|
||||
* to undefined rather than thrown — the caller layer is responsible for
|
||||
|
|
|
|||
|
|
@ -678,7 +678,7 @@ export class SubagentManager {
|
|||
}
|
||||
|
||||
// Nested CC fields. Safe to round-trip with the eemeli/yaml parser; the
|
||||
// previous skip-list carve-out is gone (see docs/yaml-parser-replacement.md).
|
||||
// previous skip-list carve-out is gone (see docs/design/yaml-parser-replacement.md).
|
||||
if (config.mcpServers && Object.keys(config.mcpServers).length > 0) {
|
||||
frontmatter['mcpServers'] = config.mcpServers;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -202,7 +202,7 @@ function parseSimple(yamlString: string): Record<string, unknown> {
|
|||
* arbitrarily nested values (e.g. CC-style `mcpServers` / `hooks`) round-trip
|
||||
* cleanly. The previous hand-rolled formatter only walked one level of
|
||||
* nesting and emitted `[object Object]` for anything deeper, corrupting the
|
||||
* file on save — see `docs/yaml-parser-replacement.md` for the audit.
|
||||
* file on save — see `docs/design/yaml-parser-replacement.md` for the audit.
|
||||
*
|
||||
* `lineWidth: 0` disables automatic line wrapping so multi-line strings are
|
||||
* preserved as-is, matching the stable-output posture the test suite assumes.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue