Merge remote-tracking branch 'origin/master' into perf/tui-resume-scan
# Conflicts: # packages/ui/tui/README.i18n.yaml
This commit is contained in:
@@ -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 packages/ui/tui/README.md
|
||||
README.md: ac45a0ec9c282f3c872b325fe30a083dd1deed33
|
||||
README.zh.md: 6bcb431713dd8a247d2b39d3392edc369ad00c90
|
||||
README.md: 78d56a6cacd040fdd32b73f779bab3fb4c77fcce
|
||||
README.zh.md: c403605bb13d252eec00a2b0ebafb5f953c884f8
|
||||
|
||||
@@ -156,7 +156,7 @@ Append-only; newly visible content follows the reusable request prefix and does
|
||||
|
||||
#### What the model sees
|
||||
|
||||
When a consumer calls `ctx.userInteraction.ask()`, this provider presents each question in order and returns selected option labels or `custom` text. Abort, cancellation, or UI disposal becomes `Error: ask_user_question was interrupted before the user answered` through `dsh-tool-ask-user`.
|
||||
When a consumer calls `ctx.userInteraction.ask()`, this provider presents each question in order and returns selected option labels, `custom` text, or both for a multi-select question. Pending custom text survives switching back to options and joins checked labels on a later options-mode submit. Abort, cancellation, or UI disposal becomes `Error: ask_user_question was interrupted before the user answered` through `dsh-tool-ask-user`.
|
||||
|
||||
#### Token effect
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ Paths prefixed with @ are files explicitly referenced by the user. Use the read
|
||||
|
||||
#### 模型看到的内容
|
||||
|
||||
消费方调用 `ctx.userInteraction.ask()` 时,此提供方会按顺序显示各个问题,并返回选中选项标签或 `custom` 文本。中止、取消或 UI dispose 会变为 `Error: ask_user_question was interrupted before the user answered`;该转换由 `dsh-tool-ask-user` 完成。
|
||||
消费方调用 `ctx.userInteraction.ask()` 时,此提供方会按顺序显示各个问题,并返回选中选项标签、`custom` 文本,或为多选题同时返回两者。切回选项后,待提交的自定义文本仍会保留,并在之后从选项模式提交时与已勾选的标签一同返回。中止、取消或 UI dispose 会变为 `Error: ask_user_question was interrupted before the user answered`;该转换由 `dsh-tool-ask-user` 完成。
|
||||
|
||||
#### Token 影响
|
||||
|
||||
|
||||
@@ -868,12 +868,15 @@ export class QuestionDialog implements Component, Focusable {
|
||||
if (this.selected.has(this.selectedIndex)) this.selected.delete(this.selectedIndex)
|
||||
else this.selected.add(this.selectedIndex)
|
||||
} else if (matchesKey(data, Key.enter)) {
|
||||
const indices = this.question.multiSelect ? [...this.selected].sort((a, b) => a - b) : [this.selectedIndex]
|
||||
if (indices.length === 0) {
|
||||
const selected = this.question.multiSelect
|
||||
? this.selectedOptionLabels()
|
||||
: [options[this.selectedIndex]?.label].filter((label): label is string => label !== undefined)
|
||||
const custom = this.question.multiSelect ? this.input.getValue().trim() : ''
|
||||
if (selected.length === 0 && custom === '') {
|
||||
this.error = 'Select at least one option, or press Tab for a custom answer.'
|
||||
return
|
||||
}
|
||||
this.done({ selected: indices.map(index => options[index]?.label).filter((label): label is string => label !== undefined) })
|
||||
this.done({ selected, ...(custom === '' ? {} : { custom }) })
|
||||
} else if (matchesKey(data, Key.tab) || data.toLowerCase() === 'c') {
|
||||
this.mode = 'custom'
|
||||
this.selectedBlockPage = { offset: 0, size: 1, maxOffset: 0 }
|
||||
@@ -889,7 +892,17 @@ export class QuestionDialog implements Component, Focusable {
|
||||
this.error = 'Enter an answer before submitting.'
|
||||
return
|
||||
}
|
||||
this.done({ selected: [], custom })
|
||||
this.done({
|
||||
selected: this.question.multiSelect ? this.selectedOptionLabels() : [],
|
||||
custom,
|
||||
})
|
||||
}
|
||||
|
||||
private selectedOptionLabels(): string[] {
|
||||
return [...this.selected]
|
||||
.sort((a, b) => a - b)
|
||||
.map(index => this.options[index]?.label)
|
||||
.filter((label): label is string => label !== undefined)
|
||||
}
|
||||
|
||||
/** Page backward through an oversized option, then through question detail. */
|
||||
@@ -955,9 +968,12 @@ export class QuestionDialog implements Component, Focusable {
|
||||
}
|
||||
headerLines.push('')
|
||||
|
||||
const customHint = this.palette.dim(this.options.length > 0
|
||||
? 'Enter submit • Esc options'
|
||||
: 'Enter submit • Esc cancel')
|
||||
const customControls = [
|
||||
...(this.options.length > 0 && this.question.multiSelect ? [`${this.selected.size} selected`] : []),
|
||||
'Enter submit',
|
||||
this.options.length > 0 ? 'Esc options' : 'Esc cancel',
|
||||
]
|
||||
const customHint = this.palette.dim(customControls.join(' • '))
|
||||
const footerLines: string[] = []
|
||||
if (this.mode === 'custom') {
|
||||
for (const line of this.input.render(innerWidth)) footerLines.push(line)
|
||||
|
||||
@@ -5890,8 +5890,29 @@ describe('TUI user-interaction dialogs', () => {
|
||||
result.terminal.send(' ')
|
||||
result.terminal.send('\x1b[B')
|
||||
result.terminal.send(' ')
|
||||
result.terminal.send('\t')
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('2 selected • Enter submit • Esc options')
|
||||
result.terminal.send('Tests')
|
||||
result.terminal.send('\r')
|
||||
await expect(multi).resolves.toEqual({ answers: [{ id: 'targets', selected: ['Code', 'Docs'] }] })
|
||||
await expect(multi).resolves.toEqual({
|
||||
answers: [{ id: 'targets', selected: ['Code', 'Docs'], custom: 'Tests' }],
|
||||
})
|
||||
|
||||
const labelsOnly = result.ctx.userInteraction.ask({
|
||||
questions: [{
|
||||
id: 'labels-only',
|
||||
question: 'Pick one target',
|
||||
multiSelect: true,
|
||||
options: [{ label: 'Code' }, { label: 'Docs' }],
|
||||
}],
|
||||
})
|
||||
await tick()
|
||||
result.terminal.send(' ')
|
||||
result.terminal.send('\r')
|
||||
await expect(labelsOnly).resolves.toEqual({
|
||||
answers: [{ id: 'labels-only', selected: ['Code'] }],
|
||||
})
|
||||
|
||||
const custom = result.ctx.userInteraction.ask({
|
||||
questions: [{ id: 'other', question: 'Choose or type', options: [{ label: 'Default' }] }],
|
||||
@@ -5934,7 +5955,6 @@ describe('TUI user-interaction dialogs', () => {
|
||||
options: [{ label: 'One', description: 'first' }, { label: 'Two' }],
|
||||
}],
|
||||
})
|
||||
const rejected = expect(answer).rejects.toMatchObject({ code: 'ASK_ABORTED' })
|
||||
await tick()
|
||||
result.terminal.send('\x1b[A')
|
||||
result.terminal.send('\x1b[B')
|
||||
@@ -5950,11 +5970,17 @@ describe('TUI user-interaction dialogs', () => {
|
||||
})
|
||||
result.terminal.send('c')
|
||||
await tick()
|
||||
result.terminal.send('keep this')
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('0 selected • Enter submit • Esc options')
|
||||
result.terminal.send('\x1b')
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('Space toggle')
|
||||
result.terminal.send('\x03')
|
||||
await rejected
|
||||
result.terminal.send(' ')
|
||||
result.terminal.send('\r')
|
||||
await expect(answer).resolves.toEqual({
|
||||
answers: [{ id: 'options', selected: ['One'], custom: 'keep this' }],
|
||||
})
|
||||
await dispose(result)
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user