Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions docs/daemon-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
65 changes: 65 additions & 0 deletions openspec/changes/style-ask-question-result-card/design.md
Original file line number Diff line number Diff line change
@@ -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.
29 changes: 29 additions & 0 deletions openspec/changes/style-ask-question-result-card/proposal.md
Original file line number Diff line number Diff line change
@@ -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.
27 changes: 27 additions & 0 deletions openspec/changes/style-ask-question-result-card/review.md
Original file line number Diff line number Diff line change
@@ -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 继续验证。
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading