Skip to content

[Bug] Question dock can remain blocked after skipping a question #352

@Astro-Han

Description

@Astro-Han

What happened?

A user hit a stuck question dock while answering a 4-question question tool prompt. The message timeline already showed 问题已忽略, but the bottom question dock still displayed question 4/4 with 忽略, 返回, and 提交 controls. The user reported that neither 忽略 nor 提交 worked anymore, and only 返回 still changed the visible question.

This is a stale pending-question state bug, not simply a missing-answer validation issue. The backend had already ended the tool call with The user dismissed this question, while the frontend still believed the same request was pending and kept blocking the composer.

Which area seems affected?

Model harness, prompts, tools, or session mechanics; UI question dock state.

How much does this affect you?

Blocks me from using PawWork.

Steps to reproduce

  1. Start a session where the assistant calls the question tool with multiple questions. 2. Navigate to the last question and choose an answer. 3. Submit while at least one earlier question has no answer, or otherwise trigger the backend Question.reply empty-answer validation path. 4. Observe that the message part can show the question as dismissed while the dock remains visible. 5. Click 忽略 or 提交 again. The dock remains stuck because the backend request has already been removed, while the frontend still has the old request in sync.data.question.

Expected interaction design

The user must not be forced to answer every question. Each question should have an explicit per-question state: answered, skipped, or still pending. 跳过当前题 should mean the user intentionally does not want to answer this question, but still wants to continue the group. This should not be treated as a tool error. 提交 should finish successfully only when every question has a clear state: answered or skipped. The model-facing result should preserve that distinction, for example "端头标签是否打印GTIN条形码?"="Skipped by user", rather than collapsing it into an ambiguous empty array. A separate group-level escape can still exist, but it should be clearly labeled as skipping or dismissing the whole group, not confused with skipping the current question.

Agreed product contract for v1: per-question skip is allowed; the bottom 忽略 action should skip the current question; final submit should require every question to be either answered or skipped; skipped questions should be returned to the model as explicit skipped semantics; only a true whole-group cancellation should reject the question tool call.

Root cause diagnosis

packages/opencode/src/question/index.ts currently treats an empty answer array inside Question.reply as a rejected question. In that branch it deletes the pending request and fails the deferred with RejectedError, but it does not publish question.rejected. The normal Question.reject path does publish question.rejected. The frontend removes the dock through packages/app/src/context/global-sync/event-reducer.ts, which only deletes pending questions on question.replied or question.rejected. Therefore the empty-answer failure path can remove the backend pending request without notifying the frontend. The frontend then keeps showing a stale dock. Further reply or reject calls hit an unknown request and currently return successfully without clearing the stale client state.

Relevant code paths: packages/opencode/src/question/index.ts reply() empty-answer branch deletes pending and fails without event; packages/opencode/src/question/index.ts reject() deletes pending and publishes question.rejected; packages/app/src/context/global-sync/event-reducer.ts removes dock entries only on question.replied and question.rejected; packages/app/src/pages/session/composer/session-question-dock.tsx sends reply and reject but does not locally remove the dock.

Proposed design

Represent question responses as a clear per-question outcome instead of overloading empty arrays. The smallest protocol-compatible option is to keep answers: string[][] for now and use a reserved sentinel string for skipped questions in the tool result, but this is fragile because labels are user/model-controlled strings. The cleaner design is to introduce an answer outcome shape such as { status: "answered", answers: string[] } | { status: "skipped" }, then have the tool result render skipped questions explicitly for the model. If backward compatibility must be preserved in the API, the backend can normalize legacy [] to skipped for now and expose a clearer shape later.

The UI should make the current-question action explicit. Rename or reinterpret the bottom 忽略 as 跳过此题 for question prompts. When clicked, mark only the current tab skipped and move to the next unanswered or unskipped tab. On the last tab, if all questions are answered or skipped, 提交 sends the group as a successful reply. If there are still untouched questions, 提交 jumps to the first untouched question instead of sending an invalid request.

A whole-group cancellation, if retained, should be separate from current-question skip. It can be secondary and clearly labeled, for example 跳过全部 or hidden behind a small menu if the UI should stay minimal. This action should call Question.reject, publish question.rejected, clear the dock, and render the message part as group dismissed.

Acceptance criteria

  • A user can skip exactly one question in a multi-question prompt and still answer the remaining questions. - Skipping one question does not mark the entire question tool call as error. - Final submit succeeds when every question is answered or skipped. - The model-facing output explicitly tells the model which questions were skipped. - The dock never remains visible after the backend has resolved or rejected the request. - Unknown or stale reply / reject attempts do not leave the user blocked. - The message timeline distinguishes skipped answers from group-level dismissal. - Focus and keyboard behavior remain usable for back, next, submit, and skip. - Regression coverage includes the exported-session shape where the message shows dismissed while the dock stays visible.

Non-goals

Do not redesign the whole question tool UI. Do not add long-form survey features, nested questions, validation rules, or required-question semantics unless a later issue explicitly asks for them. Do not force users to answer every question.

PawWork version

local build from exported session context.

OS version

macOS Darwin 25.3.0 from exported session context.

Can you reproduce it again?

Only once so far from the provided user report and session export.

Diagnostics

Evidence came from a screenshot showing 问题已忽略 in the message timeline while the bottom dock still showed question 4/4, plus the exported session /Users/yuhan/Downloads/pawwork-session-shiny-squid-2026-05-01-00-36-33.json. In that export, the final question tool part is status: error, error: The user dismissed this question, with 4 questions and no successful answers persisted for the GTIN question.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High prioritybugSomething isn't workingharnessModel harness, prompts, tool descriptions, and session mechanicsuiDesign system and user interface

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions