Skip to content

feat: add repository URL copying modal with interactive selection#24

Merged
wiiiimm merged 8 commits into
mainfrom
feature/repo-copy
Sep 3, 2025
Merged

feat: add repository URL copying modal with interactive selection#24
wiiiimm merged 8 commits into
mainfrom
feature/repo-copy

Conversation

@wiiiimm
Copy link
Copy Markdown
Owner

@wiiiimm wiiiimm commented Sep 3, 2025

Summary

• Add interactive modal for copying repository clone URLs (SSH/HTTPS)
• Implement keyboard navigation with visual selection indicators
• Support multiple interaction methods for improved UX
• Include comprehensive test coverage with 15 test cases

Features Added

  • CopyUrlModal Component: Interactive modal triggered by 'C' key
  • Keyboard Navigation: Up/down arrows to select between SSH and HTTPS
  • Visual Indicators: Blue borders, arrow indicators, and dynamic hints
  • Multiple Copy Methods: Enter for selected, S for SSH, H for HTTPS shortcuts
  • Cross-platform Clipboard: Robust clipboard support with OS-specific fallbacks
  • Error Handling: Graceful error display when clipboard operations fail

Technical Details

  • Added copyToClipboard() utility function with platform detection
  • Integrated modal state management into RepoList component
  • Toast notifications for copy success/failure feedback
  • Comprehensive Vitest test suite covering all functionality
  • Updated README.md with feature documentation

Test Coverage

✅ 15 test cases covering:

  • Modal rendering and UI elements
  • Keyboard navigation (arrows, Enter, shortcuts)
  • Copy functionality with different methods
  • Error handling and edge cases
  • Modal lifecycle (open/close)

User Experience

Users can now press 'C' on any repository to:

  1. Open an interactive modal showing both SSH and HTTPS URLs
  2. Navigate with arrow keys (SSH selected by default)
  3. Copy selected URL with Enter, or use S/H shortcuts
  4. Close with multiple options (Esc, Q, C)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Interactive Copy URL modal (shortcut C) to copy SSH or HTTPS clone URLs with keyboard navigation (↑/↓, ←/→, Enter, S/H/Y), defaults to SSH, includes toast feedback and multiple close keys (Esc/Q/C).
  • Documentation

    • Documented the Copy URLs action across core docs and roadmap marked complete.
  • Tests

    • Added comprehensive keyboard-driven tests for selection, copy and close behaviour.
  • Chores

    • Added cross-platform clipboard support and dependency for clipboard handling.

- Add interactive CopyUrlModal component with SSH/HTTPS selection
- Implement keyboard navigation with up/down arrows and Enter to copy
- Add visual selection indicators and dynamic hints
- Support multiple copy methods (Enter, S for SSH, H for HTTPS)
- Add cross-platform clipboard functionality with fallbacks
- Include comprehensive test coverage (15 test cases)
- Update README with new feature documentation

Users can now press 'C' to open an interactive modal that displays both
SSH and HTTPS clone URLs, with keyboard navigation to select and copy
the desired URL format to clipboard.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Sep 3, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Warning

Rate limit exceeded

@wiiiimm has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 7 minutes and 11 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 04a151e and cd14f8a.

📒 Files selected for processing (1)
  • tests/ui/CopyUrlModal.test.tsx (1 hunks)

Walkthrough

Adds an interactive "Copy URL" feature: a new Ink modal component, RepoList keyboard shortcut/handlers and toast feedback, a clipboard utility with clipboardy plus OS fallbacks, README/docs updates, modal re-export, new tests, and small prop additions to RepoList.

Changes

Cohort / File(s) Summary
Docs
README.md
Documents new "Copy URLs" action, modal behaviour, keyboard shortcuts (C, S, H, ↑/↓, Enter, Esc/Q/C), and marks roadmap item complete.
Dependencies
package.json
Adds dependency clipboardy@^4.0.0.
Repo list UI & control flow
src/ui/RepoList.tsx
Adds Copy URL workflow: modal state/handlers, C shortcut, action-bar/help text update, copy toast UI, input trapping while modal open, and two optional props onOrgContextChange? & initialOrgSlug?.
Modal component
src/ui/components/modals/CopyUrlModal.tsx
New Ink modal (default export) that computes SSH/HTTPS URLs, provides keyboard navigation (↑/↓, Left/Right, Enter, S/H, Y), close keys (Esc/Q/C), shows copy errors, and calls async onCopy(url, type) / onClose().
Modal exports
src/ui/components/modals/index.ts
Re-exports CopyUrlModal.
Clipboard utility
src/utils.ts
Adds export async function copyToClipboard(text: string): Promise<void> using clipboardy first, then OS fallbacks (pbcopy, clip, xclip/xsel/wl-copy) with an explanatory error on failure.
Tests
tests/ui/CopyUrlModal.test.tsx
Adds Vitest + ink-testing-library tests mocking useInput to cover rendering, keyboard navigation, copy actions (Enter/S/H/Y), close keys, null-repo case, left/right wrap, and URL generation.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User
  participant RL as RepoList
  participant M as CopyUrlModal
  participant UT as copyToClipboard
  participant CB as clipboard (clipboardy / OS)

  U->>RL: Press C
  RL->>M: Open modal (repo, handlers)

  rect rgba(240,248,255,0.9)
    note right of M: Default = SSH\nNavigate with ↑/↓, ←/→, or press S/H\nEnter/Y to copy
    U->>M: ↑/↓ / ←/→ / S / H / Enter / Y
    alt copy action (Enter / S / H / Y)
      M->>UT: onCopy(url, type)
      UT->>CB: attempt copy (clipboardy → OS fallbacks)
      CB-->>UT: success / failure
      alt success
        UT-->>M: resolved
        M->>RL: onClose()
        RL->>U: show success toast
      else failure
        UT-->>M: throw error
        M->>M: display copyError
      end
    else close (Esc / Q / C)
      U->>M: Esc/Q/C
      M->>RL: onClose()
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

I twitch my nose and press the key,
C for copy, URLs for me.
SSH or HTTPS in view,
Enter takes the one I choose.
A little toast — a rabbit hop of glee 🐇

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/repo-copy

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

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

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

Codex Review: Here are some suggestions.

Reply with @codex fix comments to fix any unresolved comments.

About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you open a pull request for review, mark a draft as ready, or comment "@codex review". If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex fix this CI failure" or "@codex address that feedback".

Comment thread src/ui/components/modals/CopyUrlModal.tsx Outdated
cursor[bot]

This comment was marked as outdated.

- Fix undefined input bug in CopyUrlModal keyboard handler
- Fix command injection vulnerability in copyToClipboard utility
- Replace shell command execution with secure spawn() calls
- Guard input parameter before toLowerCase() operations

Addresses PR feedback from @chatgpt-codex-connector and @cursor
regarding critical P1 security and stability issues.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (7)
package.json (1)

49-49: Dependency addition looks good; engines match clipboardy’s Node 18+ requirement.

No issues adding clipboardy@^4. One note: because you package binaries with pkg, the dynamic import may be excluded at build time and always hit your fallback. If that’s intentional, all good; otherwise add it to pkg’s assets or switch to a static import.

README.md (1)

262-266: Keybinding consistency: consider supporting Left/Right as per modal UX guideline.

Guidelines specify Left/Right to move focus; this modal uses Up/Down (documented here). Either add Left/Right as aliases or call out the deviation in the guidelines for this modal.

src/utils.ts (1)

62-66: Preserve error cause and give actionable guidance.

Surface the underlying OS error for debugging, and suggest a utility to install (xclip/xsel/wl-clipboard).

-    } catch (osError) {
-      throw new Error(`Failed to copy to clipboard. Please install a clipboard utility for your system.`);
+    } catch (osError) {
+      const hint = process.platform === 'linux'
+        ? 'Install xclip, xsel, or wl-clipboard.'
+        : process.platform === 'darwin'
+        ? 'pbcopy should be available on macOS.'
+        : 'Ensure clip.exe is available on PATH.';
+      throw new Error(`Failed to copy to clipboard. ${hint}`, { cause: osError as unknown });
     }
src/ui/components/modals/CopyUrlModal.tsx (1)

74-116: Follow UI styling guideline: prefer chalk-coloured strings over Ink color props.

Pre-colour strings to avoid nested Text issues. Also consider adding minHeight on the outer Box for consistent terminal spacing.

-    <Box 
+    <Box 
       flexDirection="column" 
       borderStyle="round" 
       borderColor="blue" 
       paddingX={3} 
       paddingY={2} 
-      width={Math.min(terminalWidth - 8, 80)}
+      width={Math.min(terminalWidth - 8, 80)}
+      minHeight={12}
     >
-      <Text bold color="blue">Copy Repository URL</Text>
+      <Text>{chalk.blue.bold('Copy Repository URL')}</Text>
@@
-      <Text color="gray">SSH URL:</Text>
+      <Text>{chalk.gray('SSH URL:')}</Text>
@@
-        <Text color={selectedType === 'SSH' ? 'blue' : undefined}>
-          {selectedType === 'SSH' ? '▶ ' : '  '}{sshUrl}
-        </Text>
+        <Text>
+          {selectedType === 'SSH' ? chalk.blue(`▶ ${sshUrl}`) : `  ${sshUrl}`}
+        </Text>
@@
-      <Text color="gray">HTTPS URL:</Text>
+      <Text>{chalk.gray('HTTPS URL:')}</Text>
@@
-        <Text color={selectedType === 'HTTPS' ? 'blue' : undefined}>
-          {selectedType === 'HTTPS' ? '▶ ' : '  '}{httpsUrl}
-        </Text>
+        <Text>
+          {selectedType === 'HTTPS' ? chalk.blue(`▶ ${httpsUrl}`) : `  ${httpsUrl}`}
+        </Text>
@@
-      <Text color="gray">
-        ↑↓ Select • Enter to copy {selectedType} • S copy SSH • H copy HTTPS • Esc/Q/C to close
-      </Text>
+      <Text>
+        {chalk.gray(`↑↓/←→ Select • Enter to copy ${selectedType} • S copy SSH • H copy HTTPS • Esc/Q/C to close`)}
+      </Text>
src/ui/RepoList.tsx (2)

2135-2142: Prefer chalk-coloured strings over Ink colour props for the toast

Matches local guideline to avoid nested Text colour issues.

-          <Box borderStyle="round" borderColor={copyToast.includes('Failed') ? 'red' : 'green'} paddingX={2} paddingY={0}>
-            <Text color={copyToast.includes('Failed') ? 'red' : 'green'}>{copyToast}</Text>
-          </Box>
+          <Box borderStyle="round" borderColor={copyToast.includes('Failed') ? 'red' : 'green'} paddingX={2} paddingY={0}>
+            <Text>{copyToast.includes('Failed') ? chalk.red(copyToast) : chalk.green(copyToast)}</Text>
+          </Box>

1030-1034: Align modal navigation with documented Left/Right UX

Guideline for src/ui/**/*.tsx modals specifies Left/Right to move focus, Enter to activate, Y to confirm, C/Esc to cancel. CopyUrlModal currently uses Up/Down for selection. Consider adding Left/Right aliases in the modal and updating its footer hint to include ←→.

I can draft the change in CopyUrlModal and extend tests for ←/→ if you’d like.

Also applies to: 1231-1238

tests/ui/CopyUrlModal.test.tsx (1)

81-93: Instruction footer assertions OK; consider future-proofing

If you add ←/→ support per guidelines, extend assertions to include those hints and “cancel” instead of “close”.

📜 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 156d3e6 and 3109942.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (7)
  • README.md (3 hunks)
  • package.json (1 hunks)
  • src/ui/RepoList.tsx (9 hunks)
  • src/ui/components/modals/CopyUrlModal.tsx (1 hunks)
  • src/ui/components/modals/index.ts (1 hunks)
  • src/utils.ts (1 hunks)
  • tests/ui/CopyUrlModal.test.tsx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
package.json

📄 CodeRabbit inference engine (AGENTS.md)

Ensure package.json defines bin: { "gh-manager-cli": "dist/index.js" }

Files:

  • package.json
src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Follow TypeScript strictly; avoid any types without strong justification

Files:

  • src/utils.ts
  • src/ui/RepoList.tsx
  • src/ui/components/modals/CopyUrlModal.tsx
  • src/ui/components/modals/index.ts
src/{index.tsx,ui/**/*.tsx}

📄 CodeRabbit inference engine (AGENTS.md)

