feat: Add sort direction modal for improved UX#26
Conversation
- Created new SortDirectionModal component with visual feedback - Shows current sorting key and describes what each direction means - Replaces direct toggle with 'D' key to open modal - Provides keyboard shortcuts (A/D) and arrow key navigation - Updates README to reflect the new modal-based interaction - Consistent with existing sort modal pattern for better user experience
WalkthroughIntroduces a new SortDirectionModal component and integrates it into RepoList to handle sort direction selection via a modal instead of a toggle. Updates README to reflect the modal-based interaction and adds a re-export in the modals index. Keyboard handling updated to open and manage the modal and persist selection. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant RepoList
participant SortDirectionModal as SortDirectionModal
participant UIPrefs as UI Prefs Store
User->>RepoList: Press "D"
RepoList->>RepoList: Set sortDirectionMode = true
RepoList->>SortDirectionModal: Render with currentDirection, currentSortKey
User->>SortDirectionModal: Navigate (Up/Down/Tab/A/D)
User->>SortDirectionModal: Enter (confirm) / Esc (cancel)
alt Confirm direction
SortDirectionModal-->>RepoList: onSelect(direction)
RepoList->>RepoList: Update sortDir, reset cursor, close modal
RepoList->>UIPrefs: storeUIPrefs({ sortDir })
else Cancel
SortDirectionModal-->>RepoList: onCancel()
RepoList->>RepoList: Close modal (no changes)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
✨ Finishing Touches
🧪 Generate unit tests
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (7)
README.md (2)
76-76: Tighten wording to name the keys explicitlySpelling out the keys here improves scannability and mirrors the controls section.
-- **Interactive Sorting**: Modal-based sort selection (updated, pushed, name, stars) with modal-based direction selection +- **Interactive Sorting**: Modal-based sort selection (S) and direction selection (D) — options: updated, pushed, name, stars
267-267: Document in‑modal shortcuts for consistency with the new UXMention A/D and Esc/C so the README matches the modal help line and PR description.
-- **Sort Direction**: `D` to open sort direction modal (ascending/descending) +- **Sort Direction**: `D` to open sort direction modal (ascending/descending). In the modal: `A` = Ascending, `D` = Descending, `Esc`/`C` = Cancel.src/ui/components/modals/SortDirectionModal.tsx (2)
172-176: Expose all relevant shortcuts in the hintThe modal supports Left/Right and “C to cancel”; reflect that in the footer hint.
- <Text color="gray" dimColor> - ↑↓/Enter • A/D • Esc - </Text> + <Text color="gray" dimColor> + ←→/↑↓/Enter • A/D • Esc/C + </Text>
136-136: Prefer chalk over Ink colour props (project guideline)You’re already using chalk for most styling; consider switching these remaining
color/dimColorusages to chalk for consistency and to avoid nested Text colour issues.Example:
- <Text color="gray" dimColor>Sorting by: {formatSortKey()}</Text> + <Text>{chalk.gray.dim(`Sorting by: ${formatSortKey()}`)}</Text> @@ - <Text color="gray" dimColor> - {' '}{getButtonDescription(option)} - </Text> + <Text>{chalk.gray.dim(` ${getButtonDescription(option)}`)}</Text> @@ - <Text color="gray" dimColor> - ←→/↑↓/Enter • A/D • Esc/C - </Text> + <Text>{chalk.gray.dim('←→/↑↓/Enter • A/D • Esc/C')}</Text>Also applies to: 152-154, 172-175
src/ui/components/modals/index.ts (1)
8-8: Optionally re‑export the SortDirection typeThis makes it easier for callers (e.g., RepoList) to import both the component and its type from one place.
export { default as SortDirectionModal } from './SortDirectionModal'; +export type { SortDirection } from './SortDirectionModal';src/ui/views/RepoList.tsx (2)
1492-1492: Include orgSwitcherOpen in modalOpen for consistent dimmingMinor consistency nit: when the organisation switcher is open, the header/container isn’t dimmed like other modals. Consider including orgSwitcherOpen.
- const modalOpen = deleteMode || archiveMode || syncMode || logoutMode || infoMode || visibilityMode || sortMode || sortDirectionMode || changeVisibilityMode || copyUrlMode || renameMode; + const modalOpen = deleteMode || archiveMode || syncMode || logoutMode || infoMode || visibilityMode || sortMode || sortDirectionMode || changeVisibilityMode || copyUrlMode || renameMode || orgSwitcherOpen;
2016-2030: Avoid resetting cursor and persisting when direction is unchangedSelecting the already-active direction currently resets the cursor and writes prefs, causing unnecessary UI updates. Guard on change and close the modal first for snappier UX.
- onSelect={(direction) => { - setSortDir(direction); - setSortDirectionMode(false); - setCursor(0); // Reset cursor when direction changes - storeUIPrefs({ sortDir: direction }); - // Will trigger refresh via useEffect - }} + onSelect={(direction) => { + setSortDirectionMode(false); + if (direction !== sortDir) { + setSortDir(direction); + setCursor(0); // Reset cursor only when direction actually changes + storeUIPrefs({ sortDir: direction }); + // Will trigger refresh via useEffect + } + }}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (4)
README.md(2 hunks)src/ui/components/modals/SortDirectionModal.tsx(1 hunks)src/ui/components/modals/index.ts(1 hunks)src/ui/views/RepoList.tsx(6 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use strict TypeScript with comprehensive types; avoid any without clear justification
Files:
src/ui/components/modals/index.tssrc/ui/components/modals/SortDirectionModal.tsxsrc/ui/views/RepoList.tsx
src/ui/**/*.tsx
📄 CodeRabbit inference engine (AGENTS.md)
src/ui/**/*.tsx: Implement React/Ink UI as functional components using hooks
Use chalk for colours instead of Ink colour props to avoid nested Text issues
Pre-colour strings and render within a single to prevent nested Text rendering issues
Use spacers for consistent terminal spacing
Files:
src/ui/components/modals/SortDirectionModal.tsxsrc/ui/views/RepoList.tsx
src/**/*.tsx
📄 CodeRabbit inference engine (AGENTS.md)
Use British English for all user-facing text (e.g., organisation, authorisation, colour)
Files:
src/ui/components/modals/SortDirectionModal.tsxsrc/ui/views/RepoList.tsx
🧠 Learnings (1)
📚 Learning: 2025-09-05T11:52:17.573Z
Learnt from: CR
PR: wiiiimm/gh-manager-cli#0
File: AGENTS.md:0-0
Timestamp: 2025-09-05T11:52:17.573Z
Learning: Applies to src/github.ts : Select fields: name, nameWithOwner, description, visibility, isPrivate, isFork, isArchived, stargazerCount, forkCount, primaryLanguage, updatedAt, pushedAt, diskUsage
Applied to files:
README.md
🧬 Code graph analysis (4)
src/ui/components/modals/index.ts (2)
src/ui/components/modals/SortModal.tsx (8)
SortModal(13-172)SortModalProps(7-11)sort(116-123)sort(107-114)input(32-105)option(138-150)currentIndex(24-30)sort(125-130)src/ui/components/modals/VisibilityModal.tsx (1)
VisibilityModal(14-163)
src/ui/components/modals/SortDirectionModal.tsx (2)
src/ui/components/modals/SortModal.tsx (6)
SortModal(13-172)sort(116-123)SortModalProps(7-11)sort(107-114)option(138-150)input(32-105)src/ui/components/modals/VisibilityModal.tsx (1)
VisibilityModal(14-163)
src/ui/views/RepoList.tsx (2)
src/ui/components/modals/SortDirectionModal.tsx (1)
SortDirectionModal(14-179)src/config/config.ts (1)
storeUIPrefs(96-100)
README.md (2)
src/ui/components/modals/SortModal.tsx (6)
sort(116-123)SortModal(13-172)sort(107-114)SortModalProps(7-11)currentIndex(24-30)input(32-105)src/ui/views/RepoList.main.tsx (1)
prev(944-948)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Cursor Bugbot
🔇 Additional comments (5)
src/ui/components/modals/index.ts (1)
8-8: LGTM — barrel re‑export addedRe‑exporting the modal here keeps imports consistent with other modals.
src/ui/views/RepoList.tsx (4)
12-12: Import of SortDirectionModal — LGTMBarrel import looks correct and consistent with existing modal imports.
158-158: New sortDirectionMode state — LGTMState flag integrates cleanly with existing modal flags.
1084-1087: Input correctly trapped while SortDirectionModal is openPrevents parent handlers from interfering. Matches the pattern used for other modals.
1306-1307: D key opens SortDirectionModal — LGTMBehaviour aligns with the updated UX and help footer.
- Import SortKey type from SortModal - Update currentSortKey prop to use SortKey type instead of string - Remove unnecessary default case in formatSortKey switch statement - Ensures type safety and only allows valid sort keys
|
🎉 This PR is included in version 1.32.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
src/ui/components/modals/SortDirectionModal.tsx (1)
15-20: Props and interactions look solidClear, typed props and keyboard flows mirror SortModal. Nice reuse of the SortKey union.
🧹 Nitpick comments (5)
src/ui/components/modals/SortDirectionModal.tsx (5)
4-4: Use a type-only import for SortKeyPrevents an unnecessary runtime import and aligns with strict TS hygiene.
-import { SortKey } from './SortModal'; +import type { SortKey } from './SortModal';
135-135: Optional: prefer chalk.bold for consistencyKeeps all styling in chalk for consistency with the colour guideline.
- <Text bold>Sort Direction</Text> + <Text>{chalk.bold('Sort Direction')}</Text>
59-68: Tab key handling: add a literal-tab fallbackInk versions can differ on
key.tab. Includinginput === '\t'improves portability without changing behaviour.- if (key.tab) { + if (key.tab || input === '\t') {
96-114: Exhaustiveness over defaults for SortKey switchesTo surface future SortKey additions at compile-time, prefer exhaustive switches with an
assertNeverhelper rather thandefaultfallbacks.// helper (top-level) const assertNever = (x: never): never => { throw new Error(`Unhandled sort key: ${x}`); }; // getButtonDescription(...) case 'desc': switch (currentSortKey) { case 'updated': return 'Most recently updated first'; case 'pushed': return 'Most recently pushed first'; case 'name': return 'Z to A'; case 'stars': return 'Most stars first'; } return assertNever(currentSortKey); // formatSortKey() switch (currentSortKey) { case 'updated': return 'Last Updated'; case 'pushed': return 'Last Pushed'; case 'name': return 'Name'; case 'stars': return 'Stars'; } return assertNever(currentSortKey);Also applies to: 124-131
133-178: Optional: avoid hard-coded widthFixed
width={45}can truncate on narrow terminals or wrap on wide ones. Consider omitting width or deriving from content for better resilience.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
src/ui/components/modals/SortDirectionModal.tsx(1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use strict TypeScript with comprehensive types; avoid any without clear justification
Files:
src/ui/components/modals/SortDirectionModal.tsx
src/ui/**/*.tsx
📄 CodeRabbit inference engine (AGENTS.md)
src/ui/**/*.tsx: Implement React/Ink UI as functional components using hooks
Use chalk for colours instead of Ink colour props to avoid nested Text issues
Pre-colour strings and render within a single to prevent nested Text rendering issues
Use spacers for consistent terminal spacing
Files:
src/ui/components/modals/SortDirectionModal.tsx
src/**/*.tsx
📄 CodeRabbit inference engine (AGENTS.md)
Use British English for all user-facing text (e.g., organisation, authorisation, colour)
Files:
src/ui/components/modals/SortDirectionModal.tsx
🧬 Code graph analysis (1)
src/ui/components/modals/SortDirectionModal.tsx (3)
src/ui/components/modals/SortModal.tsx (8)
SortKey(5-5)SortModal(13-172)sort(116-123)SortModalProps(7-11)sort(107-114)option(138-150)input(32-105)sort(125-130)src/ui/views/RepoList.tsx (1)
prev(1300-1304)src/ui/views/RepoList.main.tsx (1)
prev(944-948)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Cursor Bugbot
| useInput((input, key) => { | ||
| if (key.escape || (input && input.toUpperCase() === 'C')) { | ||
| onCancel(); | ||
| return; | ||
| } | ||
|
|
||
| if (key.leftArrow || key.upArrow) { | ||
| // Move selection up | ||
| if (focusedOption === 'cancel') { | ||
| // Move from cancel to last option | ||
| setFocusedOption(options[options.length - 1]); | ||
| } else if (focusedOption === 'asc') { | ||
| setFocusedOption('desc'); | ||
| } else if (focusedOption === 'desc') { | ||
| // Stay at top | ||
| } | ||
| } | ||
|
|
||
| if (key.rightArrow || key.downArrow) { | ||
| // Move selection down | ||
| if (focusedOption === 'desc') { | ||
| setFocusedOption('asc'); | ||
| } else if (focusedOption === 'asc') { | ||
| setFocusedOption('cancel'); | ||
| } else if (focusedOption === 'cancel') { | ||
| // Stay at bottom | ||
| } | ||
| } | ||
|
|
||
| if (key.tab) { | ||
| // Tab through all options including cancel | ||
| if (focusedOption === 'desc') { | ||
| setFocusedOption('asc'); | ||
| } else if (focusedOption === 'asc') { | ||
| setFocusedOption('cancel'); | ||
| } else if (focusedOption === 'cancel') { | ||
| setFocusedOption('desc'); | ||
| } | ||
| } | ||
|
|
||
| if (key.return) { | ||
| if (focusedOption === 'cancel') { | ||
| onCancel(); | ||
| } else { | ||
| onSelect(focusedOption as SortDirection); | ||
| } | ||
| } | ||
|
|
||
| // Quick select shortcuts | ||
| if (input) { | ||
| const upperInput = input.toUpperCase(); | ||
| if (upperInput === 'A') { | ||
| onSelect('asc'); | ||
| } else if (upperInput === 'D') { | ||
| onSelect('desc'); | ||
| } | ||
| } | ||
| }); |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Sanity-check: parent closes modal on select/cancel
Handler calls rely on the parent to close the modal. Confirm the parent view sets sortDirectionMode false in both paths.
🏁 Script executed:
#!/bin/bash
# Verify that SortDirectionModal consumers close the modal on both onSelect and onCancel
rg -nP --type=ts --type=tsx -C3 "SortDirectionModal" |
sed -n '1,200p'Length of output: 96
🏁 Script executed:
#!/bin/bash
# Search for SortDirectionModal usage in .tsx files
rg -nP "SortDirectionModal" -C5 -g '*.tsx'
# Search for onSelect/onCancel props in SortDirectionModal usage
rg -nP "<SortDirectionModal[^>]*(onSelect|onCancel)" -C5 -g '*.tsx'Length of output: 4479
Fix onCancel handler to close SortDirectionModal
In src/ui/views/RepoList.tsx (around line 2013), the onCancel prop of <SortDirectionModal> calls setSortMode(false)—this closes the sort-key modal instead of the sort-direction modal. Change it to setSortDirectionMode(false).
🤖 Prompt for AI Agents
In src/ui/components/modals/SortDirectionModal.tsx around lines 30-87 the
modal's onCancel is being handled in the parent (src/ui/views/RepoList.tsx ~line
2013) with setSortMode(false) which closes the wrong modal; update the parent
prop passed to SortDirectionModal so that onCancel calls
setSortDirectionMode(false) instead of setSortMode(false) (preserve any arrow
function/closure used and ensure correct typing/signature for the prop).
| return ( | ||
| <Box flexDirection="column" borderStyle="round" borderColor="cyan" paddingX={2} paddingY={1} width={45}> | ||
| <Text bold>Sort Direction</Text> | ||
| <Text color="gray" dimColor>Sorting by: {formatSortKey()}</Text> |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Replace Ink colour props with chalk and pre-colour strings
Guideline asks to use chalk (and a single Text) to avoid nested Text issues. Also update the help hint to advertise “C” as cancel (supported in the handler).
- <Text color="gray" dimColor>Sorting by: {formatSortKey()}</Text>
+ <Text>{chalk.dim.gray(`Sorting by: ${formatSortKey()}`)}</Text>
@@
- <Text color="gray" dimColor>
- {' '}{getButtonDescription(option)}
- </Text>
+ <Text>{chalk.dim.gray(` ${getButtonDescription(option)}`)}</Text>
@@
- <Text color="gray" dimColor>
- ↑↓/Enter • A/D • Esc
- </Text>
+ <Text>{chalk.dim.gray('↑↓/Enter • A/D • Esc/C')}</Text>Also applies to: 152-154, 174-176
🤖 Prompt for AI Agents
In src/ui/components/modals/SortDirectionModal.tsx around line 136 (also applies
to 152-154 and 174-176): the current code uses Ink <Text> color/dimColor props
and nested Texts; replace those by using chalk to pre-color strings and render
them with a single <Text> each (no nested Text components), removing all
color/dimColor props on Text; update the help hint strings to advertise "C" as
cancel (e.g., include "[C]ancel" or "C: cancel") and ensure the cancel key is
shown consistently in those lines; keep the text content identical aside from
coloring and the updated cancel hint.
Summary
This PR improves the user experience by replacing the direct sort direction toggle with an interactive modal, providing better visual feedback and consistency with the existing sort key modal.
Changes
SortDirectionModalcomponent with visual feedbackBefore
After
Screenshots
The modal provides context-aware descriptions based on the current sort key:
Testing
Related
This follows the same UX pattern as the sort key modal (S key), providing a consistent and intuitive interface throughout the application.
Summary by CodeRabbit
New Features
Documentation