mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10: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 <[email protected]> Co-authored-by: Claude Opus 4.8 <[email protected]>
This commit is contained in:
42 files changed
+324
-224
No files matched your search
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+20
-20
@@ -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,
|
||||
+20
-20
@@ -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
|
||||
@@ -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.
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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.
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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.
|
||||
+8
-6
@@ -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
|
||||
File renamed without changes.
@@ -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`.
|
||||
+123
-34
@@ -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
|
||||
File renamed without changes.
+2
-2
@@ -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).
|
||||
|
||||
---
|
||||
|
||||
+1
-1
@@ -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`
|
||||
|
||||
---
|
||||
|
||||
File renamed without changes.
File renamed without changes.
+6
-8
@@ -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}*`;
|
||||
File renamed without changes.
+66
-62
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user