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
322 changes: 322 additions & 0 deletions docs/design/web-ask-user-question-design.md

Large diffs are not rendered by default.

399 changes: 399 additions & 0 deletions docs/prototypes/web-ask-user-question-alignment/index.html

Large diffs are not rendered by default.

243 changes: 243 additions & 0 deletions docs/specs/2026-09-13-web-ask-user-question-requirements.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-09-14
25 changes: 25 additions & 0 deletions openspec/changes/redesign-web-ask-user-question/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
## Context

The interaction and visual decisions are recorded in `docs/specs/2026-09-13-web-ask-user-question-requirements.md` and `docs/design/web-ask-user-question-design.md`. Their 2026-09-14 revisions define numeric-key advancement, custom-input Enter, and two-stage Escape. The delta spec includes those revisions.

The daemon accepts the first answer for an entire request. Transcript self-heal replaces item identifiers, and ChatView can remain mounted across sessions.

## Goals / Non-Goals

Preserve local drafts until final batch submission and bind durable feedback to its own result. Keep the current protocol, picker appearance, composer replacement, and existing daemon interjection support.

## Decisions

- Separate selection toggling from confirmation. Confirmation ensures that its option remains selected before navigation; it cannot undo the first click of a double-click.
- Resolve keyboard targets from explicit hover/focus, then recommendation, then the first option. IME composition is handled before question shortcuts.
- Derive feedback from completed tool metadata inside shared ToolBlock rendering. Remove the unbound ChatView transient fallback: selecting the latest tool by name can attach a previous result to a new request or another session. The brief wait for `tool_end` keeps feedback authoritative.
- Persist question type and unanswered status using additive metadata fields. Existing history without type metadata remains readable.

## Risks / Trade-offs

- Browser mouse and IME event ordering can differ from pure state tests. Validate the real component event handlers and run browser interaction checks.
- Feedback appears when the tool result arrives. Until then, the tool remains pending and does not claim that a different request completed.

## Migration Plan

No stored-history migration is required. Validate the repaired Web implementation and existing native metadata tests before integration.
32 changes: 32 additions & 0 deletions openspec/changes/redesign-web-ask-user-question/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Proposal: redesign-web-ask-user-question

## Why

Web 端 `AskUserQuestion`(`QuestionPicker`)与 TUI 版存在明显交互落差:缺少回车提交、复制选项、折叠与题目切换动效;选项行无选中态反馈(序号不随选中变成对勾);无法在非末题跳过未答题;自定义答案草稿与选中态未分离;底部输入框在提问期间只是禁用占位而非被替换。设计与规格已分别更新至 `docs/design/web-ask-user-question-design.md` 与 `docs/specs/2026-09-13-web-ask-user-question-requirements.md`。

本变更重做**仅 Web 端**的提问框,使其交互对齐 TUI 基线。

## What Changes

- 重写 `web/src/lib/questionPicker.js` 纯逻辑层:导航(前/后/切换)、已作答与可跳过判定、跳过(Not answered)语义、提交/取消 payload、单选/多选自定义草稿与选中态分离、末题防误提交、Enter 选中优先级。
- 重写 `web/src/components/QuestionPicker.jsx` 视觉与交互:低饱和中性配色、选中黑底白勾序号徽章、hover 浮现「复制 / 回车」操作按钮、内联自定义输入(幽灵文字、字符计数、聚焦/灰化草稿)、折叠态、底部 `取消 / 跳过 / 提交` 按钮与 Tab 切题、数字键选择、Enter/Ctrl+Enter 快捷键。
- `ChatView` 集成:提问框打开期间**替换底部输入框**(composer dock 隐藏,提问框占据其位置),提交/取消后恢复;展示提交逐题答案汇总与取消反馈。home 与会话两个入口同步。
- 反馈卡(`QuestionFeedbackCard`)**持久化**:卡片按 `AskUserQuestion` 工具消息的**落盘元数据就地派生**(`web/src/lib/questionFeedback.js`),渲染在该消息之后。取消路径在 `make_rejected_ask_result` 落 `cancelled` 标记,提交路径落每题 `multi_select`;daemon 在回填答案时派生 `not_answered`(与 TUI 同规则)。这样卡片在回合输出结束、继续对话、切换/重载会话后都还在 —— 回合结束的 transcript self-heal 会用新 item id 覆写最近一轮,依赖缓存锚点会让卡片消失。
- 不修改 `AskUserQuestion` 工具参数 schema(题目数、每题 2–4 选项、`multiSelect` 约束不变),也不改 `request_id / session_id / answers / cancelled` 的字段名与语义;只做**增量**元数据字段。不带入超时收卷、汇总页、`Ctrl+C` 劫持。

