fix(mode): stabilize plan-mode model experience

This commit is contained in:
Tianyi Cui
2026-07-20 22:13:59 +08:00
parent 28a74d6d7e
commit 897dc82d9b
40 changed files with 620 additions and 298 deletions

View File

@@ -145,11 +145,14 @@ function elicitationForQuestion(
options: AskUserQuestionOption[],
): CreateElicitationRequest {
const title = question.header ?? 'Question'
const message = question.detail === undefined
? question.question
: `${question.question}\n\n${question.detail}`
if (options.length === 0) {
return {
sessionId,
mode: 'form',
message: question.question,
message,
requestedSchema: {
type: 'object',
title,
@@ -183,7 +186,7 @@ function elicitationForQuestion(
return {
sessionId,
mode: 'form',
message: question.question,
message,
requestedSchema: {
type: 'object',
title,
@@ -504,7 +507,7 @@ export function apply(ctx: Context, config: AcpConfig): void {
// --- Stream the harness event taxonomy to ACP session/update --------------
// --- Session modes (dsh-mode, opportunistic) ------------------------------
// The mode PICKER is dsh-mode's ACP surface (the plan-mode RFC): advertised
// The mode PICKER is dsh-mode's ACP surface (the plan-mode Agent Note): advertised
// as `modes` on session/new + session/load, switched via session/set_mode —
// optimistic `current_mode_update` (the pending mode IS the user's
// selection; the logged `mode/set` follows at the turn boundary) — and

View File

@@ -143,15 +143,18 @@ describe('acp bridge', () => {
questions: [{
id: 'language',
question: 'Which language?',
detail: 'Choose the implementation language for this project.',
options: [{ label: 'TypeScript' }],
}],
})
expect(result).toEqual({ answers: [{ id: 'language', selected: [], custom: 'Use Zig' }] })
expect(harness.elicitationRequests[0]).toMatchObject({
message: 'Which language?\n\nChoose the implementation language for this project.',
requestedSchema: {
properties: {
choice: {
title: 'Which language?',
description: 'Choose one option, or fill a custom answer below.',
oneOf: [{ const: 'TypeScript', title: 'TypeScript' }],
},

View File

@@ -227,7 +227,7 @@ export async function makeBridgeHarness(options: {
await ctx.plugin(ToolTodo)
}
if (options.withModes) {
await ctx.plugin(ModesService)
await ctx.plugin(ModesService, { modes: { plan: { section: 'Test plan mode instructions.' } } })
}
if (options.withFs) {
await ctx.plugin(LocalFileSystem, { cwd: options.fsCwd ?? options.storageDir })

View File

@@ -751,6 +751,10 @@ class QuestionDialog implements Component, Focusable {
lines.push(`${this.palette.accent('│')} ${clipped}${' '.repeat(Math.max(0, innerWidth - visibleWidth(clipped)))} ${this.palette.accent('│')}`)
}
for (const line of wrapTextWithAnsi(this.palette.bold(displayText(this.question.question)), innerWidth)) push(line)
if (this.question.detail !== undefined) {
push('')
for (const line of wrapTextWithAnsi(displayText(this.question.detail), innerWidth)) push(line)
}
push('')
if (this.mode === 'custom') {
for (const line of this.input.render(innerWidth)) push(line)

View File

@@ -731,12 +731,13 @@ describe('TUI user-interaction dialogs', () => {
const single = result.ctx.userInteraction.ask({
questions: [{
id: 'mode', header: 'Mode', question: 'Choose a mode',
id: 'mode', header: 'Mode', question: 'Choose a mode', detail: 'This choice controls the next turn.',
options: [{ label: 'Safe', description: 'Use checks' }, { label: 'Fast' }],
}],
})
await tick()
expect(result.terminal.output).toContain('Choose a mode')
expect(result.terminal.output).toContain('This choice controls the next turn.')
expect(result.terminal.output).toContain('1/2')
result.terminal.send('\x1b[B')
result.terminal.send('\r')

View File

@@ -11,7 +11,7 @@ Abstract user-interaction seam. It owns `ctx.userInteraction`, the service a mod
### Key Types
- `AskUserQuestionRequest``{ questions: [{ id, question, header?, options?, multiSelect? }], agent?, signal? }`.
- `AskUserQuestionRequest``{ questions: [{ id, question, detail?, header?, options?, multiSelect? }], agent?, signal? }`; `detail` supplies supporting text that providers render with the question without turning it into an option label.
- `AskUserQuestionOption``{ label, description? }`.
- `AskUserQuestionAnswer``{ answers: [{ id, selected, custom? }] }`.
- `UserInteractionProvider` — UI implementation with `ask(request)`.

View File

@@ -31,6 +31,8 @@ export interface AskUserQuestionItem {
id: string
/** The question to display. */
question: string
/** Optional supporting detail rendered with the question but kept out of option labels. */
detail?: string
/** Optional short heading/group label. */
header?: string
/** Optional choices the UI can render as a menu. */