@aiwayds/dsh-tui-pi 2.18.2 → 2.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -43
- package/README.zh-CN.md +11 -42
- package/lib/agents.js +67 -52
- package/lib/agents.js.map +1 -1
- package/lib/ask-user.d.ts +122 -3
- package/lib/ask-user.js +335 -41
- package/lib/ask-user.js.map +1 -1
- package/lib/attachments.js +8 -3
- package/lib/attachments.js.map +1 -1
- package/lib/btw.d.ts +4 -2
- package/lib/btw.js +22 -16
- package/lib/btw.js.map +1 -1
- package/lib/custom-provider.js +34 -28
- package/lib/custom-provider.js.map +1 -1
- package/lib/footer.d.ts +9 -8
- package/lib/footer.js +36 -18
- package/lib/footer.js.map +1 -1
- package/lib/hard-exit.d.ts +36 -0
- package/lib/hard-exit.js +64 -0
- package/lib/hard-exit.js.map +1 -0
- package/lib/history-fork.d.ts +2 -2
- package/lib/history-fork.js +12 -9
- package/lib/history-fork.js.map +1 -1
- package/lib/history-turns.d.ts +4 -2
- package/lib/history-turns.js +7 -3
- package/lib/history-turns.js.map +1 -1
- package/lib/history.js +57 -45
- package/lib/history.js.map +1 -1
- package/lib/hotkeys.js +44 -33
- package/lib/hotkeys.js.map +1 -1
- package/lib/i18n/index.d.ts +54 -0
- package/lib/i18n/index.js +98 -0
- package/lib/i18n/index.js.map +1 -0
- package/lib/i18n/registry.d.ts +65 -0
- package/lib/i18n/registry.js +129 -0
- package/lib/i18n/registry.js.map +1 -0
- package/lib/index.js +219 -109
- package/lib/index.js.map +1 -1
- package/lib/language-ask.d.ts +98 -0
- package/lib/language-ask.js +118 -0
- package/lib/language-ask.js.map +1 -0
- package/lib/live-widgets.js +9 -8
- package/lib/live-widgets.js.map +1 -1
- package/lib/login.js +5 -4
- package/lib/login.js.map +1 -1
- package/lib/messages.d.ts +0 -4
- package/lib/messages.js +14 -15
- package/lib/messages.js.map +1 -1
- package/lib/model-profiles.js +32 -20
- package/lib/model-profiles.js.map +1 -1
- package/lib/panels.js +18 -17
- package/lib/panels.js.map +1 -1
- package/lib/preflight-projcache.js +9 -3
- package/lib/preflight-projcache.js.map +1 -1
- package/lib/preset-dialog.d.ts +3 -3
- package/lib/preset-dialog.js +20 -17
- package/lib/preset-dialog.js.map +1 -1
- package/lib/provider-catalog.d.ts +23 -7
- package/lib/provider-catalog.js +90 -68
- package/lib/provider-catalog.js.map +1 -1
- package/lib/queue-panel.d.ts +2 -2
- package/lib/queue-panel.js +13 -9
- package/lib/queue-panel.js.map +1 -1
- package/lib/repair-dialog.d.ts +4 -4
- package/lib/repair-dialog.js +23 -16
- package/lib/repair-dialog.js.map +1 -1
- package/lib/route-dialog.d.ts +3 -3
- package/lib/route-dialog.js +20 -14
- package/lib/route-dialog.js.map +1 -1
- package/lib/selectors.d.ts +15 -1
- package/lib/selectors.js +57 -23
- package/lib/selectors.js.map +1 -1
- package/lib/sessions.d.ts +47 -14
- package/lib/sessions.js +162 -53
- package/lib/sessions.js.map +1 -1
- package/lib/settings.d.ts +68 -9
- package/lib/settings.js +327 -78
- package/lib/settings.js.map +1 -1
- package/lib/skills-manager.js +27 -21
- package/lib/skills-manager.js.map +1 -1
- package/lib/skills.js +5 -4
- package/lib/skills.js.map +1 -1
- package/lib/startup-info.js +9 -3
- package/lib/startup-info.js.map +1 -1
- package/lib/steer-flow.d.ts +14 -4
- package/lib/steer-flow.js +24 -9
- package/lib/steer-flow.js.map +1 -1
- package/lib/stop-dialog.d.ts +2 -2
- package/lib/stop-dialog.js +16 -12
- package/lib/stop-dialog.js.map +1 -1
- package/lib/subagent-viewer.d.ts +3 -8
- package/lib/subagent-viewer.js +38 -62
- package/lib/subagent-viewer.js.map +1 -1
- package/lib/theme-preview.js +12 -11
- package/lib/theme-preview.js.map +1 -1
- package/lib/theme-settings.d.ts +44 -2
- package/lib/theme-settings.js +97 -54
- package/lib/theme-settings.js.map +1 -1
- package/lib/tui.js +2 -1
- package/lib/tui.js.map +1 -1
- package/locales/en.json +646 -0
- package/locales/ja.json +646 -0
- package/locales/ko.json +646 -0
- package/locales/zh-CN.json +646 -0
- package/package.json +2 -1
- package/skills/dsh-tui-pi-config/SKILL.md +14 -4
package/lib/ask-user.js
CHANGED
|
@@ -84,6 +84,7 @@ import { BOLD, RESET, ansiFg, } from "./theme/index.js";
|
|
|
84
84
|
import { MARKER_W, panelThemeFns, TABLE_SEP, columnWidths, padCell, rowMarker, tableHeaderLine, tableRuleLine, } from "./panels.js";
|
|
85
85
|
import { clipToWidth, wrapText } from "./text.js";
|
|
86
86
|
import { emitNotice } from "./notice-bridge.js";
|
|
87
|
+
import { t } from "./i18n/index.js";
|
|
87
88
|
// ----------------------------------------------------------------- constants --
|
|
88
89
|
/** Maximum time between two Esc presses before the second one re-arms instead of firing. */
|
|
89
90
|
export const DOUBLE_ESC_WINDOW_MS = 200;
|
|
@@ -140,6 +141,36 @@ export const ASK_COLLAPSE_KEY = 'ctrl+t';
|
|
|
140
141
|
export const DECLINE_MESSAGE = 'User declined to answer questions.';
|
|
141
142
|
/** Transient hint when Enter lands on an incomplete confirm/submit row. */
|
|
142
143
|
export const INCOMPLETE_HINT = 'Answer every question first';
|
|
144
|
+
// ------------------------------------------------------------- timeouts --
|
|
145
|
+
/** Default no-input window per focused question, in minutes: after this long
|
|
146
|
+
* without a single keypress the question is auto-answered (recommended
|
|
147
|
+
* option, plan-safe) and the panel moves on. */
|
|
148
|
+
export const ASK_USER_IDLE_MINUTES_DEFAULT = 5;
|
|
149
|
+
/** Default hard cap per focused question, in minutes: after this long the
|
|
150
|
+
* question is auto-answered EVEN IF the user keeps interacting — the panel
|
|
151
|
+
* must never hold a run hostage indefinitely (dual rule with the idle
|
|
152
|
+
* window; either firing resolves the question). */
|
|
153
|
+
export const ASK_USER_ABSOLUTE_MINUTES_DEFAULT = 10;
|
|
154
|
+
/** Env override for the no-input window (minutes; <= 0 disables the rule). */
|
|
155
|
+
export const ASK_USER_IDLE_ENV = 'DSH_TUI_ASK_USER_IDLE_MINUTES';
|
|
156
|
+
/** Env override for the hard cap (minutes; <= 0 disables the rule). */
|
|
157
|
+
export const ASK_USER_ABSOLUTE_ENV = 'DSH_TUI_ASK_USER_ABSOLUTE_MINUTES';
|
|
158
|
+
const MINUTE_MS = 60_000;
|
|
159
|
+
/** Note folded into the answer envelope's `custom` field when a question was
|
|
160
|
+
* auto-answered with the recommended option — the model reads the answer as
|
|
161
|
+
* a tool result, so an automatic pick is declared in-band (same spirit as
|
|
162
|
+
* DECLINE_MESSAGE), never silently passed off as a human choice. */
|
|
163
|
+
export const TIMEOUT_RECOMMENDED_NOTE = 'Auto-answered after the no-input timeout: the recommended option was picked.';
|
|
164
|
+
/** Note for a plan-review question auto-answered by timeout: plans are NEVER
|
|
165
|
+
* auto-approved — the first non-approve option is picked instead. */
|
|
166
|
+
export const TIMEOUT_PLAN_DECLINED_NOTE = 'Auto-answered after the no-input timeout: no user input, so the plan was NOT approved.';
|
|
167
|
+
/** Note for a question with no options at all — there is no recommended value
|
|
168
|
+
* to fall back to, so the answer carries the explanation only. */
|
|
169
|
+
export const TIMEOUT_NO_DEFAULT_NOTE = 'Auto-answered after the no-input timeout: the question offers no default option to pick.';
|
|
170
|
+
/** Note appended when the timeout commits a half-typed sentinel buffer: the
|
|
171
|
+
* text is the user's own, but it never went through Enter, so the envelope
|
|
172
|
+
* says so. */
|
|
173
|
+
export const TIMEOUT_CUSTOM_NOTE = 'Auto-committed after the no-input timeout: the text typed so far.';
|
|
143
174
|
/** Mark left of a question a custom input wrote text into. */
|
|
144
175
|
const CUSTOM_MARK = '✎ ';
|
|
145
176
|
/**
|
|
@@ -325,7 +356,7 @@ export function buildRowList(questions, perQuestion, focusQuestion = 0) {
|
|
|
325
356
|
rows.push({
|
|
326
357
|
kind: 'question-header',
|
|
327
358
|
questionIndex: focusQuestion,
|
|
328
|
-
label: question.header ??
|
|
359
|
+
label: question.header ?? t('ask.questionFallback', { n: focusQuestion + 1 }),
|
|
329
360
|
description: question.question,
|
|
330
361
|
...(question.detail !== undefined ? { detail: question.detail } : {}),
|
|
331
362
|
selectable: false,
|
|
@@ -345,12 +376,12 @@ export function buildRowList(questions, perQuestion, focusQuestion = 0) {
|
|
|
345
376
|
rows.push({
|
|
346
377
|
kind: 'sentinel',
|
|
347
378
|
questionIndex: focusQuestion,
|
|
348
|
-
label: customAnswer !== '' ? `${CUSTOM_MARK}${customAnswer.trim()}` :
|
|
379
|
+
label: customAnswer !== '' ? `${CUSTOM_MARK}${customAnswer.trim()}` : t('ask.sentinel'),
|
|
349
380
|
selectable: true,
|
|
350
381
|
});
|
|
351
382
|
}
|
|
352
383
|
if (needsConfirmRow(questions)) {
|
|
353
|
-
rows.push({ kind: 'confirm', questionIndex: -1, label: '
|
|
384
|
+
rows.push({ kind: 'confirm', questionIndex: -1, label: t('ask.confirmRow'), selectable: true });
|
|
354
385
|
}
|
|
355
386
|
return rows;
|
|
356
387
|
}
|
|
@@ -456,6 +487,141 @@ export function canAutoSubmit(state) {
|
|
|
456
487
|
return false;
|
|
457
488
|
return allQuestionsAnswered(state);
|
|
458
489
|
}
|
|
490
|
+
/** Both rules off — the panel waits for the human exactly as before. */
|
|
491
|
+
export const ASK_USER_TIMEOUTS_DISABLED = { idleMs: 0, absoluteMs: 0 };
|
|
492
|
+
/** Minutes → ms; negatives clamp to 0 (a non-positive knob is the
|
|
493
|
+
* documented per-rule off switch, mirroring retention's maxCount). */
|
|
494
|
+
function minutesToMs(minutes) {
|
|
495
|
+
return Math.max(0, minutes) * MINUTE_MS;
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Narrow one explicit settings field: a finite number, or undefined
|
|
499
|
+
* (absent, or present-but-invalid). An invalid value emits one notice
|
|
500
|
+
* through the shared bridge naming the field and its raw value — same
|
|
501
|
+
* contract as retention's `explicitSetting` — and the caller falls to the
|
|
502
|
+
* next precedence level. Every finite value is accepted here: the <= 0
|
|
503
|
+
* case is the documented disable switch, not garbage.
|
|
504
|
+
*/
|
|
505
|
+
function explicitMinutes(section, key) {
|
|
506
|
+
const raw = section?.[key];
|
|
507
|
+
if (raw === undefined)
|
|
508
|
+
return undefined;
|
|
509
|
+
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
|
510
|
+
emitNotice(t('ask.settings.invalid', { key, value: JSON.stringify(raw) }));
|
|
511
|
+
return undefined;
|
|
512
|
+
}
|
|
513
|
+
return raw;
|
|
514
|
+
}
|
|
515
|
+
/** One env slot: a finite number, or undefined when absent/garbage — env
|
|
516
|
+
* garbage falls to the default silently (retention's finiteEnv contract). */
|
|
517
|
+
function finiteEnvMinutes(raw) {
|
|
518
|
+
if (raw === undefined || raw.trim() === '')
|
|
519
|
+
return undefined;
|
|
520
|
+
const value = Number(raw);
|
|
521
|
+
return Number.isFinite(value) ? value : undefined;
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Resolve the ask-user timeout knobs through the repo's standard precedence
|
|
525
|
+
* chain — explicit settings.yaml values (`dsh-tui.askUser.*`) outrank the
|
|
526
|
+
* `DSH_TUI_ASK_USER_*` environment variables, which outrank the defaults
|
|
527
|
+
* (5 min idle / 10 min absolute). Invalid settings values emit one notice
|
|
528
|
+
* each (notice bridge) and fall to the next level; invalid env values fall
|
|
529
|
+
* back silently. Pure; `process.env` and the settings section are passed
|
|
530
|
+
* explicitly so tests can pin them.
|
|
531
|
+
*/
|
|
532
|
+
export function resolveAskUserTimeouts(settings, env = process.env) {
|
|
533
|
+
const idle = explicitMinutes(settings, 'idleMinutes') ?? finiteEnvMinutes(env[ASK_USER_IDLE_ENV]) ?? ASK_USER_IDLE_MINUTES_DEFAULT;
|
|
534
|
+
const absolute = explicitMinutes(settings, 'absoluteMinutes') ?? finiteEnvMinutes(env[ASK_USER_ABSOLUTE_ENV]) ?? ASK_USER_ABSOLUTE_MINUTES_DEFAULT;
|
|
535
|
+
return {
|
|
536
|
+
idleMs: minutesToMs(idle),
|
|
537
|
+
absoluteMs: minutesToMs(absolute),
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Earliest deadline at which the panel's timeout should fire, or null when
|
|
542
|
+
* no rule is armed. The idle deadline runs from `lastInputAt` (any
|
|
543
|
+
* keypress); the absolute deadline runs from `focusEnteredAt` (focus entry
|
|
544
|
+
* into the current question) and applies in BOTH phases — a review page
|
|
545
|
+
* entered late inherits its question's remaining budget, so the total wait
|
|
546
|
+
* per question stays bounded. Pure; the panel arms a timer on this.
|
|
547
|
+
*/
|
|
548
|
+
export function nextTimeoutDeadline(timeouts, now, focusEnteredAt, lastInputAt) {
|
|
549
|
+
if (timeouts.idleMs <= 0 && timeouts.absoluteMs <= 0)
|
|
550
|
+
return null;
|
|
551
|
+
let deadline = Number.POSITIVE_INFINITY;
|
|
552
|
+
if (timeouts.idleMs > 0)
|
|
553
|
+
deadline = lastInputAt + timeouts.idleMs;
|
|
554
|
+
if (timeouts.absoluteMs > 0)
|
|
555
|
+
deadline = Math.min(deadline, focusEnteredAt + timeouts.absoluteMs);
|
|
556
|
+
return Number.isFinite(deadline) ? deadline : null;
|
|
557
|
+
}
|
|
558
|
+
/** Whether a per-question pending answer counts as answered. */
|
|
559
|
+
function isAnswered(answer) {
|
|
560
|
+
return answer !== undefined
|
|
561
|
+
&& (answer.selected.length > 0 || (answer.custom !== undefined && answer.custom.trim() !== ''));
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* The timeout auto-answer for ONE unanswered question — what the panel
|
|
565
|
+
* writes into `perQuestion` when a timer fires on it. Priority:
|
|
566
|
+
*
|
|
567
|
+
* 1. a non-empty live sentinel buffer is committed as the custom answer
|
|
568
|
+
* (the panel's own ↑↓ arrow-exit semantics commit non-empty buffers, so
|
|
569
|
+
* typed-but-uncommitted text is honored, not discarded);
|
|
570
|
+
* 2. a `plan-review` question NEVER auto-approves — the first option that
|
|
571
|
+
* is not the intent's `approve` label is picked (decline by omission);
|
|
572
|
+
* 3. otherwise the FIRST option — the dsh tool contract declares the
|
|
573
|
+
* recommended choice by putting it first ("put it first and append
|
|
574
|
+
* '(Recommended)' to that label").
|
|
575
|
+
*
|
|
576
|
+
* The companion envelope note is returned alongside so the panel can fold
|
|
577
|
+
* it into the answer at settle time; the note never enters panel state
|
|
578
|
+
* (it would leak into the sentinel row / review rendering).
|
|
579
|
+
*/
|
|
580
|
+
export function timeoutAnswerFor(question, liveBuffer) {
|
|
581
|
+
const buffer = (liveBuffer ?? '').trim();
|
|
582
|
+
if (buffer !== '')
|
|
583
|
+
return { answer: { selected: [], custom: buffer }, note: TIMEOUT_CUSTOM_NOTE };
|
|
584
|
+
const intent = question.intent;
|
|
585
|
+
if (intent?.kind === 'plan-review') {
|
|
586
|
+
const declineOption = (question.options ?? []).find(option => option.label !== intent.approve);
|
|
587
|
+
if (declineOption !== undefined)
|
|
588
|
+
return { answer: { selected: [declineOption.label] }, note: TIMEOUT_PLAN_DECLINED_NOTE };
|
|
589
|
+
return { answer: { selected: [] }, note: TIMEOUT_PLAN_DECLINED_NOTE };
|
|
590
|
+
}
|
|
591
|
+
const first = question.options?.[0];
|
|
592
|
+
if (first !== undefined)
|
|
593
|
+
return { answer: { selected: [first.label] }, note: TIMEOUT_RECOMMENDED_NOTE };
|
|
594
|
+
return { answer: { selected: [] }, note: TIMEOUT_NO_DEFAULT_NOTE };
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* `buildAnswerEnvelope` with the timeout notes folded in: `notes` maps a
|
|
598
|
+
* question index to the note appended to that answer item's `custom` field
|
|
599
|
+
* (created when absent, concatenated when the answer already carries custom
|
|
600
|
+
* text — the buffer-commit case). The model reads the envelope as the tool
|
|
601
|
+
* result, so every automatic pick is declared in-band. Omitting `notes`
|
|
602
|
+
* reproduces `buildAnswerEnvelope` exactly.
|
|
603
|
+
*/
|
|
604
|
+
export function buildAnswerEnvelopeWithNotes(state, notes) {
|
|
605
|
+
const envelope = buildAnswerEnvelope(state);
|
|
606
|
+
if (notes === undefined || notes.size === 0)
|
|
607
|
+
return envelope;
|
|
608
|
+
envelope.answers.forEach((item, i) => {
|
|
609
|
+
const note = notes.get(i);
|
|
610
|
+
if (note === undefined)
|
|
611
|
+
return;
|
|
612
|
+
item.custom = item.custom !== undefined && item.custom !== '' ? `${item.custom} (${note})` : note;
|
|
613
|
+
});
|
|
614
|
+
return envelope;
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* The idle countdown for the panel footer: `m:ss` until the next deadline,
|
|
618
|
+
* floored at 0:00. Pure so tests pin it without a clock.
|
|
619
|
+
*/
|
|
620
|
+
export function formatCountdown(msRemaining) {
|
|
621
|
+
const totalSeconds = Math.max(0, Math.ceil(msRemaining / 1000));
|
|
622
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
623
|
+
return `${minutes}:${String(totalSeconds % 60).padStart(2, '0')}`;
|
|
624
|
+
}
|
|
459
625
|
/**
|
|
460
626
|
* Row index to land on after a single-select answer on question `answeredQi`
|
|
461
627
|
* (option toggle or committed custom text): advance the tab focus to the
|
|
@@ -563,7 +729,7 @@ export function toggleCollapse(state) {
|
|
|
563
729
|
// ------------------------------------------------- render: questions phase --
|
|
564
730
|
/** Render width budget for the questions phase (single flex label column). */
|
|
565
731
|
const QUESTIONS_COLUMNS = () => [
|
|
566
|
-
{ key: 'label', title: '
|
|
732
|
+
{ key: 'label', title: t('ask.col.selection'), flex: true },
|
|
567
733
|
];
|
|
568
734
|
/** Inline marker shown before a selected option's number (unselected: `○`). */
|
|
569
735
|
const OPTION_SELECTED_MARK = '●';
|
|
@@ -611,20 +777,20 @@ function renderTabStrip(fns, wrap, state) {
|
|
|
611
777
|
return fns.muted(clipToWidth(parts.join(' · '), wrap));
|
|
612
778
|
}
|
|
613
779
|
/** Render the questions pane (one focused question tab) as a flat table line list behind a scroll window. */
|
|
614
|
-
export function renderQuestionsView(theme, state, width, maxVisible = ASK_USER_MAX_VISIBLE) {
|
|
780
|
+
export function renderQuestionsView(theme, state, width, maxVisible = ASK_USER_MAX_VISIBLE, countdown) {
|
|
615
781
|
const fns = panelThemeFns(theme);
|
|
616
782
|
const wrap = Math.max(2, width - 2);
|
|
617
783
|
const title = state.questions.length === 1
|
|
618
|
-
? '
|
|
619
|
-
:
|
|
784
|
+
? t('ask.title.single')
|
|
785
|
+
: t('ask.title.multi', { index: state.focusQuestion + 1, total: state.questions.length });
|
|
620
786
|
const lines = [fns.accent(BOLD + clipToWidth(title, wrap) + RESET)];
|
|
621
787
|
if (state.questions.length >= 2) {
|
|
622
788
|
lines.push(renderTabStrip(fns, wrap, state));
|
|
623
789
|
}
|
|
624
790
|
const rows = buildRowList(state.questions, state.perQuestion, state.focusQuestion);
|
|
625
791
|
if (rows.length === 0) {
|
|
626
|
-
lines.push(fns.muted(clipToWidth('
|
|
627
|
-
return finalizeQuestionsView(fns, wrap, lines, state);
|
|
792
|
+
lines.push(fns.muted(clipToWidth(t('ask.empty'), wrap)));
|
|
793
|
+
return finalizeQuestionsView(fns, wrap, lines, state, '', countdown);
|
|
628
794
|
}
|
|
629
795
|
const columns = QUESTIONS_COLUMNS();
|
|
630
796
|
const widths = columnWidths(wrap - MARKER_W, columns);
|
|
@@ -748,14 +914,14 @@ export function renderQuestionsView(theme, state, width, maxVisible = ASK_USER_M
|
|
|
748
914
|
lines.push(...body.slice(offset, offset + visible));
|
|
749
915
|
const overflow = body.length > visible;
|
|
750
916
|
lines.push(fns.subtle(clipToWidth(tableRuleLine(widths, '┴'), wrap)));
|
|
751
|
-
return finalizeQuestionsView(fns, wrap, lines, state, overflow ? ` (${cursorRank}/${selectableTotal})` : '');
|
|
917
|
+
return finalizeQuestionsView(fns, wrap, lines, state, overflow ? ` (${cursorRank}/${selectableTotal})` : '', countdown);
|
|
752
918
|
}
|
|
753
|
-
function finalizeQuestionsView(fns, wrap, lines, state, scrollInfo = '') {
|
|
919
|
+
function finalizeQuestionsView(fns, wrap, lines, state, scrollInfo = '', countdown) {
|
|
754
920
|
if (state.attentionHint !== null) {
|
|
755
921
|
lines.push(fns.attention(clipToWidth(state.attentionHint, wrap)));
|
|
756
922
|
}
|
|
757
923
|
if (state.cancelHint) {
|
|
758
|
-
lines.push(fns.attention(clipToWidth('
|
|
924
|
+
lines.push(fns.attention(clipToWidth(t('ask.escHint'), wrap)));
|
|
759
925
|
}
|
|
760
926
|
lines.push('');
|
|
761
927
|
const multiTab = state.questions.length >= 2;
|
|
@@ -764,27 +930,28 @@ function finalizeQuestionsView(fns, wrap, lines, state, scrollInfo = '') {
|
|
|
764
930
|
// scroll readout rides FIRST so narrow terminals clip the tail hints,
|
|
765
931
|
// never the position.
|
|
766
932
|
const footer = state.customEditingFor !== null
|
|
767
|
-
? '
|
|
768
|
-
: `${multiTab ? '
|
|
933
|
+
? t('ask.footer.editing')
|
|
934
|
+
: `${multiTab ? t('ask.footer.tabs') : ''}${needsConfirmRow(state.questions) ? t('ask.footer.navToggle') : t('ask.footer.navSelect')}`;
|
|
769
935
|
// The (n/m) readout goes FIRST so narrow terminals clip the hint, never
|
|
770
|
-
// the scroll info.
|
|
771
|
-
|
|
936
|
+
// the scroll info. The auto-answer countdown trails — clipped first on a
|
|
937
|
+
// narrow terminal, and it re-renders every second with the footer clock.
|
|
938
|
+
lines.push(fns.subtle(clipToWidth(scrollInfo + footer + (countdown !== undefined ? ` · ${countdown}` : ''), wrap)));
|
|
772
939
|
return lines;
|
|
773
940
|
}
|
|
774
941
|
// -------------------------------------------------- render: review phase --
|
|
775
942
|
/** Render the review page for multi-question overlays (scroll-windowed). */
|
|
776
|
-
export function renderReviewView(theme, state, width, maxVisible = ASK_USER_MAX_VISIBLE) {
|
|
943
|
+
export function renderReviewView(theme, state, width, maxVisible = ASK_USER_MAX_VISIBLE, countdown) {
|
|
777
944
|
const fns = panelThemeFns(theme);
|
|
778
945
|
const wrap = Math.max(2, width - 2);
|
|
779
|
-
const lines = [fns.accent(BOLD + clipToWidth('
|
|
946
|
+
const lines = [fns.accent(BOLD + clipToWidth(t('ask.review.title'), wrap) + RESET)];
|
|
780
947
|
// Question column with a content cap + answer column flexes to the right edge.
|
|
781
948
|
// (columnWidths only assigns the remainder to ONE flex column; two flex
|
|
782
949
|
// columns would both get the whole remainder and clip the second off.)
|
|
783
950
|
const usable = wrap - MARKER_W;
|
|
784
951
|
const leftCap = Math.max(20, Math.floor(usable * 0.45));
|
|
785
952
|
const columns = [
|
|
786
|
-
{ key: 'question', title: '
|
|
787
|
-
{ key: 'answer', title: '
|
|
953
|
+
{ key: 'question', title: t('ask.review.colQuestion'), width: leftCap },
|
|
954
|
+
{ key: 'answer', title: t('ask.review.colAnswer'), flex: true },
|
|
788
955
|
];
|
|
789
956
|
const widths = columnWidths(usable, columns);
|
|
790
957
|
lines.push(fns.subtle(clipToWidth(tableRuleLine(widths, '┬'), wrap)));
|
|
@@ -807,8 +974,8 @@ export function renderReviewView(theme, state, width, maxVisible = ASK_USER_MAX_
|
|
|
807
974
|
// Submit row (review pane).
|
|
808
975
|
{
|
|
809
976
|
const selected = state.reviewIndex === state.questions.length;
|
|
810
|
-
const leftCell = padCell(clipToWidth('
|
|
811
|
-
const rightCell = padCell(clipToWidth(allQuestionsAnswered(state) ? '
|
|
977
|
+
const leftCell = padCell(clipToWidth(t('ask.review.submit'), widths[0]), widths[0]);
|
|
978
|
+
const rightCell = padCell(clipToWidth(allQuestionsAnswered(state) ? t('ask.review.ready') : t('ask.review.incomplete'), widths[1]), widths[1]);
|
|
812
979
|
const plain = `${rowMarker(selected)}${leftCell}${TABLE_SEP}${rightCell}`;
|
|
813
980
|
const line = clipToWidth(plain, wrap);
|
|
814
981
|
body.push(selected ? fns.accent(BOLD + line + RESET) : fns.muted(line));
|
|
@@ -822,9 +989,9 @@ export function renderReviewView(theme, state, width, maxVisible = ASK_USER_MAX_
|
|
|
822
989
|
lines.push(fns.attention(clipToWidth(state.attentionHint, wrap)));
|
|
823
990
|
}
|
|
824
991
|
lines.push('');
|
|
825
|
-
const footer = '
|
|
992
|
+
const footer = t('ask.review.footer');
|
|
826
993
|
const scrollInfo = body.length > visible ? ` (${state.reviewIndex + 1}/${body.length})` : '';
|
|
827
|
-
lines.push(fns.subtle(clipToWidth(scrollInfo + footer, wrap)));
|
|
994
|
+
lines.push(fns.subtle(clipToWidth(scrollInfo + footer + (countdown !== undefined ? ` · ${countdown}` : ''), wrap)));
|
|
828
995
|
return lines;
|
|
829
996
|
}
|
|
830
997
|
function formatAnswerForReview(answer) {
|
|
@@ -835,7 +1002,7 @@ function formatAnswerForReview(answer) {
|
|
|
835
1002
|
if (answer.custom !== undefined && answer.custom !== '')
|
|
836
1003
|
return foldText(`${CUSTOM_MARK}${answer.custom}`);
|
|
837
1004
|
if (answer.selected.length === 0)
|
|
838
|
-
return '
|
|
1005
|
+
return t('ask.review.noAnswer');
|
|
839
1006
|
return foldText(answer.selected.join(', '));
|
|
840
1007
|
}
|
|
841
1008
|
// ------------------------------------------------- render: folded strip --
|
|
@@ -850,16 +1017,16 @@ export function renderCollapsedLine(theme, state, width) {
|
|
|
850
1017
|
const fns = panelThemeFns(theme);
|
|
851
1018
|
const wrap = Math.max(2, width - 2);
|
|
852
1019
|
if (state.cancelHint) {
|
|
853
|
-
return fns.attention(clipToWidth('
|
|
1020
|
+
return fns.attention(clipToWidth(t('ask.collapsed.esc'), wrap));
|
|
854
1021
|
}
|
|
855
1022
|
if (state.phase === 'review') {
|
|
856
|
-
return fns.accent(BOLD + clipToWidth('
|
|
1023
|
+
return fns.accent(BOLD + clipToWidth(t('ask.collapsed.review'), wrap) + RESET);
|
|
857
1024
|
}
|
|
858
1025
|
const answered = state.perQuestion.filter(answer => answer.selected.length > 0 || (answer.custom !== undefined && answer.custom.trim() !== '')).length;
|
|
859
1026
|
const label = state.questions.length === 1
|
|
860
|
-
? '
|
|
861
|
-
:
|
|
862
|
-
return fns.accent(BOLD + clipToWidth(`${label} ·
|
|
1027
|
+
? t('ask.collapsed.question')
|
|
1028
|
+
: t('ask.collapsed.questions', { index: state.focusQuestion + 1, total: state.questions.length, answered });
|
|
1029
|
+
return fns.accent(BOLD + clipToWidth(`${label} · ${t('ask.collapsed.expand')}`, wrap) + RESET);
|
|
863
1030
|
}
|
|
864
1031
|
// ---------------------------------- paste sanitiser + clipboard helpers --
|
|
865
1032
|
/**
|
|
@@ -1016,8 +1183,17 @@ export function consumeRightClickPaste(data, tui, clipboardImpl) {
|
|
|
1016
1183
|
* service already screens entry-time aborts, and a step aborted after this
|
|
1017
1184
|
* point discards the tool result anyway — resolving keeps the pending promise
|
|
1018
1185
|
* from ever hanging either way.
|
|
1186
|
+
*
|
|
1187
|
+
* `timeouts` arms the auto-answer rules (see {@link AskUserTimeouts}): the
|
|
1188
|
+
* FOCUSED question's idle window and hard cap. A firing timer auto-answers
|
|
1189
|
+
* the focused question (recommended option, plan-safe —
|
|
1190
|
+
* {@link timeoutAnswerFor}), hops to the next unanswered question with fresh
|
|
1191
|
+
* budgets, and — once nothing is left unanswered — settles the envelope
|
|
1192
|
+
* directly (the review page exists for a human double-check an absent human
|
|
1193
|
+
* cannot do). Defaults to disabled: callers opt in through the provider's
|
|
1194
|
+
* `resolveTimeouts`.
|
|
1019
1195
|
*/
|
|
1020
|
-
export function openAskUserPanel(deps, questions, signal) {
|
|
1196
|
+
export function openAskUserPanel(deps, questions, signal, timeouts = ASK_USER_TIMEOUTS_DISABLED) {
|
|
1021
1197
|
if (signal?.aborted) {
|
|
1022
1198
|
return Promise.resolve(buildDeclinedEnvelope(questions));
|
|
1023
1199
|
}
|
|
@@ -1025,6 +1201,90 @@ export function openAskUserPanel(deps, questions, signal) {
|
|
|
1025
1201
|
const clock = deps.now ?? Date.now;
|
|
1026
1202
|
const state = initialState(questions);
|
|
1027
1203
|
let settled = false;
|
|
1204
|
+
// ---- timeout bookkeeping (per focused question) ----
|
|
1205
|
+
// focusEnteredAt is re-stamped whenever the focused question changes
|
|
1206
|
+
// (open, tab switch, auto/manual advance, review jump-back); lastInputAt
|
|
1207
|
+
// on every keypress. The two deadlines (idle from lastInputAt, absolute
|
|
1208
|
+
// from focusEnteredAt) arm ONE timer for the earlier of the two.
|
|
1209
|
+
// The clock is NEVER read while the rules are disabled — tests inject a
|
|
1210
|
+
// queue clock whose exact consumption order the double-Esc timings
|
|
1211
|
+
// depend on, so every read here is gated behind `timeoutEnabled` and
|
|
1212
|
+
// both stamps initialize on the first arm().
|
|
1213
|
+
const timeoutEnabled = timeouts.idleMs > 0 || timeouts.absoluteMs > 0;
|
|
1214
|
+
let focusEnteredAt = 0;
|
|
1215
|
+
let lastInputAt = 0;
|
|
1216
|
+
let stampedFocus = -1;
|
|
1217
|
+
let stampedInput = false;
|
|
1218
|
+
let timer = null;
|
|
1219
|
+
const timeoutNotes = new Map();
|
|
1220
|
+
const disarm = () => {
|
|
1221
|
+
if (timer !== null) {
|
|
1222
|
+
clearTimeout(timer);
|
|
1223
|
+
timer = null;
|
|
1224
|
+
}
|
|
1225
|
+
};
|
|
1226
|
+
/** (Re)arm the timeout timer; cheap + idempotent, call after every
|
|
1227
|
+
* keypress and every focus/phase mutation. */
|
|
1228
|
+
const arm = () => {
|
|
1229
|
+
if (!timeoutEnabled || settled)
|
|
1230
|
+
return;
|
|
1231
|
+
if (state.focusQuestion !== stampedFocus) {
|
|
1232
|
+
stampedFocus = state.focusQuestion;
|
|
1233
|
+
focusEnteredAt = clock();
|
|
1234
|
+
}
|
|
1235
|
+
if (!stampedInput) {
|
|
1236
|
+
stampedInput = true;
|
|
1237
|
+
lastInputAt = clock();
|
|
1238
|
+
}
|
|
1239
|
+
disarm();
|
|
1240
|
+
const deadline = nextTimeoutDeadline(timeouts, clock(), focusEnteredAt, lastInputAt);
|
|
1241
|
+
if (deadline === null)
|
|
1242
|
+
return;
|
|
1243
|
+
timer = setTimeout(() => {
|
|
1244
|
+
timer = null;
|
|
1245
|
+
onTimeoutFire();
|
|
1246
|
+
}, Math.max(0, deadline - clock()));
|
|
1247
|
+
timer.unref?.();
|
|
1248
|
+
};
|
|
1249
|
+
/** A deadline fired on the focused question: auto-answer it if still
|
|
1250
|
+
* unanswered, then settle (everything answered) or hop to the next
|
|
1251
|
+
* unanswered question with fresh budgets. */
|
|
1252
|
+
const onTimeoutFire = () => {
|
|
1253
|
+
if (settled)
|
|
1254
|
+
return;
|
|
1255
|
+
if (state.phase === 'review') {
|
|
1256
|
+
// The review page only opens with every question answered, so this
|
|
1257
|
+
// submits the answers already given — nothing is picked for the user.
|
|
1258
|
+
settle(buildAnswerEnvelopeWithNotes(state, timeoutNotes));
|
|
1259
|
+
emitNotice(t('ask.notice.timeoutReview'));
|
|
1260
|
+
close();
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
const qi = state.focusQuestion;
|
|
1264
|
+
const question = state.questions[qi];
|
|
1265
|
+
if (question !== undefined && !isAnswered(state.perQuestion[qi])) {
|
|
1266
|
+
const { answer, note } = timeoutAnswerFor(question, state.customInputs[qi]);
|
|
1267
|
+
if (note !== undefined)
|
|
1268
|
+
timeoutNotes.set(qi, note);
|
|
1269
|
+
if (answer.custom !== undefined) {
|
|
1270
|
+
Object.assign(state, setCustomAnswer(state, qi, answer.custom));
|
|
1271
|
+
}
|
|
1272
|
+
else {
|
|
1273
|
+
const perQuestion = state.perQuestion.slice();
|
|
1274
|
+
perQuestion[qi] = answer;
|
|
1275
|
+
Object.assign(state, { ...state, perQuestion });
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
if (allQuestionsAnswered(state)) {
|
|
1279
|
+
settle(buildAnswerEnvelopeWithNotes(state, timeoutNotes));
|
|
1280
|
+
emitNotice(t('ask.notice.timeoutAuto'));
|
|
1281
|
+
close();
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
Object.assign(state, advanceAfterAnswer(state, qi));
|
|
1285
|
+
lastInputAt = clock();
|
|
1286
|
+
arm();
|
|
1287
|
+
};
|
|
1028
1288
|
/**
|
|
1029
1289
|
* Terminal close: unmount the dock panel, clear the modal flag (BEFORE
|
|
1030
1290
|
* restoreFocus so refocusEditor's guard sees the modal gone), and hand
|
|
@@ -1032,6 +1292,7 @@ export function openAskUserPanel(deps, questions, signal) {
|
|
|
1032
1292
|
* dismissed then (see below), so focus lands on the current editor.
|
|
1033
1293
|
*/
|
|
1034
1294
|
const close = () => {
|
|
1295
|
+
disarm();
|
|
1035
1296
|
livePanels.delete(panelEntry);
|
|
1036
1297
|
unmount();
|
|
1037
1298
|
deps.setModalActive(false);
|
|
@@ -1072,9 +1333,18 @@ export function openAskUserPanel(deps, questions, signal) {
|
|
|
1072
1333
|
// panel claims one extra interior line. Fake TUIs without a
|
|
1073
1334
|
// `terminal` degrade to ASK_USER_MAX_VISIBLE.
|
|
1074
1335
|
const maxVisible = askUserMaxVisibleForRows(deps.tui.terminal?.rows, state.phase === 'questions' && state.questions.length >= 2 ? 1 : 0);
|
|
1336
|
+
// Auto-answer countdown for the focused question's next deadline —
|
|
1337
|
+
// recomputed per render; the footer clock's 1s requestRender keeps it
|
|
1338
|
+
// fresh without a timer of its own.
|
|
1339
|
+
const countdown = timeoutEnabled && !settled
|
|
1340
|
+
? (() => {
|
|
1341
|
+
const deadline = nextTimeoutDeadline(timeouts, clock(), focusEnteredAt, lastInputAt);
|
|
1342
|
+
return deadline === null ? undefined : t('ask.autoIn', { time: formatCountdown(deadline - clock()) });
|
|
1343
|
+
})()
|
|
1344
|
+
: undefined;
|
|
1075
1345
|
const inner = state.phase === 'review'
|
|
1076
|
-
? renderReviewView(theme, state, innerWidth, maxVisible)
|
|
1077
|
-
: renderQuestionsView(theme, state, innerWidth, maxVisible);
|
|
1346
|
+
? renderReviewView(theme, state, innerWidth, maxVisible, countdown)
|
|
1347
|
+
: renderQuestionsView(theme, state, innerWidth, maxVisible, countdown);
|
|
1078
1348
|
return [
|
|
1079
1349
|
panelTopBorder(boxWidth, borderFg),
|
|
1080
1350
|
...inner.map(line => borderedRow(boxWidth, borderFg, line)),
|
|
@@ -1084,6 +1354,13 @@ export function openAskUserPanel(deps, questions, signal) {
|
|
|
1084
1354
|
handleInput(data) {
|
|
1085
1355
|
if (settled)
|
|
1086
1356
|
return;
|
|
1357
|
+
// Every keypress is presence: restart the focused question's idle
|
|
1358
|
+
// window, then re-arm below at each focus-changing branch (arm is
|
|
1359
|
+
// idempotent and re-stamps focusEnteredAt when the focus moved).
|
|
1360
|
+
if (timeoutEnabled) {
|
|
1361
|
+
lastInputAt = clock();
|
|
1362
|
+
arm();
|
|
1363
|
+
}
|
|
1087
1364
|
const kb = getKeybindings();
|
|
1088
1365
|
// The fold toggle outranks everything — including the sentinel edit:
|
|
1089
1366
|
// folding commits the buffer first (the ↑↓ arrow-exit semantics), so
|
|
@@ -1109,10 +1386,12 @@ export function openAskUserPanel(deps, questions, signal) {
|
|
|
1109
1386
|
if (state.phase === 'questions' && state.questions.length >= 2) {
|
|
1110
1387
|
if (matchesKey(data, 'right') || matchesKey(data, 'tab')) {
|
|
1111
1388
|
Object.assign(state, switchFocus(state, 1));
|
|
1389
|
+
arm();
|
|
1112
1390
|
return;
|
|
1113
1391
|
}
|
|
1114
1392
|
if (matchesKey(data, 'left') || matchesKey(data, 'shift+tab')) {
|
|
1115
1393
|
Object.assign(state, switchFocus(state, -1));
|
|
1394
|
+
arm();
|
|
1116
1395
|
return;
|
|
1117
1396
|
}
|
|
1118
1397
|
}
|
|
@@ -1151,6 +1430,9 @@ export function openAskUserPanel(deps, questions, signal) {
|
|
|
1151
1430
|
deps.tui.setFocus(panel);
|
|
1152
1431
|
deps.setModalActive(true);
|
|
1153
1432
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
1433
|
+
// Arm the timeout rules (no-op when disabled): the focused question's
|
|
1434
|
+
// idle window + hard cap start at mount.
|
|
1435
|
+
arm();
|
|
1154
1436
|
// Register the live panel so the right-click → paste hook (installed
|
|
1155
1437
|
// at the TUI level in tui.ts) can find an editing panel to feed. The
|
|
1156
1438
|
// entry is removed in `close()` above.
|
|
@@ -1218,13 +1500,14 @@ export function openAskUserPanel(deps, questions, signal) {
|
|
|
1218
1500
|
cancelHint: false,
|
|
1219
1501
|
attentionHint: null,
|
|
1220
1502
|
});
|
|
1503
|
+
arm();
|
|
1221
1504
|
return;
|
|
1222
1505
|
}
|
|
1223
1506
|
if (!allQuestionsAnswered(s)) {
|
|
1224
|
-
Object.assign(s, { cancelHint: false, attentionHint:
|
|
1507
|
+
Object.assign(s, { cancelHint: false, attentionHint: t('ask.incomplete') });
|
|
1225
1508
|
return;
|
|
1226
1509
|
}
|
|
1227
|
-
settle(
|
|
1510
|
+
settle(buildAnswerEnvelopeWithNotes(s, timeoutNotes));
|
|
1228
1511
|
close();
|
|
1229
1512
|
return;
|
|
1230
1513
|
}
|
|
@@ -1243,7 +1526,7 @@ export function openAskUserPanel(deps, questions, signal) {
|
|
|
1243
1526
|
// submits immediately. multiSelect never auto-submits — the user may
|
|
1244
1527
|
// want more toggles, so it routes through the Confirm row instead.
|
|
1245
1528
|
if (canAutoSubmit(next)) {
|
|
1246
|
-
settle(
|
|
1529
|
+
settle(buildAnswerEnvelopeWithNotes(s, timeoutNotes));
|
|
1247
1530
|
close();
|
|
1248
1531
|
}
|
|
1249
1532
|
else {
|
|
@@ -1257,6 +1540,7 @@ export function openAskUserPanel(deps, questions, signal) {
|
|
|
1257
1540
|
&& (answer.selected.length > 0 || (answer.custom ?? '').trim() !== '');
|
|
1258
1541
|
if (answered && s.questions[row.questionIndex]?.multiSelect !== true) {
|
|
1259
1542
|
Object.assign(s, advanceAfterAnswer(s, row.questionIndex));
|
|
1543
|
+
arm();
|
|
1260
1544
|
}
|
|
1261
1545
|
}
|
|
1262
1546
|
return;
|
|
@@ -1272,7 +1556,7 @@ export function openAskUserPanel(deps, questions, signal) {
|
|
|
1272
1556
|
Object.assign(s, { phase: 'review', reviewIndex: 0, cancelHint: false, attentionHint: null });
|
|
1273
1557
|
}
|
|
1274
1558
|
else {
|
|
1275
|
-
Object.assign(s, { cancelHint: false, attentionHint:
|
|
1559
|
+
Object.assign(s, { cancelHint: false, attentionHint: t('ask.incomplete') });
|
|
1276
1560
|
}
|
|
1277
1561
|
}
|
|
1278
1562
|
}
|
|
@@ -1320,7 +1604,7 @@ export function openAskUserPanel(deps, questions, signal) {
|
|
|
1320
1604
|
// hint slot so the "Copied" line rides the same render path
|
|
1321
1605
|
// every other transient hint already uses.
|
|
1322
1606
|
writeClipboard(buffer).catch(() => { });
|
|
1323
|
-
s.attentionHint = '
|
|
1607
|
+
s.attentionHint = t('ask.copied');
|
|
1324
1608
|
}
|
|
1325
1609
|
return;
|
|
1326
1610
|
}
|
|
@@ -1350,12 +1634,13 @@ export function openAskUserPanel(deps, questions, signal) {
|
|
|
1350
1634
|
if (s.perQuestion[qi]?.custom === undefined)
|
|
1351
1635
|
return;
|
|
1352
1636
|
if (canAutoSubmit(s)) {
|
|
1353
|
-
settle(
|
|
1637
|
+
settle(buildAnswerEnvelopeWithNotes(s, timeoutNotes));
|
|
1354
1638
|
close();
|
|
1355
1639
|
return;
|
|
1356
1640
|
}
|
|
1357
1641
|
if (s.questions.length >= 2) {
|
|
1358
1642
|
Object.assign(s, advanceAfterAnswer(s, qi));
|
|
1643
|
+
arm();
|
|
1359
1644
|
}
|
|
1360
1645
|
}
|
|
1361
1646
|
});
|
|
@@ -1400,7 +1685,13 @@ export function registerAskUserProvider(ctx, deps) {
|
|
|
1400
1685
|
const controller = new AbortController();
|
|
1401
1686
|
controllers.set(request, controller);
|
|
1402
1687
|
request.signal?.addEventListener('abort', () => controller.abort(), { once: true });
|
|
1403
|
-
|
|
1688
|
+
const open = (timeouts) => openAskUserPanel(deps, request.questions, controller.signal, timeouts);
|
|
1689
|
+
if (deps.resolveTimeouts === undefined)
|
|
1690
|
+
return open(ASK_USER_TIMEOUTS_DISABLED);
|
|
1691
|
+
// Resolved fresh per ask so a committed settings change applies to
|
|
1692
|
+
// the NEXT question without a reload; a resolution failure degrades
|
|
1693
|
+
// to the disabled panel rather than failing the ask.
|
|
1694
|
+
return deps.resolveTimeouts().then(open, () => open(ASK_USER_TIMEOUTS_DISABLED));
|
|
1404
1695
|
},
|
|
1405
1696
|
settled: request => {
|
|
1406
1697
|
// Another surface answered first — close the panel through the
|
|
@@ -1432,7 +1723,10 @@ export function registerAskUserProvider(ctx, deps) {
|
|
|
1432
1723
|
if (asking !== undefined && String(asking) !== mine)
|
|
1433
1724
|
return next();
|
|
1434
1725
|
}
|
|
1435
|
-
|
|
1726
|
+
const open = (timeouts) => openAskUserPanel(deps, request.questions, request.signal, timeouts);
|
|
1727
|
+
if (deps.resolveTimeouts === undefined)
|
|
1728
|
+
return open(ASK_USER_TIMEOUTS_DISABLED);
|
|
1729
|
+
return deps.resolveTimeouts().then(open, () => open(ASK_USER_TIMEOUTS_DISABLED));
|
|
1436
1730
|
});
|
|
1437
1731
|
}
|
|
1438
1732
|
//# sourceMappingURL=ask-user.js.map
|