Skip to content

feat(ui): add AbortSignal support to prompts#1809

Open
Satvik-art-creator wants to merge 1 commit into
Karanjot786:mainfrom
Satvik-art-creator:feat/prompt-abort-signal
Open

feat(ui): add AbortSignal support to prompts#1809
Satvik-art-creator wants to merge 1 commit into
Karanjot786:mainfrom
Satvik-art-creator:feat/prompt-abort-signal

Conversation

@Satvik-art-creator

@Satvik-art-creator Satvik-art-creator commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Description

This PR adds support for programmatic cancellation to all interactive prompt components (TextInput, Select, NumberInput, and Form) using the standard AbortController / AbortSignal API. When the signal aborts, the prompt unmounts the inline app, cleanly restores the terminal's raw mode configuration, and rejects the prompt promise with TermUIAbortError.

Related Issue

Closes #64

Which package(s)?

@termuijs/ui, @termuijs/widgets

Type of Change

  • 🐛 Bug fix (type:bug)
  • ✨ Feature (type:feature)
  • 📝 Docs (type:docs)
  • 🧪 Tests (type:testing)
  • ♻️ Refactor (type:refactor)
  • 🎨 Design / UX (type:design)
  • ♿ Accessibility (type:accessibility)
  • ⚡ Performance (type:performance)
  • 🔧 DevOps / CI (type:devops)
  • 🔒 Security (type:security)

Checklist

  • ⭐ You starred the repo. The needs-star check blocks your merge otherwise.
  • Tests pass locally: bun vitest run
  • Build passes: bun run build
  • Typecheck passes: bun run typecheck
  • You read CONTRIBUTING.md.
  • Your PR title follows type: short description.
  • Widget state mutators call markDirty() (if your change affects rendering).
  • No new any types without an inline comment explaining why.
  • No unrelated refactors bundled into this PR.

GSSoC 2026 Participation

  • You are a GSSoC 2026 contributor.
  • Your GSSoC profile: https://gssoc.girlscript.org/profile/172e996c-3943-4a86-8249-1f01922d0f64

Screenshots / Recordings (UI changes)

Notes for the Reviewer

The implementation ensures that:

  • Cleanups of event listeners are registered correctly.
  • If the signal is already aborted before the prompt starts running, it rejects immediately without configuring raw mode.
  • Custom/mock TTY and raw-mode spys are used to verify the cleanup flow under unit testing.

Summary by CodeRabbit

  • New Features

    • Prompt-related controls now support cancellation with an abort signal, so in-progress interactions can be stopped cleanly.
    • New prompt helpers are available for creating select, text, number, and form inputs.
    • The main prompt API can now be used both as a callable prompt and with its existing text, confirm, and select helpers.
  • Bug Fixes

    • Improved cleanup when prompts are cancelled, helping avoid stuck terminal input states.

@github-actions github-actions Bot added the type:feature +10 pts. New feature. label Jun 24, 2026
@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds optional AbortSignal support to TextInput, Select, NumberInput, and Form widgets and their options interfaces. Introduces promptWidget in prompts.ts with an abort/cleanup lifecycle, new widget-constructor helpers (select, textInput, numberInput, form), a redesigned callable prompt export, and a Vitest test suite verifying rejection and raw-mode restoration.

Changes

AbortSignal Integration for Prompt Widgets

Layer / File(s) Summary
AbortSignal fields on TextInput, Select, NumberInput, Form
packages/widgets/src/input/TextInput.ts, packages/ui/src/Select.ts, packages/ui/src/NumberInput.ts, packages/ui/src/Form.ts
Each widget class and its options interface gains signal?: AbortSignal and constructor wiring that assigns options.signal to this.signal.
promptWidget abort lifecycle and widget-constructor helpers
packages/ui/src/prompts.ts
Imports reworked to TermUI core/widgets. promptWidget mounts an inline App, intercepts _onSubmit/_onSelect, registers an abort listener that rejects with TermUIAbortError and unmounts, and handles pre-aborted signals immediately. New helpers select, textInput, numberInput, form forward signal. prompt replaced with a callable Prompt interface via Object.assign.
Public re-exports and abort test suite
packages/ui/src/index.ts, packages/ui/src/prompts-abort.test.ts
index.ts extends the prompts.js re-export to include select, textInput, numberInput, and form. The new test suite verifies that aborting an AbortController while each prompt is active rejects with TermUIAbortError and calls setRawMode(false), and that a pre-aborted signal rejects immediately.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant prompt as prompt / promptWidget
  participant App
  participant Widget
  participant AbortSignal

  Caller->>prompt: prompt(select({ options, signal }))
  prompt->>AbortSignal: check signal.aborted
  prompt->>App: new App(widget, inline mode)
  prompt->>AbortSignal: addEventListener("abort", abortHandler)
  App-->>Widget: mount and render

  alt AbortController.abort() called
    AbortSignal->>prompt: abort event fires
    prompt->>App: unmount()
    prompt->>AbortSignal: removeEventListener
    prompt-->>Caller: reject(new TermUIAbortError)
  else User submits widget
    Widget-->>prompt: _onSubmit / _onSelect intercepted
    prompt->>App: unmount()
    prompt->>AbortSignal: removeEventListener
    prompt-->>Caller: resolve(value)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

type:feature, area:ui, area:widgets, level:intermediate, type:testing

Suggested reviewers

  • Karanjot786

Poem

🐇 A signal arrives, the rabbit stands tall,
AbortController calls — the prompt answers the call.
Raw mode restored, no terminal smeared,
TermUIAbortError raised, the path is now cleared.
Four widgets updated, tests written with care,
The rabbit hops onward — no dirty state there! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the main change: AbortSignal support for prompts.
Description check ✅ Passed The PR description fills the required sections and includes the linked issue, packages, type, checklist, and reviewer notes.
Linked Issues check ✅ Passed The changes add signal support to TextInput, Select, NumberInput, and Form and add abort tests that verify cleanup and TermUIAbortError.
Out of Scope Changes check ✅ Passed The extra prompt helper exports and prompt API wiring are still directly related to the AbortSignal prompt feature.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added area:widgets @termuijs/widgets area:ui @termuijs/ui type:testing +10 pts. Tests. labels Jun 24, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/ui/src/Select.ts (1)

13-31: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Wire keyboard handling before using Select as a prompt widget.

