Skip to content

fix: resolve 4 bugs in termui - #3502

Closed
saurabhhhcodes wants to merge 1 commit into
Karanjot786:mainfrom
saurabhhhcodes:fix/termui-31999
Closed

fix: resolve 4 bugs in termui#3502
saurabhhhcodes wants to merge 1 commit into
Karanjot786:mainfrom
saurabhhhcodes:fix/termui-31999

Conversation

@saurabhhhcodes

@saurabhhhcodes saurabhhhcodes commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Description

This PR fixes real bugs found in the codebase:

  • Removed redundant boolean comparison: x === true is equivalent to x (and x === false to !x), and shorter to read.
  • Added rejection handler to Promise.all: an unhandled rejection in any input promise previously crashed silently.
  • Added explicit radix to parseInt: without 10, strings like '0x1F' or '08' parse in unintended bases.
  • Fixed default sort: .sort() coerces elements to strings, so [10, 9, 2] sorts as [10, 2, 9]; numeric comparator sorts correctly.

Type of Change

  • Bug fix (non-breaking change fixing an issue)

How Has This Been Tested?

  • Local manual testing

Checklist

  • My code follows the style guidelines
  • I have performed a self-review

Related Issue

Ref: #3501

Summary by CodeRabbit

  • Bug Fixes
    • Improved RSS feed handling for hexadecimal HTML entities.
    • Multi-select values now appear in consistent numeric order.
    • Improved diagnostics when form operations fail.
    • Updated form keyboard shortcut handling for clearing fields.

@github-actions github-actions Bot added area:examples Example apps. area:ui @termuijs/ui type:bug +10 pts. Bug fix. labels Aug 4, 2026
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The changes update four independent code paths: a form keyboard condition, RSS hexadecimal entity parsing, form promise rejection logging, and multi-select option ordering.

Changes

Form validation example

Layer / File(s) Summary
Clear-form keyboard condition
examples/forms-and-validation/src/index.tsx
The clear-form condition now uses event.ctrl ! instead of event.ctrl === false.

RSS entity parsing

Layer / File(s) Summary
Bounded hexadecimal parsing
examples/rss-reader/src/index.tsx
decodeEntities limits the hexadecimal substring passed to Number.parseInt() to eight characters.

Form rejection logging

Layer / File(s) Summary
Promise rejection handler
packages/ui/src/Form.ts
A .catch handler logs rejected promise errors with console.error.

Multi-select ordering

Layer / File(s) Summary
Numeric option sorting
packages/ui/src/MultiSelect.ts
selectedOptions sorts checked indexes with a numeric comparator.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested reviewers: karanjot786

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title follows the required format and clearly summarizes the four bug fixes.
Description check ✅ Passed The description explains the bug fixes, change type, testing, checklist, and related issue; some template sections are omitted.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Biome (2.5.5)
packages/ui/src/Form.ts

File contains syntax errors that prevent linting: Line 142: Expected a statement but instead found '.catch(err => console.error("Promise.all failed:", err))'.


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.

@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: 3

🧹 Nitpick comments (1)
packages/ui/src/MultiSelect.ts (1)

33-33: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a regression test for multi-digit indexes.

Line [33] correctly applies numeric sorting. The existing test at packages/ui/src/MultiSelect.test.ts:126-133 selects indexes 0 and 2, so the old lexicographic sort also passes. Add a case with at least 11 options and selected indexes 2 and 10.