## Capabilities

### New Capabilities

- `web-ask-user-question`: Web 端自适应的单选/多选提问、折叠与题目切换、内联自定义答案、复制/回车、跳过(Not answered)、Enter/Ctrl+Enter 快捷键与输入框替换契约。

### Modified Capabilities

- `ask-question-policy`: Web 端从"必须全部作答才能下一步"扩展为支持非末题跳过(Not answered)与末题仅 `Ctrl+Enter` 提交;其余端行为不变。

## Impact

- **Web 前端**:`web/src/components/QuestionPicker.jsx`、`web/src/components/QuestionFeedbackCard.jsx`(反馈卡)、`web/src/components/ChatView.jsx`(集成点)、`web/src/components/ToolBlock.jsx`(在公共工具渲染路径展示新反馈卡)、`web/src/lib/questionPicker.js`、`web/src/lib/questionFeedback.js`(反馈卡派生)、`web/src/lib/sessionTranscript.js`(归一化 `cancelled` / `multi_select`)。
- **daemon**:`src/tool/ask_user_question_tool.cpp`(取消路径落 `cancelled`、提交与超时路径落 `multi_select`)、`src/agent_loop.cpp`(答案派生 `not_answered`)。
- **测试**:`web/src/lib/questionPicker.test.js`、`web/src/lib/questionFeedback.test.js`(新增)、`web/src/lib/sessionTranscript.test.js`、`tests/tool/ask_user_question_tool_test.cpp`;`web/src/lib/runTests.js` 注册新测试文件。
- **文档**:`docs/specs/2026-09-13-web-ask-user-question-requirements.md`、`docs/design/web-ask-user-question-design.md` 为验收基线,均已记录本轮决策变更。
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
## ADDED Requirements

### Requirement: Question navigation and composer replacement

The Web UI SHALL show one question at a time, with previous/next navigation and a collapsible header. While a request is pending, the picker SHALL replace the composer input area, including when collapsed. Resolving or cancelling SHALL restore the composer.

#### Scenario: Navigate and collapse a pending request
- **WHEN** a user navigates questions or collapses and expands the picker
- **THEN** answers and custom drafts remain intact and the composer stays hidden until resolution

### Requirement: Answer selection and custom drafts

The Web UI SHALL keep preset selection, custom text, and custom activation separate. Single-select presets and active custom answers SHALL be mutually exclusive. Multi-select SHALL allow presets and active custom text together. Inactive custom drafts SHALL remain editable but SHALL NOT enter the payload.

#### Scenario: Preserve an inactive custom draft
- **WHEN** a user types a custom single-select answer and then selects a preset
- **THEN** the draft remains visible but inactive and only the preset enters the payload

#### Scenario: Combine multi-select answers
- **WHEN** a user selects several presets and enters active custom text
- **THEN** the final answer includes all selected presets and the trimmed custom text

### Requirement: Collect answers once per request

Non-final actions SHALL only update local state and advance. Unanswered non-final questions SHALL offer Skip. The final question SHALL always offer Submit, even when all answers are empty. Submission SHALL send the complete batch with existing identifiers. The daemon SHALL derive `not_answered` when both `selected` and `custom_text` are empty, yielding `Not answered` to the model.

#### Scenario: Skip and submit a batch
- **WHEN** a user skips a non-final question and submits the final question
- **THEN** no answer is sent during navigation and one complete payload is sent at submission
- **AND** skipped questions are represented as unanswered

### Requirement: Confirming an option preserves its selection

Plain activation and Space SHALL toggle selection. Enter, numeric shortcuts, inline confirmation, and double-click SHALL select the target and advance on non-final questions, without deselecting an already selected option. On the final question these option confirmation actions SHALL select without submitting.

#### Scenario: Confirm an already selected multi-select option
- **WHEN** a user double-clicks an option or confirms an already selected option
- **THEN** the option and other multi-select answers remain selected, and a non-final question advances

### Requirement: Keyboard target and text input handling

Enter SHALL target the hovered or explicitly focused option, otherwise the first recommended option, otherwise the first preset. Up/Down SHALL move focus including the custom row. Tab/Shift+Tab and Right/Left SHALL navigate outside text inputs. Digits 1–9 SHALL confirm the corresponding preset or focus custom input when beyond the preset count. Ctrl+Enter SHALL submit the final question. Inside custom input, Enter SHALL invoke the primary action without inserting a newline; other navigation keys SHALL retain text editing behavior. IME composition SHALL NOT invoke question shortcuts.

#### Scenario: Use the recommended option by default
- **WHEN** Enter is pressed without an explicit target
- **THEN** the first recommended option is selected, or the first preset if no recommendation exists

#### Scenario: Confirm Chinese input
- **WHEN** IME composition emits Enter or the composing key code
- **THEN** the picker does not advance, submit, or cancel

#### Scenario: Submit while editing custom text
- **WHEN** non-composing Enter is pressed inside custom input
- **THEN** the picker advances on a non-final question or submits the final batch

### Requirement: Two-stage Escape and cancellation

Outside text input, the first Escape SHALL clear selections while preserving custom drafts and start a 1.2 second window. A second Escape within that window SHALL cancel. Escape inside custom input SHALL exit editing without arming cancellation. The Cancel button SHALL cancel directly with the existing `cancelled` payload.

#### Scenario: Clear answers before cancelling
- **WHEN** a user presses Escape twice within 1.2 seconds
- **THEN** the first clears selections and the second cancels the request

### Requirement: Selection and copy feedback

Selected badges SHALL display a check mark. Hover SHALL expose copy and confirm actions. Successful copy SHALL show a check for 1.5 seconds without changing answers.

#### Scenario: Copy a preset description
- **WHEN** a user clicks copy
- **THEN** the option text and description are copied without changing answers or question position

### Requirement: Durable feedback belongs to its tool result

Submission, cancellation, and interjection feedback SHALL derive only from the corresponding completed tool metadata and render once through shared ToolBlock. Feedback SHALL preserve multi-select and unanswered annotations after reload, self-heal, and subsequent turns. Pending questions and other sessions SHALL NOT inherit previous feedback.

#### Scenario: Ask consecutive questions
- **WHEN** one question finishes and another starts before any composer input
- **THEN** only the completed tool has a feedback card

#### Scenario: Restore results with fresh identifiers
- **WHEN** reload or self-heal recreates items, including renamed or nameless tool results
- **THEN** each result retains exactly its own card without cached item identifiers
56 changes: 56 additions & 0 deletions openspec/changes/redesign-web-ask-user-question/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Tasks: redesign-web-ask-user-question

## 1. 纯逻辑层 `web/src/lib/questionPicker.js`

- [x] 1.1 扩展 `.openspec.yaml`/`proposal`/`spec` 已完成:导航、跳过、Enter 优先级、草稿/输入框替换、快捷键通过可单测纯函数描述。
- [x] 1.2 重写纯逻辑:保留 `normalizeQuestionRequest`/`makeInitialAnswers`/payload 构建;新增/调整已作答判定、导航(`canSkip`/`canSubmitCurrent`/末题防误提交)、`selectOption`/`selectCustom`/`saveCustom`、`recommendedOptionIndex`、`enterTargetIndex`。
- [x] 1.3 保持 payload 与 daemon 协议兼容(`question_id / selected / custom_text`、`cancelled`),`custom_text` 仅写激活态非空草稿。
- [x] 1.4 扩充 `web/src/lib/questionPicker.test.js`:跳过语义、末题仅 Ctrl+Enter、单选/多选草稿与选中态分离、Enter 目标优先级、Tab 切换、已作答/not answered 判定。

## 2. 组件层 `web/src/components/QuestionPicker.jsx`

- [x] 2.1 视觉重做:低饱和中性配色、序号徽章黑底白勾、行 hover 浅底到位、内联自定义输入(幽灵文字/计数/聚焦与灰化草稿)、折叠态、底部 `取消/跳过/提交`。
- [x] 2.2 交互:选项行 hover 浮现「复制 / 回车」按钮;复制写入剪贴板并显示 1.5s 对勾反馈;回车按钮与双击=选中并进下一题/提交。
- [x] 2.3 键盘:数字键与 Enter 确认选项后推进(末题只选中),方向键/Tab 导航,Space 切换选中;自定义输入 Enter 走主操作,IME 确认不触发快捷键;Esc 首次清空选中,1.2s 内再次按才取消。
- [x] 2.4 提交/取消后通过 `onResolve` 释放,供上层恢复 composer 并展示汇总/取消反馈。
- [x] 2.5 收卷时机:非末题「提交/跳过」仅本地推进,绝不逐题发送;末题统一 `sendQuestionAnswer` 一次、取消一次 `cancelled`;末题 `canSubmit` 恒为真(整批一并记 Not answered)。

## 3. 集成 `web/src/components/ChatView.jsx`

- [x] 3.1 home 入口与 session 入口:提问框打开期间 composer 输入区替换为提问框,提交/取消后恢复。
- [x] 3.2 接入提交逐题答案汇总与「已取消全部回答」反馈展示(在合适位置渲染结果/取消提示)。
- [x] 3.3 daemon 序列化派生 `not_answered`(`src/agent_loop.cpp`):`selected` 与 `custom_text` 均为空时置真,与 TUI `ask_question_controller.cpp` 同规则,使 Web 端跳过在 LLM 结果中呈现为 `Not answered`。
- [x] 3.4 反馈卡持久化:卡片改为按 AskUserQuestion 工具消息就地派生(`web/src/lib/questionFeedback.js`),数据源是落盘元数据而非组件临时 state —— 回合结束的 transcript self-heal 会用新 id 覆写最近一轮,缓存锚点会让卡片消失。取消路径在 `make_rejected_ask_result` 落 `cancelled` 标记、提交路径落 `multi_select`,两者都能跨重载还原。
- [x] 3.5 历史工具身份规范化:恢复持久化 transcript 时,按 `tool_call_id` 将 `assistant.tool_calls` 中的明确工具名补到缺少名称的对应 `role:tool` 结果,使提交与取消卡在 self-heal、刷新和继续对话后仍紧跟各自的 AskUserQuestion 调用。

## 4. 验证

- [x] 4.1 `pnpm test`(自 `web/`)全绿。
- [x] 4.2 `pnpm build` 通过,嵌入前端资源刷新。
- [x] 4.3 反馈卡持久化回归测试:`web/src/lib/questionFeedback.test.js`(提交/取消/未作答/id 变更/连续提问与会话隔离)+ `tests/tool/ask_user_question_tool_test.cpp` 的 cancelled 与 multi_select 断言。
- [x] 4.4 仅当与既有行为相关时同步更新 `docs`;不引入超时收卷、汇总页、`Ctrl+C` 劫持。
- [x] 4.5 完整链路回归:使用真实 `assistant.tool_calls` + `role:tool` 持久化协议形状,覆盖实时提交/取消、历史重载、回合 self-heal、继续对话、调用后紧邻卡片且每次调用只出现一张。

## 5. 基于最新 master 重新落地

- [x] 5.1 冲突判定:master 的「提问挂起时直接输入 = 插话」与本次「方案 A:提问框替换 composer」互斥,按方案 A 落地 —— 提问期间 `ace-composer-dock` 不渲染 `InputBar`。
- [x] 5.2 清理不可达路径:`ChatView.submit` 的提问插话分支随输入框一起移除(不再调用 `api.interjectQuestion`)。daemon 插话端点、排队卡片的「插话」按钮与转录 `interjected` 标记保留 —— TUI/IM 通道仍在产生该形态的落盘结果。
- [x] 5.3 反馈卡判定与 master 对齐:`questionFeedbackForItem` 不再按工具名过滤(历史页可能只剩工具结果消息,或工具已被改名),并恢复 `interjected` 形态;`QuestionFeedbackCard` 补回「已改为直接输入,取消作答」卡。
- [x] 5.4 `web/src/lib/composerEditabilityArchitecture.test.js` 改为守方案 A 的契约(composer 让位、无插话入口),并把排队卡片插话单独成条。
- [x] 5.5 重新生成 i18n 源目录;Web 侧 `pnpm test`、`pnpm build`(含 lookbehind 兼容检查)全绿。安装须用 CI 同版本 pnpm 10.32.1(`npx pnpm@10.32.1 install --frozen-lockfile`)—— 本机 pnpm 12 解析该 patch 文件会失败,属工具链版本问题,与本次改动无关。
- [x] 5.6 C++ 侧 `*AskUserQuestion*`、`AskUserQuestionPrompter.*`、`AgentLoopQuestionInterjection.*` 共 54 个用例全绿。注意:旧 `build/windows-x64-dev` 里还留着 09-13 的过期目标文件(含 `agent_loop.hpp`、`ask_user_question_prompter.hpp` 等已变动的头文件),直接跑会出现与本改动无关的堆损坏崩溃;清掉过期 obj 重编后全部通过。

## 6. PR #50 审核修复

- [x] 6.1 确认选项与切换选项分开处理,修复多选双击和已选项快捷确认丢失答案;接通 hover / 键盘焦点 / 推荐项的 Enter 目标,阻止 IME 确认键触发问答快捷键。
- [x] 6.2 反馈卡只从所属工具的持久化结果派生,在公共 ToolBlock 中渲染,移除跨请求、跨会话复用的临时反馈;验证连续问答、self-heal 和重载。
- [x] 6.3 同步 2026-09-14 已修订的键盘契约,补齐合法 OpenSpec delta 与设计记录。
- [x] 6.4 运行真实组件回归、Web 全量测试与构建、i18n 目录生成、OpenSpec strict 和差异检查,记录验证结果。

### 审核验证记录

- `pnpm test`:2417 条通过,包含 12 项真实 QuestionPicker JSX 事件回归与共享 ToolBlock 持久化渲染回归。
- `pnpm build`:通过,4433 个正则字面量兼容检查通过。
- `pnpm i18n:catalog` 与 `openspec validate redesign-web-ask-user-question --strict`:通过。
- Chromium 真实组件检查:13 项通过,覆盖 IME、双击/键盘确认、复制、推荐与悬停、Esc 请求隔离、折叠、按钮、390px 宽度;使用模拟连接,未发送真实会话答案。
- C++ 改动与原 PR `c998f336` 相同,该提交的远端 `unit-tests (linux-x64)` 已通过;本轮修复不改 C++。
4 changes: 4 additions & 0 deletions src/agent_loop.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4024,6 +4024,10 @@ bool AgentLoop::execute_tool_calls(
item["question_id"] = a.question_id;
item["selected"] = a.selected;
item["custom_text"] = a.custom_text;
// 对齐 TUI(ask_question_controller.cpp):selected 与
// custom_text 均为空的题视为未作答,让 Web 端「跳过」
// 在 LLM 结果中呈现为 "Not answered" 而非空串。
item["not_answered"] = a.selected.empty() && a.custom_text.empty();
arr.push_back(std::move(item));
}
out["answers"] = std::move(arr);
Expand Down
Loading
Loading