src/{index.tsx,ui/**/*.tsx}: Use React functional components with hooks for Ink UI
Use chalk for colours; pre-colour strings instead of Ink color props to avoid nested Text issues
Use British English for all user-facing text (e.g., colour, organisation, authorisation)

Files:

  • src/ui/RepoList.tsx
  • src/ui/components/modals/CopyUrlModal.tsx
src/ui/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

src/ui/**/*.tsx: Use Box with minHeight for consistent spacing across terminals
Implement documented keybindings (navigation, filtering, actions) in UI components
Modal UX: Left/Right to move focus, Enter to activate, Y to confirm, C/Esc to cancel

Files:

  • src/ui/RepoList.tsx
  • src/ui/components/modals/CopyUrlModal.tsx
🧠 Learnings (1)
📚 Learning: 2025-09-02T19:40:09.867Z
Learnt from: CR
PR: wiiiimm/gh-manager-cli#0
File: AGENTS.md:0-0
Timestamp: 2025-09-02T19:40:09.867Z
Learning: Applies to src/ui/**/*.tsx : Modal UX: Left/Right to move focus, Enter to activate, Y to confirm, C/Esc to cancel

Applied to files:

  • src/ui/RepoList.tsx
🧬 Code graph analysis (3)
tests/ui/CopyUrlModal.test.tsx (3)
src/types.ts (1)
  • RepoNode (8-42)
src/ui/components/modals/CopyUrlModal.tsx (1)
  • CopyUrlModal (15-126)
tests/ui/RenameModal.test.tsx (7)
  • mockUseInput (50-351)
  • mockUseInput (336-350)
  • mockUseInput (77-93)
  • mockUseInput (95-106)
  • mockUseInput (256-273)
  • mockUseInput (275-287)
  • mockUseInput (108-122)
src/ui/RepoList.tsx (4)
src/types.ts (1)
  • RepoNode (8-42)
src/utils.ts (1)
  • copyToClipboard (29-66)
src/ui/components/modals/CopyUrlModal.tsx (1)
  • CopyUrlModal (15-126)
src/ui/RepoList.main.tsx (1)
  • input (569-993)
src/ui/components/modals/CopyUrlModal.tsx (2)
src/types.ts (1)
  • RepoNode (8-42)
src/ui/components/modals/InfoModal.tsx (2)
  • InfoModal (13-57)
  • InfoModalProps (7-11)
🪛 Biome (2.1.2)
src/ui/components/modals/CopyUrlModal.tsx

[error] 27-27: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

⏰ 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 (26)
README.md (2)

63-64: Docs align with the new ‘C Copy URL’ action.

The feature description and action list look consistent with the implementation.


438-439: Roadmap entry accurately reflects the feature.

Recently implemented section reads well and matches behaviour.

src/ui/components/modals/index.ts (1)

10-10: Barrel export added correctly.

Re-exporting CopyUrlModal keeps import sites tidy and consistent with other modals.

src/ui/RepoList.tsx (10)

12-12: Import surface looks good

Re-export usage via components/modals aligns with existing pattern.


15-15: Utility import OK

copyToClipboard integration is appropriate here.


151-155: Solid, minimal state for modal + toast

Names are clear and scoped.


217-221: Clean modal open handler

Initialises target and resets toast—good.


223-227: Close handler resets all related state

Good hygiene.


1030-1034: Input trapping for modal is correct

Prevents list handlers from consuming keys while the modal is open.


1231-1238: Keybinding to open modal fits the scheme

‘C’ is consistent with footer/help.


1443-1443: Include copyUrlMode in modalOpen

Ensures global dimming/UX consistency.


1982-1990: Modal render wiring is sound

Props passed match the modal’s contract.


2116-2116: Help footer updated appropriately

Nice addition; keeps discoverability high.

tests/ui/CopyUrlModal.test.tsx (13)

7-14: Mocking Ink’s useInput correctly

This isolates stdin and keeps tests deterministic.


47-53: Good fixture reset pattern

Dynamic import ensures the mocked hook reference is current.


54-67: Render assertions cover the essentials

Title, repo, and URLs verified.


69-79: Default selection checks are clear

Arrow marker and prompt verified.


95-110: Enter-to-copy path covered

OnCopy args validated.


112-127: Shortcut S for SSH works

Good coverage.


129-144: Shortcut H for HTTPS works

Good coverage.


146-178: Arrow navigation tests read well

State transitions asserted via prompt text.


180-220: Close keys (Esc/Q/C) covered

Covers the modal lifecycle.


222-229: Null repo path handled

Error message rendered as expected.


231-240: URL generation validated

Both protocols asserted.


242-264: Selection switching integration test is useful

Covers up/down interaction end-to-end.


266-283: Enter uses current selection

Covers the main interaction flow comprehensively.

Comment thread src/ui/components/modals/CopyUrlModal.tsx Outdated
Comment thread src/ui/components/modals/CopyUrlModal.tsx
Comment thread src/ui/RepoList.tsx Outdated
Comment thread src/utils.ts Outdated
Comment thread tests/ui/CopyUrlModal.test.tsx
cursor[bot]

This comment was marked as outdated.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
src/ui/components/modals/CopyUrlModal.tsx (2)

19-24: Fix hooks order; add Left/Right navigation and Y to confirm; guard input safely.

useInput must be unconditional; also align keys with modal UX (←/→ to move focus, Y to confirm). Compute URLs lazily inside the handler when repo exists.

Apply:

-  if (!repo) {
-    return <Text color="red">No repository selected.</Text>;
-  }
-
-  const sshUrl = `git@github.com:${repo.nameWithOwner}.git`;
-  const httpsUrl = `https://github.com/${repo.nameWithOwner}.git`;
-
   // Handle keyboard input
   useInput((input, key) => {
-    if (key.escape || (input && (input.toLowerCase() === 'c' || input.toLowerCase() === 'q'))) {
+    const lower = typeof input === 'string' ? input.toLowerCase() : '';
+    if (key.escape || lower === 'c' || lower === 'q') {
       onClose();
       return;
     }
-    
-    // Up/Down arrow navigation
-    if (key.upArrow) {
+
+    if (!repo) return; // no-op when there is no repository
+
+    // Navigation (↑/↓ and ←/→)
+    if (key.upArrow || key.leftArrow) {
       setSelectedType('SSH');
       return;
     }
-    
-    if (key.downArrow) {
+
+    if (key.downArrow || key.rightArrow) {
       setSelectedType('HTTPS');
       return;
     }
     
-    // Enter key copies selected option
-    if (key.return) {
-      const urlToCopy = selectedType === 'SSH' ? sshUrl : httpsUrl;
+    // Enter/Y confirm: copy selected
+    if (key.return || lower === 'y') {
+      const urlToCopy =
+        selectedType === 'SSH'
+          ? `git@github.com:${repo.nameWithOwner}.git`
+          : `https://github.com/${repo.nameWithOwner}.git`;
       handleCopy(urlToCopy, selectedType);
       return;
     }
     
     // S/H shortcuts still work
-    if (input && input.toLowerCase() === 's') {
-      handleCopy(sshUrl, 'SSH');
+    if (lower === 's') {
+      handleCopy(`git@github.com:${repo.nameWithOwner}.git`, 'SSH');
       return;
     }
     
-    if (input && input.toLowerCase() === 'h') {
-      handleCopy(httpsUrl, 'HTTPS');
+    if (lower === 'h') {
+      handleCopy(`https://github.com/${repo.nameWithOwner}.git`, 'HTTPS');
       return;
     }
   });
+
+  if (!repo) {
+    return <Text color="red">No repository selected.</Text>;
+  }
+
+  const sshUrl = `git@github.com:${repo.nameWithOwner}.git`;
+  const httpsUrl = `https://github.com/${repo.nameWithOwner}.git`;

Verify tests cover ←/→ and Y:

#!/bin/bash
# Find tests for new keybindings
rg -nC2 -g 'tests/**' -e 'leftArrow|rightArrow' -e "['\"]y['\"]"

Also applies to: 26-61


68-70: Avoid any in catch; use unknown with safe narrowing.

Keeps TS strict across src/**/*.

-    } catch (error: any) {
-      setCopyError(`Failed to copy ${type} URL: ${error.message | | 'Unknown error'}`);
+    } catch (error: unknown) {
+      const message = error instanceof Error ? error.message : String(error ?? 'Unknown error');
+      setCopyError(`Failed to copy ${type} URL: ${message}`);
     }
🧹 Nitpick comments (8)
src/utils.ts (3)

40-57: Capture stderr and add a safety timeout for better diagnostics/robustness.

Expose underlying failures (e.g., missing binary) and avoid hanging processes.

Apply:

-    const spawnCommand = (command: string, args: string[] = []): Promise<void> => {
-      return new Promise((resolve, reject) => {
-        const child = spawn(command, args, { stdio: ['pipe', 'pipe', 'pipe'] });
+    const spawnCommand = (command: string, args: string[] = [], timeoutMs = 3000): Promise<void> => {
+      return new Promise((resolve, reject) => {
+        const child = spawn(command, args, { stdio: ['pipe', 'pipe', 'pipe'] });
+        let stderr = '';
+        const timer = setTimeout(() => {
+          child.kill();
+          reject(new Error(`${command} timed out after ${timeoutMs}ms`));
+        }, timeoutMs);
         
         child.stdin.write(text);
         child.stdin.end();
+        child.stderr.on('data', (d) => (stderr += d.toString()));
         
         child.on('close', (code) => {
+          clearTimeout(timer);
           if (code === 0) {
             resolve();
           } else {
-            reject(new Error(`Command failed with code ${code}`));
+            reject(new Error(`${command} exited with code ${code}${stderr ? `: ${stderr.trim()}` : ''}`));
           }
         });
         
         child.on('error', reject);
       });
     };

65-68: Add Windows PowerShell fallback (Set-Clipboard).

clip.exe is usually present, but having PowerShell as a secondary path hardens cross-environment behaviour.

-      } else if (platform === 'win32') {
-        // Windows - use clip
-        await spawnCommand('clip');
+      } else if (platform === 'win32') {
+        // Windows - try clip, then PowerShell
+        try {
+          await spawnCommand('clip');
+        } catch {
+          await spawnCommand('powershell', ['-NoProfile', '-Command', 'Set-Clipboard']);
+        }

80-82: Preserve underlying error details in thrown message.

Makes failures actionable without guessing which utility is missing.

-    } catch (osError) {
-      throw new Error(`Failed to copy to clipboard. Please install a clipboard utility for your system.`);
+    } catch (osError) {
+      const msg = osError instanceof Error ? osError.message : String(osError ?? 'Unknown error');
+      throw new Error(`Failed to copy to clipboard: ${msg}. If you are on Linux, install xclip, xsel, or wl-clipboard.`);
     }
src/ui/components/modals/CopyUrlModal.tsx (5)

74-81: Add minHeight for layout consistency across terminals.

Matches the UI spacing guideline.

   return (
     <Box 
       flexDirection="column" 
       borderStyle="round" 
       borderColor="blue" 
       paddingX={3} 
       paddingY={2} 
-      width={Math.min(terminalWidth - 8, 80)}
+      width={Math.min(terminalWidth - 8, 80)}
+      minHeight={12}
     >

82-82: Prefer chalk-coloured strings over Ink colour props for text.

Avoid nested Text colour issues per guidelines.

-      <Text bold color="blue">Copy Repository URL</Text>
+      <Text>{chalk.blue.bold('Copy Repository URL')}</Text>

88-98: Colour the SSH line with chalk instead of Text colour prop.

Keeps styling consistent and avoids nested colour props.

-      <Text color="gray">SSH URL:</Text>
+      <Text>{chalk.gray('SSH URL:')}</Text>
@@
-        <Text color={selectedType === 'SSH' ? 'blue' : undefined}>
-          {selectedType === 'SSH' ? '▶ ' : '  '}{sshUrl}
-        </Text>
+        <Text>
+          {selectedType === 'SSH'
+            ? chalk.blue(`▶ ${sshUrl}`)
+            : `  ${sshUrl}`}
+        </Text>

101-111: Colour the HTTPS line with chalk for consistency.

-      <Text color="gray">HTTPS URL:</Text>
+      <Text>{chalk.gray('HTTPS URL:')}</Text>
@@
-        <Text color={selectedType === 'HTTPS' ? 'blue' : undefined}>
-          {selectedType === 'HTTPS' ? '▶ ' : '  '}{httpsUrl}
-        </Text>
+        <Text>
+          {selectedType === 'HTTPS'
+            ? chalk.blue(`▶ ${httpsUrl}`)
+            : `  ${httpsUrl}`}
+        </Text>

114-116: Update hint text to reflect documented keybindings.

Include ←/→ and Y; drop Q per guidelines.

-      <Text color="gray">
-        ↑↓ Select • Enter to copy {selectedType} • S copy SSH • H copy HTTPS • Esc/Q/C to close
-      </Text>
+      <Text color="gray">
+        ↑↓/←→ Select • Enter/Y to copy {selectedType} • S copy SSH • H copy HTTPS • Esc/C to cancel
+      </Text>
📜 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 3109942 and a7ecc77.

📒 Files selected for processing (2)
  • src/ui/components/modals/CopyUrlModal.tsx (1 hunks)
  • src/utils.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Follow TypeScript strictly; avoid any types without strong justification

Files:

  • src/utils.ts
  • src/ui/components/modals/CopyUrlModal.tsx
src/{index.tsx,ui/**/*.tsx}

📄 CodeRabbit inference engine (AGENTS.md)

src/{index.tsx,ui/**/*.tsx}: Use React functional components with hooks for Ink UI
Use chalk for colours; pre-colour strings instead of Ink color props to avoid nested Text issues
Use British English for all user-facing text (e.g., colour, organisation, authorisation)

Files:

  • src/ui/components/modals/CopyUrlModal.tsx
src/ui/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

src/ui/**/*.tsx: Use Box with minHeight for consistent spacing across terminals
Implement documented keybindings (navigation, filtering, actions) in UI components
Modal UX: Left/Right to move focus, Enter to activate, Y to confirm, C/Esc to cancel

Files:

  • src/ui/components/modals/CopyUrlModal.tsx
🧬 Code graph analysis (1)
src/ui/components/modals/CopyUrlModal.tsx (2)
src/types.ts (1)
  • RepoNode (8-42)
src/ui/components/modals/InfoModal.tsx (2)
  • InfoModal (13-57)
  • InfoModalProps (7-11)
🪛 Biome (2.1.2)
src/ui/components/modals/CopyUrlModal.tsx

[error] 27-27: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

⏰ 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 (1)
src/utils.ts (1)

36-57: Resolved: command injection risk mitigated with spawn + stdin.

Switching from shell-quoted exec to spawn with stdin removes injection vectors. Good security hardening.

Comment thread src/ui/components/modals/CopyUrlModal.tsx Outdated
wiiiimm and others added 2 commits September 3, 2025 16:06
- Change catch error type from 'any' to 'unknown'
- Add proper type narrowing with instanceof Error check
- Use safe String(error) fallback for non-Error objects
- Ensure TypeScript strict rules are satisfied
- Maintain existing error message functionality

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Add timer ref management to prevent toast overlaps and memory leaks
- Fix CopyUrlModal onCopy prop type to return Promise<void>
- Improve error handling with proper unknown type narrowing
- Clean up toast timers on component unmount
- Fix test mock RepoNode to match strict interface requirements
- Remove duplicate error messages between toast and modal
- Prevent race conditions in rapid copying scenarios

Addresses all PR feedback regarding toast notifications, TypeScript
strict compliance, and memory management.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/ui/RepoList.tsx (1)

228-239: Fix copy toast lifecycle: don’t clear on modal close; avoid duplicate error toasts

Currently, closeCopyUrlModal clears the toast, so a success toast disappears as soon as the modal closes. Also, failures are shown both inside the modal and as a global toast. Keep success toast after close, and show errors only inside the modal.

Apply:

 function closeCopyUrlModal() {
   setCopyUrlMode(false);
   setCopyUrlTarget(null);
-  setCopyToast(null);
 }

 async function handleCopyUrl(url: string, type: 'SSH' | 'HTTPS'): Promise<void> {
   try {
     // Clear any existing timer before setting a new one
     if (copyToastTimerRef.current) {
       clearTimeout(copyToastTimerRef.current);
       copyToastTimerRef.current = null;
     }
     
     await copyToClipboard(url);
     setCopyToast(`Copied ${type} URL to clipboard`);
     
     // Set new timer for success toast
     copyToastTimerRef.current = setTimeout(() => {
       setCopyToast(null);
       copyToastTimerRef.current = null;
     }, 3000);
   } catch (error: unknown) {
     // Clear any existing timer before setting a new one
     if (copyToastTimerRef.current) {
       clearTimeout(copyToastTimerRef.current);
       copyToastTimerRef.current = null;
     }
-    
-    const message = error instanceof Error ? error.message : String(error) || 'Unknown error';
-    setCopyToast(`Failed to copy ${type} URL: ${message}`);
-    
-    // Set new timer for error toast
-    copyToastTimerRef.current = setTimeout(() => {
-      setCopyToast(null);
-      copyToastTimerRef.current = null;
-    }, 5000);
     
     throw error; // Re-throw so modal can handle it
   }
 }

Also applies to: 256-271

🧹 Nitpick comments (3)
src/ui/RepoList.tsx (1)

2170-2177: Use chalk to colour the toast text to match UI guidelines

Prefer pre-coloured strings over Ink colour props.

-          <Box borderStyle="round" borderColor={copyToast.includes('Failed') ? 'red' : 'green'} paddingX={2} paddingY={0}>
-            <Text color={copyToast.includes('Failed') ? 'red' : 'green'}>{copyToast}</Text>
+          <Box borderStyle="round" borderColor={copyToast.includes('Failed') ? 'red' : 'green'} paddingX={2} paddingY={0}>
+            <Text>{copyToast.includes('Failed') ? chalk.red(copyToast) : chalk.green(copyToast)}</Text>
           </Box>
src/ui/components/modals/CopyUrlModal.tsx (2)

115-117: Update instructions to reflect Left/Right and ‘Y’

Make hints match implemented keys.

-        ↑↓ Select • Enter to copy {selectedType} • S copy SSH • H copy HTTPS • Esc/Q/C to close
+        ↑↓/←→ Select • Enter/Y to copy {selectedType} • S copy SSH • H copy HTTPS • Esc/Q/C to close

120-124: Optional: pre-colour strings with chalk instead of Ink colour props

To avoid nested Text colour issues and match guidelines, consider:

-          <Text color="red">{copyError}</Text>
+          <Text>{chalk.red(copyError)}</Text>
📜 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.

📥 Commits

Reviewing files that changed from the base of the PR and between a7ecc77 and 03cbdef.

📒 Files selected for processing (3)
  • src/ui/RepoList.tsx (9 hunks)
  • src/ui/components/modals/CopyUrlModal.tsx (1 hunks)
  • tests/ui/CopyUrlModal.test.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/ui/CopyUrlModal.test.tsx
🧰 Additional context used
📓 Path-based instructions (3)
src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Follow TypeScript strictly; avoid any types without strong justification

Files:

  • src/ui/components/modals/CopyUrlModal.tsx
  • src/ui/RepoList.tsx
src/{index.tsx,ui/**/*.tsx}

📄 CodeRabbit inference engine (AGENTS.md)

src/{index.tsx,ui/**/*.tsx}: Use React functional components with hooks for Ink UI
Use chalk for colours; pre-colour strings instead of Ink color props to avoid nested Text issues
Use British English for all user-facing text (e.g., colour, organisation, authorisation)

Files:

  • src/ui/components/modals/CopyUrlModal.tsx
  • src/ui/RepoList.tsx
src/ui/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

src/ui/**/*.tsx: Use Box with minHeight for consistent spacing across terminals
Implement documented keybindings (navigation, filtering, actions) in UI components
Modal UX: Left/Right to move focus, Enter to activate, Y to confirm, C/Esc to cancel

Files:

  • src/ui/components/modals/CopyUrlModal.tsx
  • src/ui/RepoList.tsx
🧠 Learnings (1)
📚 Learning: 2025-09-02T19:40:09.867Z
Learnt from: CR
PR: wiiiimm/gh-manager-cli#0
File: AGENTS.md:0-0
Timestamp: 2025-09-02T19:40:09.867Z
Learning: Applies to src/ui/**/*.tsx : Modal UX: Left/Right to move focus, Enter to activate, Y to confirm, C/Esc to cancel

Applied to files:

  • src/ui/RepoList.tsx
🧬 Code graph analysis (2)
src/ui/components/modals/CopyUrlModal.tsx (2)
src/types.ts (1)
  • RepoNode (8-42)
src/ui/components/modals/InfoModal.tsx (2)
  • InfoModalProps (7-11)
  • InfoModal (13-57)
src/ui/RepoList.tsx (3)
src/types.ts (1)
  • RepoNode (8-42)
src/utils.ts (1)
  • copyToClipboard (29-84)
src/ui/components/modals/CopyUrlModal.tsx (1)
  • CopyUrlModal (15-127)
🪛 Biome (2.1.2)
src/ui/components/modals/CopyUrlModal.tsx

[error] 27-27: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

⏰ 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 (9)
src/ui/RepoList.tsx (7)

12-12: LGTM: correct imports and utility usage

Importing CopyUrlModal and copyToClipboard is correct and aligns with the new workflow.

Also applies to: 15-15


151-166: Toast timer lifecycle guard looks good

Nice use of a ref and cleanup effect to avoid dangling timers.


1065-1068: LGTM: input trapping while copy modal is open

Good isolation—parent ignores input when CopyUrlModal is active.


1266-1273: LGTM: keyboard shortcut to open modal

‘C’ to open aligns with the README and the footer help.


2017-2025: LGTM: modal integration

Props and async onCopy wiring are correct; width constraint matches other modals.


1478-1479: LGTM: modalOpen includes copy modal

Ensures proper dimming/interaction block across the UI.


2151-2152: LGTM: footer help updated

Help text correctly advertises “C Copy URL”.

src/ui/components/modals/CopyUrlModal.tsx (2)

63-71: Good: strict error typing and safe narrowing

Using unknown with proper narrowing is compliant with TS strict rules.


74-126: UI layout is clear and consistent

Borders, selection indicators, and dynamic hinting are well executed.

Comment thread src/ui/components/modals/CopyUrlModal.tsx Outdated
wiiiimm and others added 2 commits September 3, 2025 16:22
- Move useInput hook before early return to comply with Rules of Hooks
- Add Left/Right arrow navigation with wrap-around functionality
- Support both lowercase and uppercase 'Y' key for copy action
- Update footer instructions to show all available navigation options
- Add 5 comprehensive tests for new navigation features
- Ensure proper state management and prevent default behavior

Fixes critical React hooks violation while enhancing user experience
with more intuitive navigation patterns.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Keep left/right arrow functionality as a hidden convenience feature
while only showing the primary ↑↓ navigation in the footer instructions.
This provides a cleaner UI while still supporting intuitive horizontal
navigation for power users.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/ui/components/modals/CopyUrlModal.tsx (1)

22-29: Normalise input handling via a single lowercased char variable

Reduces repetition and keeps arrow/printable-key checks tidy. This repeats a prior suggestion.

Apply:

-  useInput((input, key) => {
+  useInput((input, key) => {
+    const ch = input?.toLowerCase();
@@
-    if (key.escape || (input && (input.toLowerCase() === 'c' || input.toLowerCase() === 'q'))) {
+    if (key.escape || ch === 'c' || ch === 'q') {
       onClose();
       return;
     }
@@
-    if (key.return || (input && input.toLowerCase() === 'y')) {
+    if (key.return || ch === 'y') {
@@
-    if (input && input.toLowerCase() === 's') {
+    if (ch === 's') {
@@
-    if (input && input.toLowerCase() === 'h') {
+    if (ch === 'h') {

Also applies to: 57-77

🧹 Nitpick comments (7)
src/ui/components/modals/CopyUrlModal.tsx (7)

100-107: Clamp width and add a minimum height for consistent layout

Avoid negative widths on narrow terminals and align with the “use Box with minHeight” guideline.

Apply:

-  return (
-    <Box 
+  const modalWidth = Math.max(40, Math.min(terminalWidth - 8, 80));
+  return (
+    <Box 
       flexDirection="column" 
       borderStyle="round" 
       borderColor="blue" 
       paddingX={3} 
       paddingY={2} 
-      width={Math.min(terminalWidth - 8, 80)}
+      width={modalWidth}
+      minHeight={12}
     >

108-149: Pre‑colour strings with chalk instead of Ink Text colour props

Our UI guideline prefers chalk-coloured strings to avoid nested Text quirks. Replace Text colour props with chalk, keeping border colours as-is.

Apply:

-      <Text bold color="blue">Copy Repository URL</Text>
+      <Text>{chalk.blue.bold('Copy Repository URL')}</Text>
@@
-      <Text color="gray">SSH URL:</Text>
+      <Text>{chalk.gray('SSH URL:')}</Text>
@@
-      >
-        <Text color={selectedType === 'SSH' ? 'blue' : undefined}>
-          {selectedType === 'SSH' ? '▶ ' : '  '}{sshUrl}
-        </Text>
+      >
+        <Text>
+          {selectedType === 'SSH'
+            ? chalk.blue(`▶ ${sshUrl}`)
+            : `  ${sshUrl}`}
+        </Text>
       </Box>
@@
-      <Text color="gray">HTTPS URL:</Text>
+      <Text>{chalk.gray('HTTPS URL:')}</Text>
@@
-      >
-        <Text color={selectedType === 'HTTPS' ? 'blue' : undefined}>
-          {selectedType === 'HTTPS' ? '▶ ' : '  '}{httpsUrl}
-        </Text>
+      >
+        <Text>
+          {selectedType === 'HTTPS'
+            ? chalk.blue(`▶ ${httpsUrl}`)
+            : `  ${httpsUrl}`}
+        </Text>
       </Box>
@@
-      <Text color="gray">
-        ↑↓←→ Select • Enter/Y to copy {selectedType} • S copy SSH • H copy HTTPS • Esc/Q/C to close
-      </Text>
+      <Text>
+        {chalk.gray(`↑↓←→ Select • Enter/Y to copy ${selectedType} • S copy SSH • H copy HTTPS • Esc/C to cancel`)}
+      </Text>
@@
-      {copyError && (
+      {copyError && (
         <>
           <Box height={1}><Text> </Text></Box>
-          <Text color="red">{copyError}</Text>
+          <Text>{chalk.red(copyError)}</Text>
         </>
       )}

81-83: Use chalk for the error message to match the guideline

Apply:

-  if (!repo) {
-    return <Text color="red">No repository selected.</Text>;
-  }
+  if (!repo) {
+    return <Text>{chalk.red('No repository selected.')}</Text>;
+  }

58-77: Deduplicate URL construction inside the input handler

Reuse the render-scoped sshUrl/httpsUrl instead of recomputing them in each branch.

Apply:

-    if (key.return || (input && input.toLowerCase() === 'y')) {
-      const sshUrl = `git@github.com:${repo.nameWithOwner}.git`;
-      const httpsUrl = `https://github.com/${repo.nameWithOwner}.git`;
-      const urlToCopy = selectedType === 'SSH' ? sshUrl : httpsUrl;
+    if (key.return || (input && input.toLowerCase() === 'y')) {
+      const urlToCopy = selectedType === 'SSH' ? sshUrl : httpsUrl;
       handleCopy(urlToCopy, selectedType);
       return;
     }
@@
-    if (input && input.toLowerCase() === 's') {
-      const sshUrl = `git@github.com:${repo.nameWithOwner}.git`;
+    if (input && input.toLowerCase() === 's') {
       handleCopy(sshUrl, 'SSH');
       return;
     }
@@
-    if (input && input.toLowerCase() === 'h') {
-      const httpsUrl = `https://github.com/${repo.nameWithOwner}.git`;
+    if (input && input.toLowerCase() === 'h') {
       handleCopy(httpsUrl, 'HTTPS');
       return;
     }

Note: If your linter flags “use-before-define” for sshUrl/httpsUrl in the handler, move their declarations above useInput and guard with repo checks, or keep current recomputation.

Also applies to: 85-87


13-20: Make urlTypes readonly and derive UrlType from it

Keeps the union in sync with the options array automatically.

Apply:

-type UrlType = 'SSH' | 'HTTPS';
+const urlTypes = ['SSH', 'HTTPS'] as const;
+type UrlType = typeof urlTypes[number];
@@
-  const urlTypes: UrlType[] = ['SSH', 'HTTPS'];
+  // urlTypes defined above as readonly tuple

94-96: Guarantee a meaningful fallback error message

String(error) is almost always truthy; ensure we fall back to 'Unknown error' when needed.

Apply:

-      const message = error instanceof Error ? error.message : String(error) || 'Unknown error';
+      const message =
+        error instanceof Error
+          ? (error.message || 'Unknown error')
+          : (error != null && String(error).trim() !== '' ? String(error) : 'Unknown error');

140-142: Wording: use ‘cancel’ (British English) and align with documented keys

Guideline says: Left/Right to move focus, Enter to activate, Y to confirm, C/Esc to cancel. If you keep ‘Q’ as an extra, either include it in docs or drop it from the handler.

Apply (text-only change; keep handler as-is or update docs):

-        {chalk.gray(`↑↓←→ Select • Enter/Y to copy ${selectedType} • S copy SSH • H copy HTTPS • Esc/Q/C to close`)}
+        {chalk.gray(`↑↓←→ Select • Enter/Y to copy ${selectedType} • S copy SSH • H copy HTTPS • Esc/C to cancel`)}
📜 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 03cbdef and bd3ce1a.

📒 Files selected for processing (2)
  • src/ui/components/modals/CopyUrlModal.tsx (1 hunks)
  • tests/ui/CopyUrlModal.test.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/ui/CopyUrlModal.test.tsx
🧰 Additional context used
📓 Path-based instructions (3)
src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Follow TypeScript strictly; avoid any types without strong justification

Files:

  • src/ui/components/modals/CopyUrlModal.tsx
src/{index.tsx,ui/**/*.tsx}

📄 CodeRabbit inference engine (AGENTS.md)

src/{index.tsx,ui/**/*.tsx}: Use React functional components with hooks for Ink UI
Use chalk for colours; pre-colour strings instead of Ink color props to avoid nested Text issues
Use British English for all user-facing text (e.g., colour, organisation, authorisation)

Files:

  • src/ui/components/modals/CopyUrlModal.tsx
src/ui/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

src/ui/**/*.tsx: Use Box with minHeight for consistent spacing across terminals
Implement documented keybindings (navigation, filtering, actions) in UI components
Modal UX: Left/Right to move focus, Enter to activate, Y to confirm, C/Esc to cancel

Files:

  • src/ui/components/modals/CopyUrlModal.tsx
🧠 Learnings (1)
📚 Learning: 2025-09-02T19:40:09.867Z
Learnt from: CR
PR: wiiiimm/gh-manager-cli#0
File: AGENTS.md:0-0
Timestamp: 2025-09-02T19:40:09.867Z
Learning: Applies to src/ui/**/*.tsx : Modal UX: Left/Right to move focus, Enter to activate, Y to confirm, C/Esc to cancel

Applied to files:

  • src/ui/components/modals/CopyUrlModal.tsx
🧬 Code graph analysis (1)
src/ui/components/modals/CopyUrlModal.tsx (2)
src/types.ts (1)
  • RepoNode (8-42)
src/ui/components/modals/InfoModal.tsx (2)
  • InfoModalProps (7-11)
  • InfoModal (13-57)
⏰ 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 (1)
src/ui/components/modals/CopyUrlModal.tsx (1)

21-44: Hooks order and keyboard UX look solid

  • useInput is unconditional and guarded for repo presence.
  • Left/Right focus, Up/Down shortcuts, Enter and Y to confirm, and C/Esc to cancel are implemented. Nice wrap-around logic.

Also applies to: 46-78

- Create single 'ch' variable for normalized character input
- Remove repetitive input?.toLowerCase() calls
- Clean up conditional checks for character-based inputs
- Maintain exact same functionality with cleaner code
- Reduces repetition and improves readability

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
tests/ui/CopyUrlModal.test.tsx (4)

79-91: Remove duplicate “footer instructions” test.

The test at Lines 364–376 duplicates the one at Lines 79–91. Keep one to avoid redundant assertions.

-  it('shows updated footer instructions with new navigation options', () => {
-    mockUseInput.mockImplementation(() => {});
-    
-    const { lastFrame, unmount } = render(<CopyUrlModal {...defaultProps} />);
-    
-    const output = lastFrame() || '';
-    expect(output).toContain('↑↓ Select');
-    expect(output).toContain('Enter/Y to copy SSH');
-    expect(output).toContain('S copy SSH');
-    expect(output).toContain('H copy HTTPS');
-    expect(output).toContain('Esc/Q/C to close');
-    unmount();
-  });

Also applies to: 364-376


45-50: Reset all spies between tests to prevent cross-test leakage.

Add a global clear to reset any vi.fn() call counts (e.g. defaultProps spies) between tests.

   beforeEach(async () => {
     // Reset and get the mocked useInput using dynamic import
     const ink = await import('ink');
     mockUseInput = (ink as any).useInput;
     mockUseInput.mockReset();
+    vi.clearAllMocks();
   });

93-108: Also assert the modal triggers onClose after a successful copy.

This verifies the full success path, not just onCopy invocation.

-  it('handles Enter key to copy selected option (SSH by default)', () => {
-    const mockOnCopy = vi.fn().mockResolvedValue(undefined);
+  it('handles Enter key to copy selected option (SSH by default) and closes on success', async () => {
+    const mockOnCopy = vi.fn().mockResolvedValue(undefined);
+    const mockOnClose = vi.fn();
     let inputCallback: any;
     
     mockUseInput.mockImplementation((callback: any) => {
       inputCallback = callback;
     });
     
-    const { unmount } = render(<CopyUrlModal {...defaultProps} onCopy={mockOnCopy} />);
+    const { unmount } = render(<CopyUrlModal {...defaultProps} onCopy={mockOnCopy} onClose={mockOnClose} />);
     
     // Simulate Enter key (SSH should be selected by default)
     inputCallback('', { return: true });
     
     expect(mockOnCopy).toHaveBeenCalledWith('git@github.com:owner/test-repo.git', 'SSH');
+    // allow async handler to resolve and close
+    await Promise.resolve();
+    expect(mockOnClose).toHaveBeenCalled();
     unmount();
   });

220-227: Add a guard test: ignore input when repo is null.

Ensures the early-return in the input handler is honoured.

   it('shows "No repository selected" when repo is null', () => {
     mockUseInput.mockImplementation(() => {});
     
     const { lastFrame, unmount } = render(<CopyUrlModal {...defaultProps} repo={null} />);
     
     expect(lastFrame()).toContain('No repository selected.');
     unmount();
   });
+
+  it('ignores inputs when repo is null', () => {
+    const mockOnClose = vi.fn();
+    let cb: any;
+    mockUseInput.mockImplementation((fn: any) => { cb = fn; });
+    const { unmount } = render(<CopyUrlModal {...defaultProps} repo={null} onClose={mockOnClose} />);
+    cb('q', {}); // would normally close
+    cb('', { escape: true });
+    expect(mockOnClose).not.toHaveBeenCalled();
+    unmount();
+  });
📜 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.

📥 Commits

Reviewing files that changed from the base of the PR and between bd3ce1a and f83e2ec.

📒 Files selected for processing (2)
  • src/ui/components/modals/CopyUrlModal.tsx (1 hunks)
  • tests/ui/CopyUrlModal.test.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/ui/components/modals/CopyUrlModal.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
tests/ui/CopyUrlModal.test.tsx (2)
src/types.ts (1)
  • RepoNode (8-42)
src/ui/components/modals/CopyUrlModal.tsx (1)
  • CopyUrlModal (15-152)
🔇 Additional comments (2)
tests/ui/CopyUrlModal.test.tsx (2)

1-377: Solid, comprehensive UI test coverage.

Good use of ink-testing-library, targeted keyboard-event simulation, and cleanup with unmount(). Overall structure reads well.


19-36: RepoNode mock matches the source type.

The mock now aligns with src/types.RepoNode (no extraneous fields). Nice fix.

Comment thread tests/ui/CopyUrlModal.test.tsx
- Test onCopy rejection scenario
- Verify error message displays in UI
- Ensure modal stays open on error
- Add comprehensive error path coverage

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
@wiiiimm wiiiimm merged commit 45998e9 into main Sep 3, 2025
3 checks passed
wiiiimm pushed a commit that referenced this pull request Sep 3, 2025
# [1.21.0](v1.20.0...v1.21.0) (2025-09-03)

### Features

* add repository URL copying modal with interactive selection ([#24](#24)) ([45998e9](45998e9))
@wiiiimm
Copy link
Copy Markdown
Owner Author

wiiiimm commented Sep 3, 2025

🎉 This PR is included in version 1.21.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant