diff --git a/.agents/notes/implemented/feature/2026-07-24-web-plan-mode.i18n.yaml b/.agents/notes/implemented/feature/2026-07-24-web-plan-mode.i18n.yaml index 34091877bf..3c9b4bfeac 100644 --- a/.agents/notes/implemented/feature/2026-07-24-web-plan-mode.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-24-web-plan-mode.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -2026-07-24-web-plan-mode.md: 3b8a14a279dee478a291b19d5c0aed82f5a16c5c -2026-07-24-web-plan-mode.zh.md: ca52918257c289fab966f1915348b08de45d86f1 +2026-07-24-web-plan-mode.md: 3f27dee80b4e059f8ed079f5d9d25b3472e7fcc7 +2026-07-24-web-plan-mode.zh.md: fd0ca0d51814ee499bc28c41736d7bb87a5e406e diff --git a/.agents/notes/implemented/feature/2026-07-24-web-plan-mode.md b/.agents/notes/implemented/feature/2026-07-24-web-plan-mode.md index 3b8a14a279..3f27dee80b 100644 --- a/.agents/notes/implemented/feature/2026-07-24-web-plan-mode.md +++ b/.agents/notes/implemented/feature/2026-07-24-web-plan-mode.md @@ -34,7 +34,7 @@ Business and transport failures leave the confirmed snapshot unchanged, re-enabl ## Exit review -`exit_plan_mode` remains registered in both modes for request-cache stability. In plan mode the model submits the complete Markdown plan through that tool. The plan service asks through `ctx.userInteraction`, and the already-composed Web question plugin presents the plan detail with Approve, Keep planning, and the free-text answer channel. +`exit_plan_mode` remains registered in both modes for request-cache stability. In plan mode the model submits the complete Markdown plan through that tool. The plan service asks through `ctx.userInteraction`, and the already-composed Web question plugin presents the plan detail with Approve, Keep planning, and the free-text answer channel. The capped question card keeps its title, navigation, and submission actions fixed while the complete plan and choices share an internal scroll region. Approval queues inactive mode for the next step; it does not rewrite the current tool batch. Keep planning or custom feedback leaves plan mode active and returns corrective feedback to the model. If the review channel is unavailable or aborted, the tool fails closed and the policy tells the model to ask the user to switch modes manually. diff --git a/.agents/notes/implemented/feature/2026-07-24-web-plan-mode.zh.md b/.agents/notes/implemented/feature/2026-07-24-web-plan-mode.zh.md index ca52918257..fd0ca0d518 100644 --- a/.agents/notes/implemented/feature/2026-07-24-web-plan-mode.zh.md +++ b/.agents/notes/implemented/feature/2026-07-24-web-plan-mode.zh.md @@ -34,7 +34,7 @@ Web 宿主可以通过[可选会话 RPC 契约](../architecture/2026-07-24-web-p ## 退出评审 -`exit_plan_mode` 在两种模式下都会保持注册,以维持请求缓存稳定性。在 plan mode 中,模型通过该工具提交完整的 Markdown plan。plan 服务经由 `ctx.userInteraction` 发起询问,已组合进 Web 的问题插件会展示 plan 详情,并提供「批准」、「继续规划」和自由文本回答渠道。 +`exit_plan_mode` 在两种模式下都会保持注册,以维持请求缓存稳定性。在 plan mode 中,模型通过该工具提交完整的 Markdown plan。plan 服务经由 `ctx.userInteraction` 发起询问,已组合进 Web 的问题插件会展示 plan 详情,并提供「批准」、「继续规划」和自由文本回答渠道。设有高度上限的问题卡片会固定显示标题、导航操作和提交操作,而完整 plan 与选项共享同一个内部滚动区域。 批准会将未激活模式排到下一步骤生效,不会重写当前工具批次。选择继续规划或提供自定义反馈时,plan mode 保持激活,并向模型返回修正反馈。若评审渠道不可用或已中止,工具会采取失败关闭策略,策略则要求模型请用户手动切换模式。 diff --git a/apps/web/tests/smoke-fixture.e2e.ts b/apps/web/tests/smoke-fixture.e2e.ts index 0c6776cf5e..df3c0e0a24 100644 --- a/apps/web/tests/smoke-fixture.e2e.ts +++ b/apps/web/tests/smoke-fixture.e2e.ts @@ -312,6 +312,39 @@ describe('web boot chain success pass (keyless, ten real bundles, ?fixture)', () await composer.getByRole('radio', { name: '工程落地型' }).click() await composer.getByText('2 / 3', { exact: true }).waitFor() await composer.getByRole('button', { name: '跳过本题', exact: true }).click() + const detail = composer.getByText('按当前招聘目标选择;跳过则视为不设偏好。') + // Exercise provider-sized plan detail through the assembled composer + // without making the shared fixture transcript permanently enormous. + await detail.evaluate((element) => { + element.textContent = Array.from( + { length: 80 }, + (_, index) => `Plan section ${String(index + 1)} keeps review context readable.`, + ).join(' ') + }) + const scrollRegion = composer.locator('[data-question-scroll]') + const layout = await composer.evaluate((root) => { + const card = root.querySelector('section') + const scroller = root.querySelector('[data-question-scroll]') + const footer = root.querySelector('footer') + if (!(card instanceof HTMLElement) || + !(scroller instanceof HTMLElement) || + !(footer instanceof HTMLElement)) return null + const cardRect = card.getBoundingClientRect() + const footerRect = footer.getBoundingClientRect() + return { + cardClientHeight: card.clientHeight, + cardScrollHeight: card.scrollHeight, + footerInsideCard: footerRect.bottom <= cardRect.bottom, + scrollerClientHeight: scroller.clientHeight, + scrollerScrollHeight: scroller.scrollHeight, + } + }) + expect(layout).not.toBeNull() + expect(layout?.cardScrollHeight).toBe(layout?.cardClientHeight) + expect(layout?.footerInsideCard).toBe(true) + expect(layout?.scrollerScrollHeight).toBeGreaterThan(layout?.scrollerClientHeight ?? 0) + await scrollRegion.evaluate((element) => { element.scrollTop = element.scrollHeight }) + await expect.poll(() => scrollRegion.evaluate(element => element.scrollTop)).toBeGreaterThan(0) await composer.getByRole('checkbox', { name: '系统设计' }).click() await composer.getByRole('checkbox', { name: 'Agent 产品判断' }).click() await composer.getByRole('checkbox', { name: 'Agent 产品判断' }).press('Enter') diff --git a/packages/client/ui-question/README.md b/packages/client/ui-question/README.md index a02c85d54c..467edfdfff 100644 --- a/packages/client/ui-question/README.md +++ b/packages/client/ui-question/README.md @@ -2,7 +2,7 @@ Web `ask_user_question` feature plugin. Its host half mounts `dsh-tool-ask-user` only when the Web feature is selected; its browser half registers the `question` entry in the conversation-owned `conversation.composer` keyed slot. -The component renders one question at a time with progress navigation, single- and multi-select choices, recommendation badges derived from label suffixes, and custom answers. Single-select choices advance immediately, and Enter submits once every question is answered or skipped; Enter during IME composition confirms the input candidate without advancing. It submits one structured answer batch for the whole request: “Skip this question” retains other drafts and emits the existing blank `{ selected: [] }` shape for that item, while close rejects the whole wait as `ASK_CANCELLED`. +The component renders one question at a time with progress navigation, single- and multi-select choices, recommendation badges derived from label suffixes, and custom answers. The capped card keeps its title, navigation, and submission actions fixed while long detail and choices share an internal scroll region. Single-select choices advance immediately, and Enter submits once every question is answered or skipped; Enter during IME composition confirms the input candidate without advancing. It submits one structured answer batch for the whole request: “Skip this question” retains other drafts and emits the existing blank `{ selected: [] }` shape for that item, while close rejects the whole wait as `ASK_CANCELLED`. Selection state is local to a component keyed by the request rpcId. A replay with the same id preserves a still-mounted draft, while `question/resolved` from the host removes the composer. The host remains authoritative: successful HTTP delivery does not remove pending state locally. diff --git a/packages/client/ui-question/src/client/QuestionComposer.module.css b/packages/client/ui-question/src/client/QuestionComposer.module.css index 6c0c1b854c..96005bf399 100644 --- a/packages/client/ui-question/src/client/QuestionComposer.module.css +++ b/packages/client/ui-question/src/client/QuestionComposer.module.css @@ -67,7 +67,7 @@ } .detail { - margin: 2px 0 0; + margin: 0 2px 8px; color: var(--dsw-alias-label-tertiary); font-size: 13px; line-height: 20px; @@ -113,13 +113,19 @@ cursor: default; } +.body { + display: flex; + flex: 1 1 auto; + flex-direction: column; + min-height: 0; + overflow-y: auto; + overscroll-behavior: contain; +} + .options { display: flex; flex-direction: column; gap: 4px; - /* The scrollable region of the capped card (ChatView list pattern). */ - min-height: 0; - overflow-y: auto; } .option { diff --git a/packages/client/ui-question/src/client/QuestionComposer.tsx b/packages/client/ui-question/src/client/QuestionComposer.tsx index 3571263f61..a0990fd375 100644 --- a/packages/client/ui-question/src/client/QuestionComposer.tsx +++ b/packages/client/ui-question/src/client/QuestionComposer.tsx @@ -171,7 +171,6 @@ function QuestionFlow({ pending }: { pending: PendingQuestion }) { : question.question} {question.multiSelect === true && 可多选} - {question.detail !== undefined &&

{question.detail}

}
{index + 1} / {questions.length} @@ -199,79 +198,82 @@ function QuestionFlow({ pending }: { pending: PendingQuestion }) {
-
- {(question.options ?? []).map((option, optionIndex) => { - const selected = draft.selected.includes(option.label) - const display = parseRecommendedLabel(option.label) - return ( - - ) - })} - -
- {hasOptions && ( - - )} - {draft.customOpen && ( -