feat(ui): add AbortSignal support to prompts#1809
Conversation
📝 WalkthroughWalkthroughAdds optional ChangesAbortSignal Integration for Prompt Widgets
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 winWire keyboard handling before using
Selectas a prompt widget.
promptWidgetresolvesSelectonly through_onSelect, but this class has nohandleKey()orevents.on('key', ...)path to callconfirm().prompt(select(...))can hang during normal keyboard use; add a lowercase-key handler forenter,space,up,down, andescapeand subscribe it in the constructor. As per coding guidelines, “Interactive components must implementhandleKey(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 winSuppress submit callbacks after an abort during async validation.
signalis stored, butsubmit()never checks it afterawait Promise.all(validationPromises). If the prompt aborts while validation is pending, the promise rejects and unmounts, but_onSubmitcan 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
📒 Files selected for processing (7)
packages/ui/src/Form.tspackages/ui/src/NumberInput.tspackages/ui/src/Select.tspackages/ui/src/index.tspackages/ui/src/prompts-abort.test.tspackages/ui/src/prompts.tspackages/widgets/src/input/TextInput.ts
| 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); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
📐 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
| 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; |
There was a problem hiding this comment.
🎯 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.
| 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.
| 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); |
There was a problem hiding this comment.
📐 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.tsRepository: Karanjot786/TermUI
Length of output: 406
🏁 Script executed:
#!/bin/bash
sed -n '170,220p' packages/ui/src/prompts.tsRepository: Karanjot786/TermUI
Length of output: 2442
🏁 Script executed:
#!/bin/bash
sed -n '170,220p' packages/ui/src/prompts.tsRepository: 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
| app.mount().catch((err) => { | ||
| cleanup(); | ||
| reject(err); | ||
| }); |
There was a problem hiding this comment.
🩺 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.
| 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.
|
@Karanjot786 Kindly review the pr. And let me know if any changes to made |
Description
This PR adds support for programmatic cancellation to all interactive prompt components (
TextInput,Select,NumberInput, andForm) using the standardAbortController/AbortSignalAPI. When the signal aborts, the prompt unmounts the inline app, cleanly restores the terminal's raw mode configuration, and rejects the prompt promise withTermUIAbortError.Related Issue
Closes #64
Which package(s)?
@termuijs/ui,@termuijs/widgetsType of Change
type:bug)type:feature)type:docs)type:testing)type:refactor)type:design)type:accessibility)type:performance)type:devops)type:security)Checklist
needs-starcheck blocks your merge otherwise.bun vitest runbun run buildbun run typecheckCONTRIBUTING.md.type: short description.markDirty()(if your change affects rendering).anytypes without an inline comment explaining why.GSSoC 2026 Participation
https://gssoc.girlscript.org/profile/172e996c-3943-4a86-8249-1f01922d0f64Screenshots / Recordings (UI changes)
Notes for the Reviewer
The implementation ensures that:
Summary by CodeRabbit
New Features
Bug Fixes