promptWidget resolves Select only through _onSelect, but this class has no handleKey() or events.on('key', ...) path to call confirm(). prompt(select(...)) can hang during normal keyboard use; add a lowercase-key handler for enter, space, up, down, and escape and subscribe it in the constructor. As per coding guidelines, “Interactive components must implement handleKey(event: KeyEvent) with key names in lowercase.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/Select.ts` around lines 13 - 31, The Select widget currently
has no keyboard event path to trigger confirm(), so promptWidget can only
resolve via _onSelect and may hang during normal use. Update Select to implement
handleKey(event: KeyEvent) with lowercase key names, covering enter, space, up,
down, and escape, and wire that handler in the constructor via the widget’s key
event subscription so keyboard interaction can open, navigate, confirm, and
cancel the selection.

Source: Coding guidelines

packages/ui/src/Form.ts (1)

14-38: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Suppress submit callbacks after an abort during async validation.

signal is stored, but submit() never checks it after await Promise.all(validationPromises). If the prompt aborts while validation is pending, the promise rejects and unmounts, but _onSubmit can still fire later with stale values.

Proposed fix
         this._isValidating = false;
+        if (this.signal?.aborted) {
+            this.markDirty();
+            return;
+        }
         if (!hasErr) this._onSubmit?.(this.values);
         this.markDirty();

Also applies to: 59-86

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/Form.ts` around lines 14 - 38, In Form.submit() the abort
signal is not rechecked after async validation, so _onSubmit can still run with
stale values if the prompt aborts mid-validation. Update the submit flow in Form
to check this.signal after awaiting Promise.all(validationPromises) and before
invoking the submit callback, and bail out early if the signal is aborted; use
the existing Form class, submit() logic, and _onSubmit callback path as the main
places to change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/ui/src/prompts-abort.test.ts`:
- Around line 10-115: The abort prompt tests rely on stdio monkeypatching
instead of the real Screen harness, so update the tests to render through Screen
from `@termuijs/core` using updateRect() and render() before exercising prompt()
with select, textInput, numberInput, and form. Remove the direct
process.stdin/process.stdout mocking approach, create a real Screen test setup,
and assert the abort behavior through the rendered widget flow while still
verifying TermUIAbortError and cleanup.

In `@packages/ui/src/prompts.ts`:
- Around line 130-173: In promptWidget, handle an already-aborted widget.signal
before the process.stdin.isTTY check so pre-aborted prompts return
TermUIAbortError instead of NonInteractiveError. Read the signal early, reject
immediately if signal.aborted, and only then continue to the TTY guard and App
construction; keep the existing onAbort cleanup path consistent with this
early-exit behavior.
- Around line 217-220: The prompt promise is only handled on `app.mount()`
rejection, so it can stay pending when `App.exit()` or SIGINT causes the app to
unmount without `_onSubmit`/`_onSelect` firing. Update the `app.mount()` flow in
`prompts.ts` to also settle the outer promise when mount completes normally due
to exit, using the existing `cleanup()` path and a clear resolve/reject decision
tied to the prompt state. Make sure the fix is applied around `app.mount()` and
the prompt lifecycle handlers so the returned promise always settles.
- Around line 183-212: Add a brief inline rationale for each type assertion in
the prompt submission handlers, or refactor the flow to avoid the assertions
entirely. In the `prompt`/submit logic around `TextInput`, `NumberInput`,
`Form`, and the custom widget branch, annotate why `resolve(...)` must use `as
T` or `as unknown as T` so the type narrowing is clear to future readers. Keep
the explanation close to the `originalOnSubmit` overrides and `resolve` calls in
`packages/ui/src/prompts.ts`.

---

Outside diff comments:
In `@packages/ui/src/Form.ts`:
- Around line 14-38: In Form.submit() the abort signal is not rechecked after
async validation, so _onSubmit can still run with stale values if the prompt
aborts mid-validation. Update the submit flow in Form to check this.signal after
awaiting Promise.all(validationPromises) and before invoking the submit
callback, and bail out early if the signal is aborted; use the existing Form
class, submit() logic, and _onSubmit callback path as the main places to change.

In `@packages/ui/src/Select.ts`:
- Around line 13-31: The Select widget currently has no keyboard event path to
trigger confirm(), so promptWidget can only resolve via _onSelect and may hang
during normal use. Update Select to implement handleKey(event: KeyEvent) with
lowercase key names, covering enter, space, up, down, and escape, and wire that
handler in the constructor via the widget’s key event subscription so keyboard
interaction can open, navigate, confirm, and cancel the selection.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: be7d61ee-b519-4a90-ad39-53d4e0a61868

📥 Commits

Reviewing files that changed from the base of the PR and between 0fdff96 and 76c84b7.

📒 Files selected for processing (7)
  • packages/ui/src/Form.ts
  • packages/ui/src/NumberInput.ts
  • packages/ui/src/Select.ts
  • packages/ui/src/index.ts
  • packages/ui/src/prompts-abort.test.ts
  • packages/ui/src/prompts.ts
  • packages/widgets/src/input/TextInput.ts

Comment on lines +10 to +115
beforeEach(() => {
originalIsTTY = Object.getOwnPropertyDescriptor(process.stdin, 'isTTY');
originalSetRawMode = Object.getOwnPropertyDescriptor(process.stdin, 'setRawMode');

Object.defineProperty(process.stdin, 'isTTY', {
configurable: true,
get: () => true,
});

setRawModeSpy = vi.fn();
Object.defineProperty(process.stdin, 'setRawMode', {
configurable: true,
value: setRawModeSpy,
});

// Mock write to avoid writing TUI escape sequences to test output
vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
});

afterEach(() => {
vi.restoreAllMocks();
if (originalIsTTY) {
Object.defineProperty(process.stdin, 'isTTY', originalIsTTY);
} else {
delete (process.stdin as any).isTTY;
}
if (originalSetRawMode) {
Object.defineProperty(process.stdin, 'setRawMode', originalSetRawMode);
} else {
delete (process.stdin as any).setRawMode;
}
});

it('rejects select prompt with TermUIAbortError on signal abort', async () => {
const controller = new AbortController();
const sel = select({
options: ['a', 'b', 'c'],
signal: controller.signal,
});

const promise = prompt(sel);

// Abort the controller
controller.abort();

await expect(promise).rejects.toThrow(TermUIAbortError);
// Verify raw mode was cleaned up (called with false)
expect(setRawModeSpy).toHaveBeenCalledWith(false);
});

it('rejects textInput prompt with TermUIAbortError on signal abort', async () => {
const controller = new AbortController();
const input = textInput({
signal: controller.signal,
});

const promise = prompt(input);
controller.abort();

await expect(promise).rejects.toThrow(TermUIAbortError);
expect(setRawModeSpy).toHaveBeenCalledWith(false);
});

it('rejects numberInput prompt with TermUIAbortError on signal abort', async () => {
const controller = new AbortController();
const input = numberInput({
signal: controller.signal,
});

const promise = prompt(input);
controller.abort();

await expect(promise).rejects.toThrow(TermUIAbortError);
expect(setRawModeSpy).toHaveBeenCalledWith(false);
});

it('rejects form prompt with TermUIAbortError on signal abort', async () => {
const controller = new AbortController();
const f = form(
[
{ name: 'name', label: 'Name', type: 'text' },
],
{
signal: controller.signal,
}
);

const promise = prompt(f);
controller.abort();

await expect(promise).rejects.toThrow(TermUIAbortError);
expect(setRawModeSpy).toHaveBeenCalledWith(false);
});

it('rejects immediately if signal is already aborted', async () => {
const controller = new AbortController();
controller.abort();

const sel = select({
options: ['a', 'b', 'c'],
signal: controller.signal,
});

await expect(prompt(sel)).rejects.toThrow(TermUIAbortError);
});
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Use the real Screen test harness instead of stdio monkeypatch-only tests.

These tests currently patch process.stdin/stdout and never render through Screen.updateRect() + render(), which violates the repo test policy for packages/**/*.test.ts files.
As per coding guidelines, packages/**/*.test.{ts,tsx} tests must use the real Screen from @termuijs/core and render widgets via updateRect() and render().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/prompts-abort.test.ts` around lines 10 - 115, The abort
prompt tests rely on stdio monkeypatching instead of the real Screen harness, so
update the tests to render through Screen from `@termuijs/core` using updateRect()
and render() before exercising prompt() with select, textInput, numberInput, and
form. Remove the direct process.stdin/process.stdout mocking approach, create a
real Screen test setup, and assert the abort behavior through the rendered
widget flow while still verifying TermUIAbortError and cleanup.

Source: Coding guidelines

Comment on lines +130 to +173
export async function promptWidget<T = any /* Allow resolving any prompt value type */>(widget: Widget): Promise<T> {
if (!process.stdin.isTTY) throw new NonInteractiveError();

let inlineRows = 3;
if (widget instanceof Form) {
inlineRows = typeof widget.style?.height === 'number' ? widget.style.height : 5;
} else if (widget instanceof Select) {
inlineRows = 1 + (widget as any /* Bypass private options access */)._options.length;
} else if (widget instanceof NumberInput) {
inlineRows = typeof widget.style?.height === 'number' ? widget.style.height : 3;
} else if (widget instanceof TextInput) {
inlineRows = typeof widget.style?.height === 'number' ? widget.style.height : 3;
}

const app = new App(widget, {
screenMode: 'inline',
inlineRows,
skipFallback: true,
});

const signal = (widget as any /* Bypass private signal access */).signal;

return new Promise<T>((resolve, reject) => {
let completed = false;

const cleanup = () => {
if (completed) return;
completed = true;
if (signal) {
signal.removeEventListener('abort', onAbort);
}
app.unmount();
};

const onAbort = () => {
cleanup();
reject(new TermUIAbortError());
};

if (signal) {
if (signal.aborted) {
cleanup();
reject(new TermUIAbortError());
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Check pre-aborted signals before the TTY guard.

With the current order, an already-aborted prompt in a non-TTY context throws NonInteractiveError instead of the requested TermUIAbortError. Read widget.signal first and reject aborts before constructing App or checking process.stdin.isTTY.

Proposed fix
 export async function promptWidget<T = any /* Allow resolving any prompt value type */>(widget: Widget): Promise<T> {
+    const signal = (widget as any /* Bypass private signal access */).signal;
+    if (signal?.aborted) {
+        throw new TermUIAbortError();
+    }
     if (!process.stdin.isTTY) throw new NonInteractiveError();
@@
-    const signal = (widget as any /* Bypass private signal access */).signal;
-
     return new Promise<T>((resolve, reject) => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function promptWidget<T = any /* Allow resolving any prompt value type */>(widget: Widget): Promise<T> {
if (!process.stdin.isTTY) throw new NonInteractiveError();
let inlineRows = 3;
if (widget instanceof Form) {
inlineRows = typeof widget.style?.height === 'number' ? widget.style.height : 5;
} else if (widget instanceof Select) {
inlineRows = 1 + (widget as any /* Bypass private options access */)._options.length;
} else if (widget instanceof NumberInput) {
inlineRows = typeof widget.style?.height === 'number' ? widget.style.height : 3;
} else if (widget instanceof TextInput) {
inlineRows = typeof widget.style?.height === 'number' ? widget.style.height : 3;
}
const app = new App(widget, {
screenMode: 'inline',
inlineRows,
skipFallback: true,
});
const signal = (widget as any /* Bypass private signal access */).signal;
return new Promise<T>((resolve, reject) => {
let completed = false;
const cleanup = () => {
if (completed) return;
completed = true;
if (signal) {
signal.removeEventListener('abort', onAbort);
}
app.unmount();
};
const onAbort = () => {
cleanup();
reject(new TermUIAbortError());
};
if (signal) {
if (signal.aborted) {
cleanup();
reject(new TermUIAbortError());
return;
export async function promptWidget<T = any /* Allow resolving any prompt value type */>(widget: Widget): Promise<T> {
const signal = (widget as any /* Bypass private signal access */).signal;
if (signal?.aborted) {
throw new TermUIAbortError();
}
if (!process.stdin.isTTY) throw new NonInteractiveError();
let inlineRows = 3;
if (widget instanceof Form) {
inlineRows = typeof widget.style?.height === 'number' ? widget.style.height : 5;
} else if (widget instanceof Select) {
inlineRows = 1 + (widget as any /* Bypass private options access */)._options.length;
} else if (widget instanceof NumberInput) {
inlineRows = typeof widget.style?.height === 'number' ? widget.style.height : 3;
} else if (widget instanceof TextInput) {
inlineRows = typeof widget.style?.height === 'number' ? widget.style.height : 3;
}
const app = new App(widget, {
screenMode: 'inline',
inlineRows,
skipFallback: true,
});
return new Promise<T>((resolve, reject) => {
let completed = false;
const cleanup = () => {
if (completed) return;
completed = true;
if (signal) {
signal.removeEventListener('abort', onAbort);
}
app.unmount();
};
const onAbort = () => {
cleanup();
reject(new TermUIAbortError());
};
if (signal) {
if (signal.aborted) {
cleanup();
reject(new TermUIAbortError());
return;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/prompts.ts` around lines 130 - 173, In promptWidget, handle
an already-aborted widget.signal before the process.stdin.isTTY check so
pre-aborted prompts return TermUIAbortError instead of NonInteractiveError. Read
the signal early, reject immediately if signal.aborted, and only then continue
to the TTY guard and App construction; keep the existing onAbort cleanup path
consistent with this early-exit behavior.

Comment on lines +183 to +212
resolve(option.value as T);
};
} else if (widget instanceof TextInput) {
const originalOnSubmit = (widget as any /* Bypass private access */)._onSubmit;
(widget as any /* Bypass private access */)._onSubmit = (value: string) => {
originalOnSubmit?.(value);
cleanup();
resolve(value as unknown as T);
};
} else if (widget instanceof NumberInput) {
const originalOnSubmit = (widget as any /* Bypass private access */)._onSubmit;
(widget as any /* Bypass private access */)._onSubmit = (value: number | null) => {
originalOnSubmit?.(value);
cleanup();
resolve(value as unknown as T);
};
} else if (widget instanceof Form) {
const originalOnSubmit = (widget as any /* Bypass private access */)._onSubmit;
(widget as any /* Bypass private access */)._onSubmit = (values: Record<string, string>) => {
originalOnSubmit?.(values);
cleanup();
resolve(values as unknown as T);
};
} else {
const originalOnSubmit = (widget as any /* Bypass private access */)._onSubmit;
if (typeof originalOnSubmit === 'function') {
(widget as any /* Bypass private access */)._onSubmit = (value: any /* Custom widget onSubmit values */) => {
originalOnSubmit?.(value);
cleanup();
resolve(value as T);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify assertion sites in the changed prompt implementation.
rg -n 'as (unknown as )?T\b|as unknown as T' packages/ui/src/prompts.ts

Repository: Karanjot786/TermUI

Length of output: 406


🏁 Script executed:

#!/bin/bash
sed -n '170,220p' packages/ui/src/prompts.ts

Repository: Karanjot786/TermUI

Length of output: 2442


🏁 Script executed:

#!/bin/bash
sed -n '170,220p' packages/ui/src/prompts.ts

Repository: Karanjot786/TermUI

Length of output: 2442


Add inline rationale for the new as T casts. The resolve(...) calls in packages/ui/src/prompts.ts:183-212 need a short inline explanation for each as T / as unknown as T assertion, or should be refactored to avoid the casts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/prompts.ts` around lines 183 - 212, Add a brief inline
rationale for each type assertion in the prompt submission handlers, or refactor
the flow to avoid the assertions entirely. In the `prompt`/submit logic around
`TextInput`, `NumberInput`, `Form`, and the custom widget branch, annotate why
`resolve(...)` must use `as T` or `as unknown as T` so the type narrowing is
clear to future readers. Keep the explanation close to the `originalOnSubmit`
overrides and `resolve` calls in `packages/ui/src/prompts.ts`.

Source: Coding guidelines

Comment on lines +217 to +220
app.mount().catch((err) => {
cleanup();
reject(err);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Settle the prompt when App exits without widget completion.

app.mount() can resolve via App.exit()/SIGINT without invoking any _onSubmit/_onSelect wrapper, but this code only handles rejection. In that path the returned prompt promise remains pending even though the terminal app has unmounted.

Proposed fix
-        app.mount().catch((err) => {
+        app.mount().then((code) => {
+            if (!completed) {
+                cleanup();
+                reject(new TermUIAbortError(`Prompt exited with code ${code}`));
+            }
+        }).catch((err) => {
             cleanup();
             reject(err);
         });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
app.mount().catch((err) => {
cleanup();
reject(err);
});
app.mount().then((code) => {
if (!completed) {
cleanup();
reject(new TermUIAbortError(`Prompt exited with code ${code}`));
}
}).catch((err) => {
cleanup();
reject(err);
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/prompts.ts` around lines 217 - 220, The prompt promise is
only handled on `app.mount()` rejection, so it can stay pending when
`App.exit()` or SIGINT causes the app to unmount without `_onSubmit`/`_onSelect`
firing. Update the `app.mount()` flow in `prompts.ts` to also settle the outer
promise when mount completes normally due to exit, using the existing
`cleanup()` path and a clear resolve/reject decision tied to the prompt state.
Make sure the fix is applied around `app.mount()` and the prompt lifecycle
handlers so the returned promise always settles.

@Satvik-art-creator

Copy link
Copy Markdown
Contributor Author

@Karanjot786 Kindly review the pr. And let me know if any changes to made

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:ui @termuijs/ui area:widgets @termuijs/widgets type:feature +10 pts. New feature. type:testing +10 pts. Tests.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feature] Add AbortSignal support to all prompt components

1 participant