diff --git a/src/ui/tui/__tests__/WizardAskScreen.test.ts b/src/ui/tui/__tests__/WizardAskScreen.test.ts
index f210f388..b751c571 100644
--- a/src/ui/tui/__tests__/WizardAskScreen.test.ts
+++ b/src/ui/tui/__tests__/WizardAskScreen.test.ts
@@ -21,6 +21,7 @@ vi.mock('../../../utils/analytics.js', () => ({
import { WizardStore } from '@ui/tui/store';
import {
+ askEscapeHint,
handleAskKey,
isRequiredButEmpty,
} from '@ui/tui/screens/WizardAskScreen';
@@ -62,6 +63,22 @@ describe('handleAskKey', () => {
});
});
+describe('askEscapeHint', () => {
+ it('says plain "skip" for a single-question request', () => {
+ expect(askEscapeHint(1, 0)).toBe('skip');
+ });
+
+ it('names the scope on a multi-question request', () => {
+ expect(askEscapeHint(5, 0)).toBe('skip all 5 questions');
+ });
+
+ it('warns that answers already given are discarded', () => {
+ expect(askEscapeHint(5, 4)).toBe(
+ 'skip all 5 questions, discarding the 4 you answered',
+ );
+ });
+});
+
describe('isRequiredButEmpty', () => {
it('blocks an empty required text field (required defaults to true)', () => {
expect(isRequiredButEmpty({}, '')).toBe(true);
diff --git a/src/ui/tui/screens/WizardAskScreen.tsx b/src/ui/tui/screens/WizardAskScreen.tsx
index e47ba643..80931841 100644
--- a/src/ui/tui/screens/WizardAskScreen.tsx
+++ b/src/ui/tui/screens/WizardAskScreen.tsx
@@ -37,6 +37,24 @@ export function handleAskKey(
if (key.escape) store.cancelPendingQuestion();
}
+/**
+ * What pressing Esc actually does, phrased for the footer hint.
+ *
+ * Esc declines the *whole* request — {@link WizardStore.cancelPendingQuestion}
+ * builds a cancelled answer for every question, so the ones already typed are
+ * discarded too. The footer used to label that "skip", which on a multi-question
+ * request reads as "skip this field": the warehouse task walks a source's
+ * credentials one field at a time, several of them optional, and a user who
+ * pressed Esc to pass on an optional field instead threw away the whole source
+ * and dropped the agent onto its browser-handoff fallback. Naming the scope
+ * costs a few characters and makes the destructive key read as destructive.
+ */
+export function askEscapeHint(total: number, answered: number): string {
+ if (total <= 1) return 'skip';
+ if (answered <= 0) return `skip all ${total} questions`;
+ return `skip all ${total} questions, discarding the ${answered} you answered`;
+}
+
/**
* Whether an answer would leave a required question effectively unanswered.
*
@@ -170,6 +188,11 @@ export const WizardAskScreen = ({ store }: WizardAskScreenProps) => {
const total = pending.questions.length;
const progress = total > 1 ? `Question ${index + 1} of ${total}` : null;
+ const escapeHint = askEscapeHint(total, index);
+ // An optional text field already accepts an empty Enter (see
+ // `isRequiredButEmpty`) — it just never said so, leaving Esc as the only
+ // visible exit from a question the user did not want to answer.
+ const canSkipOne = question.kind === 'text' && question.required === false;
const submit = (value: string | string[]) => {
// Don't let a required field go through empty — it would reach the agent as
@@ -246,13 +269,21 @@ export const WizardAskScreen = ({ store }: WizardAskScreenProps) => {
{Icons.warning} This field is required — type an answer, or press
- ESC to skip.
+ ESC to {escapeHint}.
+
+
+ )}
+ {canSkipOne && (
+
+
+ Optional — press ENTER on an
+ empty answer to skip just this one.
)}
- ESC skip
+ ESC {escapeHint}