🤖 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/MultiSelect.ts` at line 33, Add a regression case in the
MultiSelect tests covering at least 11 options with indexes 2 and 10 selected,
and assert the returned options remain in numeric index order. Keep the existing
coverage unchanged and target the behavior exposed by MultiSelect’s
checked-options sorting.
🤖 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 `@examples/forms-and-validation/src/index.tsx`:
- Line 125: In the keyboard handler condition, replace the invalid `event.ctrl
!` expression with the valid negated boolean check `!event.ctrl`, preserving the
existing Ctrl+C quit behavior and handling plain `c` as intended.

In `@examples/rss-reader/src/index.tsx`:
- Around line 30-31: Update the entity-decoding logic around codePoint to parse
the full hexadecimal suffix rather than truncating it, return match when the
suffix exceeds eight hexadecimal digits, and only call String.fromCodePoint for
finite values at or below 0x10FFFF; otherwise preserve the original match.

In `@packages/ui/src/Form.ts`:
- Around line 141-142: Move the trailing rejection handler into the Form class’s
submit() method by wrapping await Promise.all(validationPromises) in
try/catch/finally; keep _isValidating = false and markDirty() in finally so
rejected validators reset validation state, and remove the out-of-class .catch
that breaks parsing.

---

Nitpick comments:
In `@packages/ui/src/MultiSelect.ts`:
- Line 33: Add a regression case in the MultiSelect tests covering at least 11
options with indexes 2 and 10 selected, and assert the returned options remain
in numeric index order. Keep the existing coverage unchanged and target the
behavior exposed by MultiSelect’s checked-options sorting.
🪄 Autofix

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: 396372fe-e3f7-4e2c-9ad4-622f9e1ea257

📥 Commits

Reviewing files that changed from the base of the PR and between 6c7584e and 8aaf8d4.

📒 Files selected for processing (4)
  • examples/forms-and-validation/src/index.tsx
  • examples/rss-reader/src/index.tsx
  • packages/ui/src/Form.ts
  • packages/ui/src/MultiSelect.ts

}

if (event.key === 'c' && event.ctrl === false) {
if (event.key === 'c' && event.ctrl !) {

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 | 🔴 Critical | ⚡ Quick win

Replace the invalid condition with a valid boolean check.

event.ctrl ! is invalid TypeScript syntax. The example will fail to parse and cannot build or run. Use !event.ctrl to handle plain c without changing the existing Ctrl+C quit behavior.

Proposed fix
-        if (event.key === 'c' && event.ctrl !) {
+        if (event.key === 'c' && !event.ctrl) {
📝 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
if (event.key === 'c' && event.ctrl !) {
if (event.key === 'c' && !event.ctrl) {
🤖 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 `@examples/forms-and-validation/src/index.tsx` at line 125, In the keyboard
handler condition, replace the invalid `event.ctrl !` expression with the valid
negated boolean check `!event.ctrl`, preserving the existing Ctrl+C quit
behavior and handling plain `c` as intended.

Comment on lines +30 to 31
const codePoint = Number.parseInt(entity.slice(2, 10), 16);
return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : match;

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

node -e '
try {
  String.fromCodePoint(0x110000);
  process.exit(1);
} catch (error) {
  if (!(error instanceof RangeError)) process.exit(2);
}
'

rg -n -C 4 'decodeEntities|String\.fromCodePoint|&`#x`[0-9A-Fa-f]' \
  examples/rss-reader/src/index.tsx

Repository: Karanjot786/TermUI

Length of output: 1658


🏁 Script executed:

#!/bin/bash
set -euo pipefail

node - <<'JS'
const tests = [
  ['&`#x110000`;', false],
  ['&`#x123456789`;', false],
];

for (const [input, shouldThrow] of tests) {
  const entity = input.slice(1, -1);
  const slice = entity.slice(2, 10);
  const codePoint = Number.parseInt(slice, 16);
  let threw = false;
  let value;
  try {
    value = String.fromCodePoint(codePoint);
  } catch (error) {
    threw = true;
  }
  console.log(JSON.stringify({
    input,
    sliced: slice,
    codePoint: codePoint.toString(16),
    decoded: value,
    threw,
    finite: Number.isFinite(codePoint)
  }));
}
JS

Repository: Karanjot786/TermUI

Length of output: 341


Reject oversized hexadecimal code points before decoding.

entity.slice(2, 10) truncates longer hexadecimal entities, so &#x123456789; decodes &#x12345678; and loses the final digit. The same path also lets values above 0x10FFFF, such as &#x110000;, pass Number.isFinite and throw RangeError from String.fromCodePoint. Use the full hexadecimal suffix, return match when it is longer than eight digits, and reject values greater than 0x10FFFF.

Proposed fix
-      const codePoint = Number.parseInt(entity.slice(2, 10), 16);
-      return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : match;
+      const hex = entity.slice(2);
+      if (hex.length > 8) return match;
+      const codePoint = Number.parseInt(hex, 16);
+      return codePoint <= 0x10FFFF ? String.fromCodePoint(codePoint) : match;
📝 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
const codePoint = Number.parseInt(entity.slice(2, 10), 16);
return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : match;
const hex = entity.slice(2);
if (hex.length > 8) return match;
const codePoint = Number.parseInt(hex, 16);
return codePoint <= 0x10FFFF ? String.fromCodePoint(codePoint) : match;
🤖 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 `@examples/rss-reader/src/index.tsx` around lines 30 - 31, Update the
entity-decoding logic around codePoint to parse the full hexadecimal suffix
rather than truncating it, return match when the suffix exceeds eight
hexadecimal digits, and only call String.fromCodePoint for finite values at or
below 0x10FFFF; otherwise preserve the original match.

Comment thread packages/ui/src/Form.ts
Comment on lines +141 to +142

.catch(err => console.error("Promise.all failed:", err)); No newline at end of file

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 | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

npx biome check packages/ui/src/Form.ts

Repository: Karanjot786/TermUI

Length of output: 2385


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Form.ts outline =="
ast-grep outline packages/ui/src/Form.ts || true

echo "== Relevant lines around submit/catch =="
nl -ba packages/ui/src/Form.ts | sed -n '70,105p;132,148p'

echo "== parse/typecheck with local tsc if available =="
if command -v tsc >/dev/null 2>&1; then
  tsc --version
  tsc --noEmit packages/ui/src/Form.ts 2>&1 || true
else
  echo "tsc not available"
fi

echo "== package scripts and devDependency config =="
sed -n '1,220p' package.json

Repository: Karanjot786/TermUI

Length of output: 863


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Relevant lines around submit/catch =="
awk 'NR>=70 && NR<=105 {printf "%6d\t%s\n", NR, $0} NR>=132 && NR<=148 {printf "%6d\t%s\n", NR, $0}' packages/ui/src/Form.ts

echo "== package scripts and devDependency config =="
awk '{printf "%6d\t%s\n", NR, $0}' package.json | sed -n '1,220p'

echo "== TypeScript/biome availability and parse probe =="
for cmd in npm npx tsc biome pnpm yarn; do
  command -v "$cmd" >/dev/null 2>&1 && printf '%s=%s\n' "$cmd" "$(command -v "$cmd")"
done

if command -v node >/dev/null 2>&1; then
  node - <<'JS'
const fs = require('fs');
const src = fs.readFileSync('packages/ui/src/Form.ts', 'utf8');
console.log(JSON.stringify({
  submitOpenMethods: src.includes('async submit('),
  catchFoundOutsideMethodLikePattern: /\.catch\(err => console\.error\("Promise\.all failed:", err\)\)/.test(src),
  fileLength: src.length
}, null, 2));
try {
  require('typescript').createSourceFile('Form.ts', src, require('typescript').ScriptTarget.Latest, true);
  console.log({typescript_parse: "ok"});
} catch (e) {
  console.log({typescript_parse_error: String(e.message)});
}
try {
  new Function(src);
  console.log({js_parse: "ok"});
} catch (e) {
  console.log({js_parse_error: String(e.message)});
}
JS
fi

Repository: Karanjot786/TermUI

Length of output: 4231


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Full package/tsconfig references =="
git ls-files | rg '(^|/)(tsconfig.*json|package.json)$|packages/ui/(package.json|tsconfig.*json)$'
for f in package.json packages/ui/package.json $(git ls-files | rg '(^|/)tsconfig.*json$'); do
  echo "--- $f"
  sed -n '1,220p' "$f"
done

echo "== Parse check with installed TypeScript =="
node - <<'JS'
const path = require('node:path');
const tsPath = path.join(process.cwd(), 'node_modules', 'typescript');
const ts = require(tsPath);

const src = require('node:fs').readFileSync('packages/ui/src/Form.ts', 'utf8');
const fileName = 'packages/ui/src/Form.ts';
const sourceFile = ts.createSourceFile(fileName, src, ts.ScriptTarget.Latest, true);

sourceFile.forEachChild(node => {
  const text = (n) => ts.SyntaxKind[n.kind];
});

function walk(node, indent = 0) {
  console.log(`${' '.repeat(indent)}${ts.SyntaxKind[node.kind]} ${node.getStart()}-${node.getEnd()}`);
  ts.forEachChild(node, n => walk(n, indent + 2));
}

walk(sourceFile);

const diagnostics = ts.getPreEmitDiagnostics(sourceFile, fs ? fs : undefined);
for (const diag of diagnostics) {
  const message = ts.flattenDiagnosticMessageText(diag.messageText, '\n');
  const loc = diag.file
    ? `${diag.file.fileName} ${diag.file.getLineAndCharacterOfPosition(diag.start || 0).line + 1}`
    : 'global';
  console.log(`${loc}: ${diag.code}: ${message}`);
}

try {
  new Function(src);
  console.log({js_parse: "ok"});
} catch (e) {
  console.log({js_parse_error: String(e.message)});
}
JS

Repository: Karanjot786/TermUI

Length of output: 23040


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== File length and trailing catch location =="
wc -l packages/ui/src/Form.ts
awk 'NR>=96 && NR<=102 || NR>=139 && NR<=144 {printf "%d: %s\n", NR, $0}' packages/ui/src/Form.ts

echo "== Installed TypeScript compiler location and parse run =="
node - <<'JS'
const { exec } = require('node:child_process');
const src = require('node:fs').readFileSync('packages/ui/src/Form.ts', 'utf8');
const file = require('node:fs').writeFileSync('/tmp/Form-outer-catch.ts', src, 'utf8');
exec('node ./node_modules/typescript/lib/tsc.js --noEmit --target ES2022 --module ESNext /tmp/Form-outer-catch.ts', (err, stdout, stderr) => {
  console.log('typescript_path=', require.resolve('typescript', { paths: [process.cwd()] }));
  console.log('exit=', err ? err.code : 0);
  console.log('stdout=', stdout);
  console.log('stderr=', stderr);
});
JS

Repository: Karanjot786/TermUI

Length of output: 1423


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Run installed global tsc =="
tsc --version
tsc --noEmit --target ES2022 --module ESNext packages/ui/src/Form.ts 2>&1
echo "== Run installed global TypeScript parser =="
node "$TSC_NODE_OPTIONS" "$(which node)" -e '
const path = require("node:path");
const ts = require(path.dirname(process.argv[2]) + "/typescript/lib/typescript.js");
const src = require("node:fs").readFileSync("packages/ui/src/Form.ts", "utf8");
const sourceFile = ts.createSourceFile("Form.ts", src, ts.ScriptTarget.Latest, true);
const diagnostics = ts.getPreEmitDiagnostics(sourceFile);
for (const diag of diagnostics) {
  const pos = diag.file && diag.start !== undefined ? diag.file.getLineAndCharacterOfPosition(diag.start) : null;
  console.log((diag.file || global) + ":" + (pos && pos.line + 1 || "") + ":" + (pos && pos.character + 1 || "") + " TS" + diag.code + ": " + diag.messageText);
}
'

Repository: Karanjot786/TermUI

Length of output: 466


🏁 Script executed:

#!/bin/bash
set -euo pipefail

Repository: Karanjot786/TermUI

Length of output: 156


Move the rejection handler inside submit().

Line 142 appends .catch(...) after the Form class ends, which makes packages/ui/src/Form.ts fail to parse and fail npm run typecheck. Wrap the await Promise.all(validationPromises) call in try/catch/finally, and keep _isValidating = false and markDirty() in finally so a rejected validator still resets validation state.

🧰 Tools
🪛 Biome (2.5.5)

[error] 142-142: Expected a statement but instead found '.catch(err => console.error("Promise.all failed:", err))'.

(parse)

🤖 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 141 - 142, Move the trailing rejection
handler into the Form class’s submit() method by wrapping await
Promise.all(validationPromises) in try/catch/finally; keep _isValidating = false
and markDirty() in finally so rejected validators reset validation state, and
remove the out-of-class .catch that breaks parsing.

Source: Linters/SAST tools

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

Labels

area:examples Example apps. area:ui @termuijs/ui type:bug +10 pts. Bug fix.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant