diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 74aa649c0..1e62f30e3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,6 +8,37 @@ on: - master jobs: + web-tests: + name: web-tests (linux-x64) + runs-on: ubuntu-22.04 + defaults: + run: + working-directory: web + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: '10.32.1' + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: pnpm + cache-dependency-path: web/pnpm-lock.yaml + + - name: Install web dependencies + run: pnpm install --frozen-lockfile + + - name: Test web UI + run: pnpm test + + - name: Build web UI + run: pnpm build + unit-tests: name: unit-tests (linux-x64) runs-on: ubuntu-22.04 diff --git a/docs/daemon-api.md b/docs/daemon-api.md index 4ba700d15..982730709 100644 --- a/docs/daemon-api.md +++ b/docs/daemon-api.md @@ -3867,6 +3867,15 @@ close event. } ``` +Completed AskUserQuestion results carry UI-only +`metadata.ask_user_question_result` in `tool_end` events and persisted tool +messages. Successful answers contain ordered `items` with `question` and +`answer` text. Explicit rejection contains `{"cancelled":true,"items":[]}`; +its `success` remains `false` and its provider-visible output remains +`[Error] User declined to answer questions.`. Clients can use this namespaced +marker to retain cancellation feedback after reloading history. A generic +tool's unrelated `metadata.cancelled` flag is not question-result metadata. + #### AskUserQuestion answer policy (`agent_loop.question_policy`) `question_request` / `question_closed` behavior depends on the configured diff --git a/openspec/changes/style-ask-question-result-card/design.md b/openspec/changes/style-ask-question-result-card/design.md new file mode 100644 index 000000000..076593ca1 --- /dev/null +++ b/openspec/changes/style-ask-question-result-card/design.md @@ -0,0 +1,65 @@ +## Context + +AskUserQuestion already has two UI surfaces: the active picker/overlay while the agent waits for input, and the completed tool result after the user answers. This change only targets the completed result. + +The current model-visible output is a single-line text string produced by `format_ask_answers()`. That string is part of the tool-result contract and feeds back to the provider. Desktop/web also receives structured tool lifecycle events, but successful `tool_end` events normally omit output and rely on summary or generic tool folding. TUI renders `tool_result` text directly when a tool has no `ToolSummary`. + +## Goals / Non-Goals + +**Goals:** + +- Keep the completed AskUserQuestion result as tool transcript content rather than merging it into the assistant bubble. +- Render desktop/web completed results with a compact card matching the provided visual direction. +- Preserve complete answer text for the model and persisted history. +- Make the result resumable from persisted session metadata. +- Keep TUI behavior readable with the existing tool-result text path and a better tool-call preview. + +**Non-Goals:** + +- No picker/overlay redesign while the question is pending. +- No WebSocket protocol change for `question_request` or `question_answer`. +- No change to AskUserQuestion validation, answer semantics, or failure output. +- No mandatory TUI graphical card matching every desktop detail such as hover tooltips. + +## Decisions + +### Store structured Q/A as ToolResult metadata + +Add a generic `ToolResult::metadata` JSON object and have `ToolExecutor::format_tool_result()` copy it to the persisted `ChatMessage.metadata`. `AgentLoop` also emits the same metadata in `tool_end` payloads for live desktop/web rendering. + +Rationale: this keeps the model-visible output stable while giving UI code a structured, durable source of truth. Parsing the existing answer string would be fragile because questions and answers may contain quotes, commas, or translated text. + +Alternative considered: encode the UI data in `ToolSummary.metrics`. Rejected because metrics are flat label/value pairs and would not represent ordered Q/A rows cleanly. + +### Let ToolBlock render the card + +Desktop/web should treat the completed AskUserQuestion answer as a specialized tool block. `ToolBlock` can switch to a `QuestionResultCard` when the tool entry contains `askUserQuestionResult` metadata. + +Rationale: the user accepted keeping it as tool display. This avoids reshaping assistant messages and preserves existing activity grouping rules for other tools. + +### Preserve full output for copy and model use + +The visible card clamps individual Q/A text to two lines. The underlying output and metadata keep full text. Native tooltips are added only for actually clamped text by measuring rendered height after layout. + +Rationale: the UI remains compact without losing data for provider context, copy actions, resume, or future rendering. + +### Keep TUI on the text path + +Do not attach `ToolSummary` to AskUserQuestion. The TUI already preserves tool-result line breaks and can show the improved text result. The shared preview path should show a readable AskUserQuestion call summary. + +Rationale: adding summary would collapse the result into a single line and hide the Q/A unless expanded. + +## Risks / Trade-offs + +- [Risk] Generic `ToolResult::metadata` may be overused by future tools. Mitigation: document it as UI/persistence metadata and keep AskUserQuestion keys namespaced. +- [Risk] Line-clamp tooltip detection depends on DOM measurement. Mitigation: isolate it in a small component and fall back to no tooltip when measurement is unavailable. +- [Risk] Persisted sessions need the card after resume. Mitigation: normalize `metadata.ask_user_question_result` from persisted tool messages into tool entries. + +## PR #49 persistence repair + +- Use the existing shared ToolBlock renderer for both submission and cancellation. The PR's proposed ChatView `renderAfterItem` extension and feedback modules were absent from its committed tree; a second per-view card renderer would duplicate the existing result surface. +- Explicit cancellation carries `ask_user_question_result.cancelled=true` and an empty `items` array. Preserve `success=false` and the existing provider-visible rejection text. Legacy rejection records without this metadata continue to use their generic tool output. +- Normalize only namespaced question-result metadata. Generic tool metadata such as `cancelled=true` must not create an AskUserQuestion card. +- Treat structured question metadata as the source of truth for card visibility, including renamed tools and history slices missing the assistant call. Cancellation takes precedence over stale answer rows and remains outside processed summaries. +- Resolve persisted tool names in message order, clearing pending calls at user-turn boundaries and replacing stale names when a later assistant call reuses an ID. Consume each result once; preserve explicit tool names and never inspect future calls. +- Exercise the actual ToolBlock JSX with React server rendering in the persistence tests. A synthetic sequence of labels alone cannot prove that the application renders the card. diff --git a/openspec/changes/style-ask-question-result-card/proposal.md b/openspec/changes/style-ask-question-result-card/proposal.md new file mode 100644 index 000000000..35b3b57e4 --- /dev/null +++ b/openspec/changes/style-ask-question-result-card/proposal.md @@ -0,0 +1,29 @@ +## Why + +AskUserQuestion answers are currently surfaced as ordinary tool output, which makes multi-question confirmations hard to scan and easy to lose inside generic tool transcript chrome. The user-facing desktop/web result should preserve the tool result location while rendering the answered questions as a compact confirmation card. + +## What Changes + +- Add structured UI metadata to successful AskUserQuestion tool results, containing the answered question/answer pairs in input order. +- Render AskUserQuestion results in desktop/web tool blocks as a collapsible "confirmed N items" card with compact Q/A rows, answer emphasis, separators, two-line clamping, and native tooltip for clamped text. +- Keep the model-visible tool output contract unchanged so providers still receive the existing full answer text. +- Improve the AskUserQuestion tool invocation preview so TUI/desktop history shows a readable "asking N confirmation items" line instead of raw JSON. +- Preserve explicit cancellation feedback across history reloads and keep both feedback states in the existing shared tool renderer; make the PR #49 frontend tests self-contained. + +## Capabilities + +### New Capabilities + +None. + +### Modified Capabilities + +- `ask-user-question-tool`: successful tool results carry structured UI metadata and a readable tool-call preview while preserving the model-visible output contract. +- `desktop-ask-user-question-ui`: desktop/web renders answered AskUserQuestion results as a collapsible confirmation card inside the normal tool display. + +## Impact + +- C++ tool result plumbing: `ToolResult`, `ToolExecutor`, `AgentLoop`, web tool event payloads. +- AskUserQuestion tool formatting and unit tests. +- Desktop/web transcript reduction and `ToolBlock` rendering. +- Web tests for transcript state and Q/A card behavior. diff --git a/openspec/changes/style-ask-question-result-card/review.md b/openspec/changes/style-ask-question-result-card/review.md new file mode 100644 index 000000000..2da895dc7 --- /dev/null +++ b/openspec/changes/style-ask-question-result-card/review.md @@ -0,0 +1,27 @@ +# PR #49 审查与修复 + +审查原始提交:`63b77f76f5edf713acb1a7cdae8b861d42c4961f`。 +合入前 master:`ff5546960265fa049887b77d4a06027d31e05eee`。 + +## 已修复问题 + +1. **P1:前端测试无法启动。** `runTests.js` 导入了未提交的 `questionFeedback.test.js`,持久化测试也依赖不存在的 `questionFeedback.js`。实际运行 `pnpm test` 得到 `ERR_MODULE_NOT_FOUND`。已补齐共享反馈逻辑和测试。 +2. **P1:测试中的取消数据没有真实生产来源。** `make_rejected_ask_result()` 只返回失败与英文错误文本,没有测试使用的 `ask_user_question_result.cancelled`。已在真实取消路径写入 UI metadata,并验证异步提问结果及 `format_tool_result` 持久化,同时保持 provider 输出和 TUI 文本回退不变。 +3. **P1:测试要求的渲染接口不存在,取消结果也没有卡片。** 仓库实际使用 `ToolBlock` 渲染确认卡,PR 的 `renderAfterItem` 断言针对未提交的另一套实现。已沿用共享 `ToolBlock` 显示提交/取消反馈,以 React 渲染实际 JSX 验证各场景,取消卡没有无效的展开操作。 +4. **P2:中断调用复用 ID 后仍可能串工具名。** 原实现只记录第一个未消费的 ID;旧调用没有结果时,新调用会继承旧名称。新增回归确实得到 `bash` 而非预期 `file_read`。现按最近前序调用更新,并在用户回合边界清理未完成映射。 +5. **P2:取消卡会丢失或错误套用。** 缺少原始调用、工具改名且取消结果没有回答项时,会被折叠进活动摘要;普通工具的裸 `metadata.cancelled` 又可能被误判为问答。现仅解析命名空间下的问答结果,并以结构化反馈决定卡片是否常驻。 + +同时保留原 PR 的初始化列表兼容修复,并在 GitHub PR/master 工作流增加前端测试及构建。原来的绿色检查只有 C++,无法发现上述缺失模块。 + +## 验证 + +- `pnpm test`:通过,日志包含 2323 项通过记录。 +- `pnpm build`:通过,产物正则兼容检查通过。 +- `pnpm i18n:catalog`:重新生成中英文目录,仅增加取消反馈文案。 +- Windows `MinSizeRel` 测试目标编译通过;提问、主题确认、轮次引导、工具名映射、取消结果持久化等 219 项原生回归全部通过。 +- Ubuntu GCC 11:本次涉及的工具实现及测试文件 `-fsyntax-only` 检查通过。 +- `openspec validate style-ask-question-result-card --strict`:变更校验通过。工具提示既有主 spec 含 delta 标题,影响后续归档;本次未执行归档。 +- GitHub workflow YAML 解析通过,包含 `web-tests` 与 `unit-tests` 两个任务。 +- `QueueCardList.jsx` 与 `globals.css` 的原有未提交修改按 SHA-256 核对保持不变。 + +本次原生验证为上述定向回归,未宣称完整 CTest 套件通过;最终 Linux 全量结果由推送后的 CI 继续验证。 diff --git a/openspec/changes/style-ask-question-result-card/specs/ask-user-question-tool/spec.md b/openspec/changes/style-ask-question-result-card/specs/ask-user-question-tool/spec.md new file mode 100644 index 000000000..0506c50ba --- /dev/null +++ b/openspec/changes/style-ask-question-result-card/specs/ask-user-question-tool/spec.md @@ -0,0 +1,65 @@ +## ADDED Requirements + +### Requirement: AskUserQuestion result includes structured UI metadata + +Successful `AskUserQuestion` results SHALL include UI-only metadata under `ask_user_question_result` containing the answered question/answer pairs in the same order as the input `questions`. The metadata MUST NOT replace or truncate `ToolResult.output`, and provider-visible tool output SHALL continue to follow the existing output contract. + +#### Scenario: Successful answers carry ordered result rows +- **WHEN** AskUserQuestion completes successfully with two answered questions +- **THEN** the returned tool result metadata MUST contain `ask_user_question_result.items` with two entries +- **AND** each entry MUST contain the original `question` text and final `answer` text +- **AND** the item order MUST match the input question order + +#### Scenario: Model-visible output remains unchanged +- **WHEN** AskUserQuestion completes successfully +- **THEN** `ToolResult.output` MUST remain the existing full answer string consumed by the model +- **AND** the metadata MUST NOT require the provider adapter to parse UI fields + +### Requirement: AskUserQuestion tool call preview is readable + +The shared tool-call preview builder SHALL return a compact readable preview for `AskUserQuestion` calls instead of falling back to raw JSON. + +#### Scenario: Preview shows question count +- **WHEN** `ToolExecutor::build_tool_call_preview("AskUserQuestion", args)` receives valid arguments containing three questions +- **THEN** the preview MUST include `AskUserQuestion` +- **AND** the preview MUST indicate that three confirmation questions/items are being asked + +#### Scenario: Preview includes first question when available +- **WHEN** the first question text is present in the arguments +- **THEN** the preview SHOULD include a truncated form of that first question + +## MODIFIED Requirements + +### Requirement: 输出契约 + +成功应答时,工具 MUST 返回 `ToolResult{ success=true }`,`output` 为单行字符串: + +``` +User has answered your questions: "Q1"="A1", "Q2"="A2", ... +``` + +- 顺序与输入 `questions` 顺序一致; +- multi-select 答案把多个选中的 label 以 `", "` 拼接成单值; +- "Other..." 答案使用用户键入的原始文本,不附加任何前缀; +- 答案和问题文本中若自身含有 `"`,实现 MUST NOT 做额外转义(与上游一致,作为已记录的已知行为); +- 工具 MUST NOT 填充 `ToolSummary`(让 TUI 走默认 fold 路径); +- 成功结果 MUST 同时携带 `ask_user_question_result` UI metadata,用于桌面/Web 工具卡片渲染,但该 metadata MUST NOT 改变 `output` 文本。 + +#### Scenario: 单题单选输出 + +- **WHEN** 成功单选应答单题,answer 为 "axios" +- **THEN** `ToolResult.output` MUST 等于 `User has answered your questions: "Which library?"="axios"` +- **AND** `ToolResult.metadata.ask_user_question_result.items[0]` MUST contain `question:"Which library?"` and `answer:"axios"` + +#### Scenario: 多题混合输出 + +- **WHEN** 题 1 单选 "axios",题 2 多选 "TypeScript" + "Prettier" +- **THEN** `ToolResult.output` MUST 等于 `User has answered your questions: "Q1"="axios", "Q2"="TypeScript, Prettier"`(其中 Q1 / Q2 为用户原始问题文本) +- **AND** `ToolResult.metadata.ask_user_question_result.items` MUST contain two ordered Q/A entries + +#### Scenario: 拒绝路径输出 + +- **WHEN** 用户按 Esc 或 agent abort +- **THEN** `ToolResult` MUST 满足 `success=false` 且 `output == "[Error] User declined to answer questions."` +- **AND** the result MUST contain UI-only `ask_user_question_result` metadata with `cancelled:true` and an empty `items` array +- **AND** persistence and live tool events MUST retain this metadata without changing the rejection output text diff --git a/openspec/changes/style-ask-question-result-card/specs/desktop-ask-user-question-ui/spec.md b/openspec/changes/style-ask-question-result-card/specs/desktop-ask-user-question-ui/spec.md new file mode 100644 index 000000000..d878bfa8e --- /dev/null +++ b/openspec/changes/style-ask-question-result-card/specs/desktop-ask-user-question-ui/spec.md @@ -0,0 +1,80 @@ +## ADDED Requirements + +### Requirement: Answered AskUserQuestion renders as confirmation card + +Desktop / Web UI SHALL render completed successful AskUserQuestion results as a compact collapsible confirmation card inside the normal tool display. The card SHALL use the structured `ask_user_question_result` metadata when present. + +#### Scenario: Card title shows confirmed count +- **WHEN** a completed AskUserQuestion tool result contains three Q/A items +- **THEN** the visible tool block MUST show a header titled `已确认 3 项` +- **AND** the header MUST include a green confirmation icon and a collapse/expand affordance + +#### Scenario: Card body shows compact Q/A rows +- **WHEN** the confirmation card is expanded +- **THEN** each item MUST render a Q row and an A row using a fixed-width marker column +- **AND** question text MUST be visually weaker than answer text +- **AND** answer markers and the confirmation icon MUST use the success/green theme color +- **AND** items MUST be separated by subtle divider lines rather than large vertical gaps + +#### Scenario: Card can collapse +- **WHEN** the user clicks the card header +- **THEN** the Q/A body MUST hide or show +- **AND** the header state text and chevron direction MUST update to match the collapsed state + +#### Scenario: Long text is clamped with native tooltip +- **WHEN** a question or answer visually overflows two lines +- **THEN** the visible text MUST be clamped to at most two lines with an ellipsis +- **AND** hovering that clamped text MUST expose the full text through the native tooltip +- **AND** unclamped text MUST NOT receive the extra tooltip or help cursor + +#### Scenario: Theme and width compatibility +- **WHEN** the card renders in light or dark theme and within a narrow desktop WebView +- **THEN** it MUST use existing theme tokens for background, border, text, and success colors +- **AND** long URLs or English tokens MUST wrap or break without creating horizontal page overflow + +### Requirement: AskUserQuestion result card survives resume + +Desktop / Web UI SHALL render the same confirmation card after loading persisted session history when the stored tool result message contains `metadata.ask_user_question_result`. + +#### Scenario: Persisted tool message restores card +- **WHEN** session history contains a tool message with `metadata.ask_user_question_result.items` +- **THEN** transcript normalization MUST create a completed tool entry that carries those Q/A items +- **AND** `ToolBlock` MUST render the confirmation card instead of the generic completed-tool fallback + +### Requirement: AskUserQuestion result card remains visible in processed history + +Desktop / Web transcript projection SHALL keep AskUserQuestion confirmation cards visible as standalone tool blocks and SHALL NOT fold them into `已处理` processed activity summaries. + +#### Scenario: Card before final answer stays visible +- **WHEN** a turn contains process activity, an AskUserQuestion confirmation card, more process activity, and a final assistant answer +- **THEN** projection MUST split process activity into processed summaries around the confirmation card +- **AND** the confirmation card MUST remain a top-level visible tool item +- **AND** the confirmation card MUST NOT appear inside any processed summary `collapsedItems` + +#### Scenario: Card before task completion stays visible +- **WHEN** a turn contains process activity, an AskUserQuestion confirmation card, more process activity, a final assistant answer, and a successful task completion +- **THEN** projection MUST keep the confirmation card as a top-level visible tool item before the final answer +- **AND** processed summaries MUST cover only the process activity before and after the confirmation card +- **AND** the completion summary MUST remain visible after the final answer + +### Requirement: Explicit cancellation feedback survives transcript replacement + +Desktop / Web UI SHALL display `已取消全部回答` in the shared tool renderer when a completed tool result contains namespaced `ask_user_question_result.cancelled=true` metadata. Cancellation SHALL remain a standalone tool result even when its tool name is missing or rewritten, and SHALL NOT display successful answer rows or a confirmation count. + +#### Scenario: Cancellation survives reload and self-heal +- **WHEN** a live cancelled question is replaced by persisted history during reload, self-heal, or transcript replacement +- **THEN** exactly one cancellation card MUST remain at that tool result's transcript position +- **AND** continuing the conversation MUST NOT fold that card into processed activity + +#### Scenario: Other tool metadata is not question feedback +- **WHEN** a different tool carries a plain `metadata.cancelled` flag without namespaced question-result metadata +- **THEN** it MUST retain the generic tool rendering and MUST NOT create a question feedback card + +### Requirement: Persisted tool names respect invocation order and turn boundaries + +Transcript normalization SHALL preserve explicit tool names and otherwise recover a tool result's name only from an unconsumed preceding call in the same user turn. A later assistant call reusing an ID SHALL replace stale pending metadata for that ID. + +#### Scenario: Interrupted call ID is reused +- **WHEN** an earlier call has no result and a later call reuses its ID +- **THEN** the later result MUST use the later call's tool name +- **AND** no result MUST borrow a tool name from a future call or an earlier user turn diff --git a/openspec/changes/style-ask-question-result-card/tasks.md b/openspec/changes/style-ask-question-result-card/tasks.md new file mode 100644 index 000000000..61ec4d051 --- /dev/null +++ b/openspec/changes/style-ask-question-result-card/tasks.md @@ -0,0 +1,39 @@ +## 1. Tool Result Data + +- [x] 1.1 Add generic `ToolResult::metadata` and propagate it through `ToolExecutor::format_tool_result`. +- [x] 1.2 Include result metadata in `tool_end` WebSocket payloads for live desktop/web rendering. +- [x] 1.3 Populate `ask_user_question_result.items` in both TUI and async AskUserQuestion success paths. +- [x] 1.4 Add a readable `AskUserQuestion` branch to `ToolExecutor::build_tool_call_preview`. + +## 2. Desktop/Web Rendering + +- [x] 2.1 Normalize live `tool_end.metadata.ask_user_question_result` into tool entries. +- [x] 2.2 Normalize persisted tool-message metadata into resumed tool entries. +- [x] 2.3 Implement the collapsible AskUserQuestion confirmation card in `ToolBlock`. +- [x] 2.4 Add two-line clamp styling and overflow-only native tooltip behavior for Q/A text. +- [x] 2.5 Preserve existing generic tool block behavior for non-AskUserQuestion tools. +- [x] 2.6 Keep AskUserQuestion confirmation cards visible outside processed activity summaries. + +## 3. Tests + +- [x] 3.1 Update AskUserQuestion C++ unit tests for structured metadata. +- [x] 3.2 Add C++ tool event payload coverage for result metadata. +- [x] 3.3 Add web transcript normalization tests for live and persisted AskUserQuestion cards. +- [x] 3.4 Add web component/logic coverage for clamped tooltip behavior where practical. +- [x] 3.5 Add web projection regression coverage for AskUserQuestion cards around `已处理` summaries. + +## 4. Verification + +- [x] 4.1 Run focused C++ tests for AskUserQuestion and tool event payloads. +- [x] 4.2 Run `pnpm test` from `web/`. +- [x] 4.3 Run `openspec validate style-ask-question-result-card --strict`. +- [x] 4.4 Re-run web tests after excluding AskUserQuestion cards from processed summaries. + +## 5. PR #49 Persistence Review + +- [x] 5.1 Persist explicit cancellation metadata through the real AskUserQuestion result path and cover the provider-visible output contract. +- [x] 5.2 Restore tool names only from the current turn's nearest preceding unconsumed call; cover interrupted and reused call IDs. +- [x] 5.3 Supply the missing feedback module, keep submit/cancel rendering in ToolBlock, and preserve cancellation cards without a canonical tool name. +- [x] 5.4 Replace the unimplemented renderer-extension assertion with real component rendering checks; cover reload, self-heal, replacement, continuation, and unrelated metadata. +- [x] 5.5 Run web tests/build, native focused tests, GCC compilation, strict OpenSpec validation, and scoped diff checks before merging and pushing master. +- [x] 5.6 Add frontend tests and production build to GitHub's PR/master checks so missing frontend imports cannot pass unnoticed. diff --git a/src/tool/ask_user_question_tool.cpp b/src/tool/ask_user_question_tool.cpp index dd7d66c41..13348b087 100644 --- a/src/tool/ask_user_question_tool.cpp +++ b/src/tool/ask_user_question_tool.cpp @@ -293,6 +293,9 @@ ToolResult make_rejected_ask_result() { ToolResult r; r.output = "[Error] User declined to answer questions."; r.success = false; + r.metadata = {{"ask_user_question_result", { + {"cancelled", true}, {"items", nlohmann::json::array()} + }}}; return r; } diff --git a/src/tool/tool_protocol_names.cpp b/src/tool/tool_protocol_names.cpp index 7c1b8d1f4..dcb462774 100644 --- a/src/tool/tool_protocol_names.cpp +++ b/src/tool/tool_protocol_names.cpp @@ -105,6 +105,11 @@ ScopedModelToolNameMappings::ScopedModelToolNameMappings( set_model_tool_name_mappings(std::move(mappings)); } +ScopedModelToolNameMappings::ScopedModelToolNameMappings( + std::initializer_list mappings) + : ScopedModelToolNameMappings( + ToolProtocolNameMappings(mappings.begin(), mappings.end())) {} + ScopedModelToolNameMappings::~ScopedModelToolNameMappings() { set_model_tool_name_mappings(previous_); } diff --git a/src/tool/tool_protocol_names.hpp b/src/tool/tool_protocol_names.hpp index e41e8a423..34feaa074 100644 --- a/src/tool/tool_protocol_names.hpp +++ b/src/tool/tool_protocol_names.hpp @@ -2,6 +2,7 @@ #include +#include #include #include #include @@ -38,6 +39,8 @@ bool set_model_tool_name_mappings(ToolProtocolNameMappings mappings, class ScopedModelToolNameMappings { public: explicit ScopedModelToolNameMappings(ToolProtocolNameMappings mappings); + ScopedModelToolNameMappings( + std::initializer_list mappings); ~ScopedModelToolNameMappings(); ScopedModelToolNameMappings(const ScopedModelToolNameMappings&) = delete; ScopedModelToolNameMappings& operator=(const ScopedModelToolNameMappings&) = delete; diff --git a/tests/hooks/hook_runtime_test.cpp b/tests/hooks/hook_runtime_test.cpp index eb2a483a9..ad264bcad 100644 --- a/tests/hooks/hook_runtime_test.cpp +++ b/tests/hooks/hook_runtime_test.cpp @@ -79,7 +79,7 @@ TEST(HookRuntime, MatcherAcceptsModelFacingAliasWhenRewriteActive) { } // 映射关闭时 "write" 不再是别名:canonical 值原样返回(matcher 本身仍可能 // 按正则子串命中 file_write,那是既有的正则语义,不在本用例范围内)。 - acecode::ScopedModelToolNameMappings none({}); + acecode::ScopedModelToolNameMappings none(acecode::ToolProtocolNameMappings{}); EXPECT_EQ(acecode::canonical_hook_match_value("write"), "write"); EXPECT_EQ(acecode::canonical_hook_match_value("Write"), "file_write"); } diff --git a/tests/tool/ask_user_question_tool_test.cpp b/tests/tool/ask_user_question_tool_test.cpp index 12c3fb925..a5ca6828e 100644 --- a/tests/tool/ask_user_question_tool_test.cpp +++ b/tests/tool/ask_user_question_tool_test.cpp @@ -502,6 +502,11 @@ TEST(AskUserQuestionUnattendedTest, AsyncToolStillPromptsWithoutActiveGoal) { auto r = tool.execute(args, ctx); EXPECT_TRUE(prompter_called); EXPECT_FALSE(r.success); + EXPECT_EQ(r.output, "[Error] User declined to answer questions."); + ASSERT_TRUE(r.metadata.contains("ask_user_question_result")); + const auto& feedback = r.metadata.at("ask_user_question_result"); + EXPECT_EQ(feedback.at("cancelled"), true); + EXPECT_EQ(feedback.at("items"), nlohmann::json::array()); } TEST(AskUserQuestionTimeoutTest, NoRecommendedOptionRemainsNotAnswered) { diff --git a/tests/tool/tool_result_attachments_test.cpp b/tests/tool/tool_result_attachments_test.cpp index 6f14dcf3b..f0a03b837 100644 --- a/tests/tool/tool_result_attachments_test.cpp +++ b/tests/tool/tool_result_attachments_test.cpp @@ -1,6 +1,7 @@ #include #include "tool/tool_executor.hpp" +#include "tool/ask_user_question_tool.hpp" TEST(ToolResultAttachments, FormatToolResultCarriesAttachmentContentParts) { acecode::ToolResult result; @@ -54,3 +55,17 @@ TEST(ToolResultAttachments, FormatToolResultCarriesUiMetadata) { EXPECT_EQ(items[0]["question"], "Q?"); EXPECT_EQ(items[0]["answer"], "A"); } + +TEST(ToolResultAttachments, CancelledAskResultPersistsFeedbackWithoutChangingOutput) { + const auto result = acecode::make_rejected_ask_result(); + const auto message = acecode::ToolExecutor::format_tool_result("call-cancel", result); + + EXPECT_FALSE(result.success); + EXPECT_EQ(message.role, "tool"); + EXPECT_EQ(message.tool_call_id, "call-cancel"); + EXPECT_EQ(message.content, "[Error] User declined to answer questions."); + ASSERT_TRUE(message.metadata.contains("ask_user_question_result")); + EXPECT_EQ(message.metadata.at("ask_user_question_result"), + (nlohmann::json{{"cancelled", true}, {"items", nlohmann::json::array()}})); + EXPECT_TRUE(acecode::format_ask_user_question_result_display(message.metadata).empty()); +} diff --git a/web/scripts/i18n-en-overrides.mjs b/web/scripts/i18n-en-overrides.mjs index 76eb2c5c1..ad05bea23 100644 --- a/web/scripts/i18n-en-overrides.mjs +++ b/web/scripts/i18n-en-overrides.mjs @@ -1,6 +1,7 @@ // Human-reviewed product terminology and phrases where generic machine // translation is ambiguous without ACECode context. export const ENGLISH_SOURCE_OVERRIDES = Object.freeze({ + '已取消全部回答': 'All answers cancelled', '仅显示 file_edit / file_write / apply_patch 工具的改动': 'Show only changes made by the file_edit / file_write / apply_patch tools', '国庆节': 'National Day', '下载国庆节主题': 'Download National Day theme', diff --git a/web/src/components/ToolBlock.jsx b/web/src/components/ToolBlock.jsx index 690320b0d..e881e0c67 100644 --- a/web/src/components/ToolBlock.jsx +++ b/web/src/components/ToolBlock.jsx @@ -20,6 +20,7 @@ import { highlightSourceForFile } from '../lib/sourceCodeHighlight.js'; import { fallbackToolSummary } from '../lib/toolSummaryFallback.js'; import { codeTextFromCopyButtonTarget, copyTextToClipboard } from '../lib/codeBlockCopy.js'; import { normalizeTaskCompleteMarkdown } from '../lib/taskCompleteSummary.js'; +import { questionFeedbackForTool } from '../lib/questionFeedback.js'; import { DESKTOP_CONTEXT_ACTION_EVENT, DESKTOP_CONTEXT_ACTIONS, @@ -193,7 +194,7 @@ function AskUserQuestionResultCard({ result, toolContextAttrs }) { const fullText = askUserQuestionText({ items }); return ( -
+