Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,35 @@ const handleShutdown = (signal: string) => {
process.exit(0);
};

// Function to show sponsorship message
const showSponsorshipMessage = () => {
console.log('\n' + '─'.repeat(60));
console.log('\nπŸ’š Thank you for using gh-manager-cli!\n');
console.log('If this app saved you time, please consider supporting');
console.log('the development of more open-source projects like this:\n');
console.log(' β˜• Buy Me a Coffee: https://buymeacoffee.com/wiiiimm');
console.log(' πŸš€ Visit my site: https://wiiiimm.codes');
console.log(' πŸ’¬ Leave feedback: https://github.com/wiiiimm/gh-manager-cli');
console.log('\nYour support and contributions make a difference! πŸ™\n');
console.log('─'.repeat(60) + '\n');
};

// Register shutdown handlers
process.on('SIGINT', () => handleShutdown('SIGINT')); // Ctrl+C
process.on('SIGTERM', () => handleShutdown('SIGTERM')); // Kill signal
process.on('exit', (code) => {
// Only show sponsorship message on normal exit (code 0)
// and not when there's an error or when using --version/--help
const isNormalExit = code === 0;
const isInteractiveSession = !argv.includes('--version') &&
!argv.includes('-v') &&
!argv.includes('--help') &&
!argv.includes('-h');

if (isNormalExit && isInteractiveSession) {
showSponsorshipMessage();
}

logger.info('gh-manager-cli exited', {
exitCode: code,
uptime: process.uptime()
Expand Down
5 changes: 5 additions & 0 deletions src/ui/components/modals/InfoModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ export default function InfoModal({ repo, terminalWidth, onClose }: InfoModalPro
<Text color="gray">Updated: {formatDate(repo.updatedAt)} β€’ Pushed: {formatDate(repo.pushedAt)}</Text>
<Text color="gray">Size: {repo.diskUsage} KB</Text>
<Box height={1}><Text> </Text></Box>
<Box flexDirection="column" borderStyle="single" borderColor="yellow" paddingX={1} marginY={1}>
<Text color="yellow">πŸ’š Enjoying gh-manager-cli?</Text>
<Text color="gray">Support the project at:</Text>
<Text color="cyan">buymeacoffee.com/wiiiimm</Text>
</Box>
<Text color="gray">Press Esc or I to close</Text>
</Box>
);
Expand Down
40 changes: 39 additions & 1 deletion src/ui/views/RepoList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ export default function RepoList({ token, maxVisibleRows, onLogout, viewerLogin,
const [ownerAffiliations, setOwnerAffiliations] = useState<OwnerAffiliation[]>(['OWNER']);
const [orgSwitcherOpen, setOrgSwitcherOpen] = useState(false);

// Sponsor reminder state
const [operationCount, setOperationCount] = useState(0);
const [showSponsorReminder, setShowSponsorReminder] = useState(false);

// Search state (server-side)
const [searchItems, setSearchItems] = useState<RepoNode[]>([]);
const [searchEndCursor, setSearchEndCursor] = useState<string | null>(null);
Expand Down Expand Up @@ -189,6 +193,19 @@ export default function RepoList({ token, maxVisibleRows, onLogout, viewerLogin,
})();
}, [initialOrgSlug, token, prefsLoaded, client, addDebugMessage]);

// Helper to track successful operations and show sponsor reminder
function trackSuccessfulOperation() {
const newCount = operationCount + 1;
setOperationCount(newCount);

// Show sponsor reminder every 5 operations
if (newCount % 5 === 0) {
setShowSponsorReminder(true);
// Hide the reminder after 5 seconds
setTimeout(() => setShowSponsorReminder(false), 5000);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: Unmanaged Timers Cause React Warnings and Memory Leaks

The setTimeout in trackSuccessfulOperation isn't managed, which can lead to React warnings and memory leaks if the component unmounts. Additionally, multiple operations in quick succession can cause the sponsor reminder to disappear prematurely due to overlapping timers.

Fix in CursorΒ Fix in Web

}

Comment on lines +196 to +208
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

πŸ› οΈ Refactor suggestion

Make the counter update atomic and clear the hide timer to avoid leaks

Use functional setState to prevent stale reads, and store/clear the timeout so it doesn’t fire after unmount or stack with multiple operations.

-  function trackSuccessfulOperation() {
-    const newCount = operationCount + 1;
-    setOperationCount(newCount);
-    
-    // Show sponsor reminder every 5 operations
-    if (newCount % 5 === 0) {
-      setShowSponsorReminder(true);
-      // Hide the reminder after 5 seconds
-      setTimeout(() => setShowSponsorReminder(false), 5000);
-    }
-  }
+  function trackSuccessfulOperation() {
+    setOperationCount(prev => {
+      const next = prev + 1;
+      if (next % 5 === 0) {
+        setShowSponsorReminder(true);
+        // Reset any existing timer and start a new one
+        if (sponsorTimerRef.current) clearTimeout(sponsorTimerRef.current);
+        sponsorTimerRef.current = setTimeout(() => setShowSponsorReminder(false), 5000);
+      }
+      return next;
+    });
+  }

Additions needed outside this hunk:

// near other refs (e.g., around the copyToastTimerRef)
const sponsorTimerRef = useRef<NodeJS.Timeout | null>(null);

// extend existing unmount cleanup
useEffect(() => {
  return () => {
    if (copyToastTimerRef.current) clearTimeout(copyToastTimerRef.current);
    if (sponsorTimerRef.current) clearTimeout(sponsorTimerRef.current);
  };
}, []);
πŸ€– Prompt for AI Agents
In src/ui/views/RepoList.tsx around lines 196 to 208, the operation counter uses
non-functional state updates and the sponsor reminder timeout is not tracked or
cleared; change setOperationCount to the functional form (prev => prev + 1) to
avoid stale reads, store the timeout ID in a new sponsorTimerRef (declare const
sponsorTimerRef = useRef<NodeJS.Timeout | null>(null) near other refs), clear
any existing sponsorTimerRef.current before creating a new setTimeout and assign
the ID to sponsorTimerRef.current, and extend the component unmount cleanup
effect to clear sponsorTimerRef.current if set so the timeout won't fire after
unmount or stack across operations.

function closeArchiveModal() {
setArchiveMode(false);
setArchiveTarget(null);
Expand Down Expand Up @@ -301,6 +318,7 @@ export default function RepoList({ token, maxVisibleRows, onLogout, viewerLogin,
setItems(prev => prev.map(updateRepo));
setSearchItems(prev => prev.map(updateRepo));

trackSuccessfulOperation(); // Track the successful operation
closeArchiveModal();
} catch (e) {
setArchiving(false);
Expand Down Expand Up @@ -511,6 +529,7 @@ export default function RepoList({ token, maxVisibleRows, onLogout, viewerLogin,
setSearchTotalCount((c) => Math.max(0, c - 1));
}

trackSuccessfulOperation(); // Track the successful operation
setDeleteMode(false);
setDeleteTarget(null);
setTypedCode('');
Expand Down Expand Up @@ -1621,6 +1640,19 @@ export default function RepoList({ token, maxVisibleRows, onLogout, viewerLogin,
{/* Header bar */}
{headerBar}

{/* Sponsor reminder notification */}
{showSponsorReminder && (
<Box marginX={1} marginBottom={1}>
<Box borderStyle="single" borderColor="yellow" paddingX={2} paddingY={1}>
<Box flexDirection="column" alignItems="center">
<Text color="yellow">πŸ’š Thanks for using gh-manager-cli!</Text>
<Text color="gray">Your support helps craft more open-source tools</Text>
<Text color="cyan">β˜• buymeacoffee.com/wiiiimm</Text>
</Box>
</Box>
</Box>
)}
Comment on lines +1643 to +1654
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

πŸ› οΈ Refactor suggestion

Use chalk for colouring and adjust layout to account for the banner’s height

  • Colour: follow src/ui/** guideline to pre-colour with chalk in a single Text. Use full https link for clickability.
  • Layout: the banner consumes vertical rows but contentHeight doesn’t subtract it, so the list/footer can be clipped. Subtract the banner height when visible.
-      {showSponsorReminder && (
-        <Box marginX={1} marginBottom={1}>
-          <Box borderStyle="single" borderColor="yellow" paddingX={2} paddingY={1}>
-            <Box flexDirection="column" alignItems="center">
-              <Text color="yellow">πŸ’š Thanks for using gh-manager-cli!</Text>
-              <Text color="gray">Your support helps craft more open-source tools</Text>
-              <Text color="cyan">β˜• buymeacoffee.com/wiiiimm</Text>
-            </Box>
-          </Box>
-        </Box>
-      )}
+      {showSponsorReminder && (
+        <Box marginX={1} marginBottom={1}>
+          <Box borderStyle="single" borderColor="yellow" paddingX={2} paddingY={1}>
+            <Text>
+              {chalk.yellow('πŸ’š Thanks for using gh-manager-cli!')}{'\n'}
+              {chalk.gray('Your support helps craft more open‑source tools')}{'\n'}
+              {chalk.cyan('β˜• https://buymeacoffee.com/wiiiimm')}
+            </Text>
+          </Box>
+        </Box>
+      )}

Add this outside to fix height:

// after footer/header constants
const sponsorHeight = showSponsorReminder ? 4 : 0;
// subtract sponsorHeight
const contentHeight = Math.max(1, availableHeight - headerHeight - footerHeight - containerPadding - sponsorHeight);

Optional: centralise the sponsor URL/message in a constants module to avoid drift across files.

πŸ€– Prompt for AI Agents
In src/ui/views/RepoList.tsx around lines 1643-1654, the sponsor banner should
use chalk to pre-colour a single Text node and use a full https:// link, and the
UI height calculations must subtract the banner height so list/footer aren’t
clipped; update the banner to render a single Text with chalk-applied colors for
each line (no inline color props), replace the short URL with the full
https://buymeacoffee.com/wiiiimm, and after the header/footer constants add a
sponsorHeight = showSponsorReminder ? 4 : 0 then subtract sponsorHeight from
contentHeight calculation (availableHeight - headerHeight - footerHeight -
containerPadding - sponsorHeight). Optionally move the sponsor message/URL into
a shared constants module to avoid duplication.


{/* Main content container with border - fixed height */}
<Box borderStyle="single" borderColor={modalOpen ? 'gray' : 'yellow'} paddingX={1} paddingY={1} marginX={1} height={contentHeight + containerPadding + 2} flexDirection="column">
{deleteMode && deleteTarget ? (
Expand Down Expand Up @@ -2167,7 +2199,7 @@ export default function RepoList({ token, maxVisibleRows, onLogout, viewerLogin,
)}
</Box>

{/* Help footer - 4 lines */}
{/* Help footer - 5 lines */}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Footer now has 5 lines but footerHeight is still 4 β€” list area can be clipped

Update the footerHeight constant accordingly (or compute dynamically).

Outside this hunk:

-const footerHeight = 4; // Footer with border + margin (flexible height)
+const footerHeight = 5; // Footer with border + margin (matches 5 rendered lines)
πŸ“ 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
{/* Help footer - 5 lines */}
const footerHeight = 5; // Footer with border + margin (matches 5 rendered lines)
πŸ€– Prompt for AI Agents
In src/ui/views/RepoList.tsx around line 2202, the footer is rendered with 5
lines but the footerHeight constant is still set to 4, causing the list area to
be clipped; update the footerHeight to match the actual footer line count (set
to 5) or replace the hardcoded constant with a dynamic calculation (measure the
footer element or derive from computed line-height/rows) and propagate that
value to all layout calculations that use footerHeight so the list area is sized
correctly.

<Box marginTop={1} paddingX={1} flexDirection="column">
{/* Line 1: Basic navigation */}
<Box width={terminalWidth} justifyContent="center">
Expand All @@ -2193,6 +2225,12 @@ export default function RepoList({ token, maxVisibleRows, onLogout, viewerLogin,
K Cache Info β€’ W Org Switch β€’ Del/Backspace Delete β€’ Ctrl+L Logout β€’ Q Quit
</Text>
</Box>
{/* Line 5: Sponsorship */}
<Box width={terminalWidth} justifyContent="center" marginTop={1}>
<Text color="yellow" dimColor={modalOpen ? true : undefined}>
πŸ’š Support the project: buymeacoffee.com/wiiiimm
</Text>
</Box>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: Footer Height Mismatch Causes UI Issues

The help footer now renders 5 lines (including the new sponsorship line), but the footerHeight constant used for layout calculations remains at 4. This mismatch causes incorrect content area height calculations, potentially leading to UI overflow or content being cut off.

Fix in CursorΒ Fix in Web

</Box>

{/* Debug panel */}
Expand Down
Loading