Skip to content

better TUI perf & better result everywhere#179

Merged
bmdavis419 merged 5 commits intomainfrom
davis/react-tui-experiment
Feb 13, 2026
Merged

better TUI perf & better result everywhere#179
bmdavis419 merged 5 commits intomainfrom
davis/react-tui-experiment

Conversation

@bmdavis419
Copy link
Collaborator

@bmdavis419 bmdavis419 commented Feb 11, 2026

Greptile Overview

Greptile Summary

This PR migrates the CLI TUI from Solid.js to React and adopts the better-result pattern across web APIs.

Major Changes:

  • Complete TUI framework migration from @opentui/solid to @opentui/react with React 19
  • Added React Compiler (Babel plugin) for automatic memoization and performance optimization
  • Adopted better-result pattern in Convex actions/queries and API routes with typed error classes
  • Fixed previous cursor position bugs in repo-mention and command palettes by adding bounds checking
  • New focus-registry.ts for global focus management and opentui-hooks.ts for React event wrappers

Critical Issue:

  • input-section.tsx:193 has cursor position calculation bug that will break history navigation on multi-line wrapped input

Plan Files:

  • .codex/commands/adopt-better-result.md - Command documentation for better-result adoption
  • .codex/skills/better-result-adopt/SKILL.md - Skill definition for migration workflow
  • apps/web/better-result-audit.md - Tracks which modules have adopted the pattern

The better-result refactoring follows the documented pattern correctly, maintaining boundary-safe error handling while preserving backward compatibility.

Confidence Score: 3/5

  • Has one critical cursor position bug that will break history navigation on wrapped input
  • The TUI migration from Solid to React is comprehensive and mostly correct, with previous palette bugs fixed. However, the cursor position calculation bug in input-section.tsx (line 193) will cause incorrect cursor placement when navigating to history entries that wrap to multiple lines. The better-result adoption appears sound with proper error typing and boundary handling.
  • apps/cli/src/tui/components/input-section.tsx - cursor calculation bug on line 193

Important Files Changed

Filename Overview
apps/cli/src/tui/components/input-section.tsx Migrated from Solid to React; has cursor position calculation bug in history navigation (line 193)
apps/cli/src/tui/components/main-input.tsx Migrated from Solid to React; correctly implements cursor position as row*width+col for multi-line wrapping
apps/cli/src/tui/App.tsx Migrated from Solid to React; renderer initialization refactored, mouse input handling added
apps/web/src/lib/result/http.ts New utilities for Result-based HTTP handling, Convex action wrapping, and response helpers
apps/web/src/convex/authHelpers.ts Adds Result-based auth helpers alongside existing throwing versions for backward compatibility
apps/web/src/convex/usage.ts Extensive better-result adoption for Autumn billing API calls with typed error handling

Last reviewed commit: cd12e24

@bmdavis419 bmdavis419 changed the title tis working react tui... NO MERGE: testing react tui Feb 11, 2026
Copy link
Collaborator Author

bmdavis419 commented Feb 11, 2026

@bmdavis419 bmdavis419 marked this pull request as ready for review February 11, 2026 10:02
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

43 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +20 to +23
const filteredCommands = useMemo(() => {
if (!input) return COMMANDS;
return filterCommands(trimmedInput);
});
}, [input]);
Copy link
Contributor

Choose a reason for hiding this comment

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

Out-of-bounds selection index
selectedIndex is never reset/clamped when filteredCommands changes, but tab/return read filteredCommands[selectedIndex] (lines 40-55). If a user navigates down, then types more characters so the filtered list shrinks, selectedIndex can point past the end and selection becomes a no-op. Consider clamping/resetting selectedIndex whenever filteredCommands.length changes.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/cli/src/tui/components/command-palette.tsx
Line: 20:23

Comment:
**Out-of-bounds selection index**
`selectedIndex` is never reset/clamped when `filteredCommands` changes, but `tab`/`return` read `filteredCommands[selectedIndex]` (lines 40-55). If a user navigates down, then types more characters so the filtered list shrinks, `selectedIndex` can point past the end and selection becomes a no-op. Consider clamping/resetting `selectedIndex` whenever `filteredCommands.length` changes.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 63 to +66
const selectRepo = () => {
const selectedRepo = filteredRepos()[selectedIndex()];
if (selectedRepo) {
const idx = curInputIdx();
const currentState = props.inputState;
const newContent = '@' + selectedRepo.name + ' ';
const newState = [
...currentState.slice(0, idx),
{ content: newContent, type: 'mention' as const },
...currentState.slice(idx + 1)
];
props.setInputState(newState);
const inputRef = props.inputRef;
if (inputRef) {
let newCursorPos = 0;
for (let i = 0; i <= idx; i++) {
newCursorPos += i === idx ? newContent.length : getDisplayLength(currentState[i]!);
}
// Calculate the new text and update textarea
const newText = newState
.map((p) => (p.type === 'pasted' ? `[~${p.lines} lines]` : p.content))
.join('');
inputRef.setText(newText);
inputRef.editBuffer.setCursor(0, newCursorPos);
}
const selectedRepo = filteredRepos[selectedIndex];
if (!selectedRepo) return;

Copy link
Contributor

Choose a reason for hiding this comment

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

Selection can become invalid
selectRepo() reads filteredRepos[selectedIndex], but selectedIndex is never clamped/reset when filteredRepos changes. If the user moves the selection and then types to narrow results, selectedIndex can go out of range and selectRepo() will early-return, making Tab/Enter appear broken.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/cli/src/tui/components/repo-mention-palette.tsx
Line: 63:66

Comment:
**Selection can become invalid**
`selectRepo()` reads `filteredRepos[selectedIndex]`, but `selectedIndex` is never clamped/reset when `filteredRepos` changes. If the user moves the selection and then types to narrow results, `selectedIndex` can go out of range and `selectRepo()` will early-return, making Tab/Enter appear broken.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 327 to 329
onCursorChange={(e) => {
props.setCursorPosition(e.visualColumn);
}}
Copy link
Contributor

Choose a reason for hiding this comment

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

Cursor position units mismatch
InputSection treats cursorPosition as a linear offset across the full composed input, but MainInput sets it to e.visualColumn in onCursorChange (line 328), which is per-line. Other code paths set it to logicalCursor.col orrow * availableWidth + col(paste path at lines 136-138). Once the input wraps to multiple lines, mention/command detection and palettes that depend oncursorPosition` will behave incorrectly unless a single consistent coordinate system is used.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/cli/src/tui/components/main-input.tsx
Line: 327:329

Comment:
**Cursor position units mismatch**
`InputSection` treats `cursorPosition` as a linear offset across the full composed input, but `MainInput` sets it to `e.visualColumn` in `onCursorChange` (line 328), which is per-line. Other code paths set it to `logicalCursor.col` orrow * availableWidth + col` (paste path at lines 136-138). Once the input wraps to multiple lines, mention/command detection and palettes that depend on `cursorPosition` will behave incorrectly unless a single consistent coordinate system is used.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 44 to 50
const filteredRepos = useMemo(() => {
const repos = config.repos;
const curInput = props.inputState[curInputIdx()]?.content;
if (!curInput) return repos;
const trimmedInput = curInput.toLowerCase().trim().slice(1);
return repos.filter((repo) => repo.name.toLowerCase().includes(trimmedInput));
});
}, [config.repos, props.inputState, props.cursorPosition, props.inputRef]);
Copy link
Contributor

Choose a reason for hiding this comment

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

Stale filtered repo list
filteredRepos is memoized but its computation calls curInputIdx() which reads props.inputRef.logicalCursor (lines 25-41). The memo deps include props.cursorPosition, but curInputIdx() doesn't use it; if the cursor moves without changing inputState or the inputRef identity, filteredRepos can stay stale and show the wrong suggestions.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/cli/src/tui/components/repo-mention-palette.tsx
Line: 44:50

Comment:
**Stale filtered repo list**
`filteredRepos` is memoized but its computation calls `curInputIdx()` which reads `props.inputRef.logicalCursor` (lines 25-41). The memo deps include `props.cursorPosition`, but `curInputIdx()` doesn't use it; if the cursor moves without changing `inputState` or the `inputRef` identity, `filteredRepos` can stay stale and show the wrong suggestions.

How can I resolve this? If you propose a fix, please make it concise.

@bmdavis419 bmdavis419 force-pushed the davis/react-tui-experiment branch from 58cf892 to f34426a Compare February 12, 2026 23:20
@bmdavis419 bmdavis419 changed the title NO MERGE: testing react tui better TUI perf & better result everywhere Feb 12, 2026
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

77 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

if (!inputRef) return;
inputRef.gotoBufferEnd();
const cursor = inputRef.logicalCursor;
setCursorPosition(cursor.col);
Copy link
Contributor

Choose a reason for hiding this comment

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

Cursor position calculation inconsistency breaks multi-line input history navigation. Uses cursor.col but should use cursor.row * availableWidth + cursor.col to match main-input.tsx:48,255. When navigating to history entries that wrap, cursor position will be wrong.

Suggested change
setCursorPosition(cursor.col);
const availableWidth = Math.max(1, terminalDimensions.width - 4);
setCursorPosition(cursor.row * availableWidth + cursor.col);
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/cli/src/tui/components/input-section.tsx
Line: 193:193

Comment:
Cursor position calculation inconsistency breaks multi-line input history navigation. Uses `cursor.col` but should use `cursor.row * availableWidth + cursor.col` to match main-input.tsx:48,255. When navigating to history entries that wrap, cursor position will be wrong.

```suggestion
			const availableWidth = Math.max(1, terminalDimensions.width - 4);
			setCursorPosition(cursor.row * availableWidth + cursor.col);
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Collaborator Author

bmdavis419 commented Feb 13, 2026

Merge activity

  • Feb 13, 5:09 AM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Feb 13, 5:09 AM UTC: @bmdavis419 merged this pull request with Graphite.

@bmdavis419 bmdavis419 merged commit 5bec05e into main Feb 13, 2026
3 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant