Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/ui/tui/__tests__/WizardAskScreen.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ vi.mock('../../../utils/analytics.js', () => ({

import { WizardStore } from '@ui/tui/store';
import {
askEscapeHint,
handleAskKey,
isRequiredButEmpty,
} from '@ui/tui/screens/WizardAskScreen';
Expand Down Expand Up @@ -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);
Expand Down
35 changes: 33 additions & 2 deletions src/ui/tui/screens/WizardAskScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -246,13 +269,21 @@ export const WizardAskScreen = ({ store }: WizardAskScreenProps) => {
<Box marginTop={1}>
<Text color={Colors.accent}>
{Icons.warning} This field is required — type an answer, or press
ESC to skip.
ESC to {escapeHint}.
</Text>
</Box>
)}
{canSkipOne && (
<Box marginTop={1}>
<Text dimColor>
Optional — press <Text color={Colors.accent}>ENTER</Text> on an
empty answer to skip just this one.
</Text>
</Box>
)}
<Box marginTop={1}>
<Text dimColor>
<Text color={Colors.accent}>ESC</Text> skip
<Text color={Colors.accent}>ESC</Text> {escapeHint}
</Text>
</Box>
</ModalOverlay>
Expand Down
Loading