feat: add in-app sponsorship prompts and reminders#28
Conversation
- Show sponsorship message when TUI exits normally - Display Buy Me a Coffee link - Add wiiiimm.codes site link - Invite users to leave feedback on GitHub - Only shows on normal exit, not for --help or --version
- Add sponsor section to InfoModal (yellow border box) - Add sponsor line to help footer - Add periodic sponsor reminder after every 5 operations - Reminder auto-hides after 5 seconds - Shows buymeacoffee.com/wiiiimm link
WalkthroughAdds sponsorship messaging across CLI and UI: a console banner on normal interactive exits, a donation block in InfoModal, and a reminder system in RepoList that shows a banner after every five successful archive/delete operations, auto-hiding after 5 seconds. Help footer gains a sponsorship line. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant User
participant CLI as CLI Process
participant Exit as process.on('exit')
participant Msg as showSponsorshipMessage
User->>CLI: Run command
CLI-->>Exit: Exit event (code)
Exit->>Exit: Compute isNormalExit (code===0)<br/>and isInteractiveSession (!--version/--help)
alt Normal interactive exit
Exit->>Msg: showSponsorshipMessage()
Msg-->>Exit: Printed banner
else Non-zero or non-interactive
Exit-->>Exit: Skip banner
end
Exit-->>CLI: Continue existing shutdown/logging
sequenceDiagram
autonumber
participant User
participant UI as RepoList
participant Ops as Archive/Delete Ops
participant Tracker as trackSuccessfulOperation
participant Banner as Sponsor Reminder
User->>UI: Archive/Delete action
UI->>Ops: Perform operation
Ops-->>UI: Success
UI->>Tracker: Increment operationCount
alt Every 5th success
Tracker->>UI: set showSponsorReminder = true
UI->>Banner: Render banner (header & main)
Note over UI,Banner: Auto-hide after 5 seconds
else Not a 5th success
Tracker-->>UI: No banner
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
✨ Finishing Touches
🧪 Generate unit tests
Comment |
| setShowSponsorReminder(true); | ||
| // Hide the reminder after 5 seconds | ||
| setTimeout(() => setShowSponsorReminder(false), 5000); | ||
| } |
There was a problem hiding this comment.
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.
| <Text color="yellow" dimColor={modalOpen ? true : undefined}> | ||
| 💚 Support the project: buymeacoffee.com/wiiiimm | ||
| </Text> | ||
| </Box> |
There was a problem hiding this comment.
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.
# [1.34.0](v1.33.0...v1.34.0) (2025-09-06) ### Features * add in-app sponsorship prompts and reminders ([#28](#28)) ([443aa4b](443aa4b))
|
🎉 This PR is included in version 1.34.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
src/ui/components/modals/InfoModal.tsx (1)
54-58: Prefer chalk-coloured single Text and a fully qualified URLAvoid Ink colour props in src/ui/**; pre-colour with chalk and render inside a single Text. Also use https so terminals detect a clickable link.
- <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> + <Box flexDirection="column" borderStyle="single" borderColor="yellow" paddingX={1} marginY={1}> + <Text> + {chalk.yellow('💚 Enjoying gh-manager-cli?')} + {'\n'} + {chalk.gray('Support the project at:')} + {'\n'} + {chalk.cyan('https://buymeacoffee.com/wiiiimm')} + </Text> + </Box>src/index.tsx (1)
102-113: Gate the exit banner to interactive TTY sessions and allow opt-outAvoid printing in CI/non-TTY contexts and provide an env opt-out. Also safer to check isTTY in addition to flags.
- const isNormalExit = code === 0; - const isInteractiveSession = !argv.includes('--version') && - !argv.includes('-v') && - !argv.includes('--help') && - !argv.includes('-h'); - - if (isNormalExit && isInteractiveSession) { + const isNormalExit = code === 0; + const isInteractiveSession = !argv.includes('--version') && + !argv.includes('-v') && + !argv.includes('--help') && + !argv.includes('-h'); + const isTTY = Boolean(process.stdout && process.stdout.isTTY); + const sponsorOptOut = process.env.GH_MANAGER_NO_SPONSOR === '1' || process.env.CI === 'true'; + + if (isNormalExit && isInteractiveSession && isTTY && !sponsorOptOut) { showSponsorshipMessage(); }Optional: prefer process.stdout.write over console.log inside the exit handler to minimise buffering risk.
src/ui/views/RepoList.tsx (1)
2229-2233: Use chalk and a fully qualified link for the sponsorship footer linePre-colour with chalk and include https for better terminal linkifying.
- <Text color="yellow" dimColor={modalOpen ? true : undefined}> - 💚 Support the project: buymeacoffee.com/wiiiimm - </Text> + <Text dimColor={modalOpen ? true : undefined}> + {chalk.yellow('💚 Support the project: https://buymeacoffee.com/wiiiimm')} + </Text>
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/index.tsx(1 hunks)src/ui/components/modals/InfoModal.tsx(1 hunks)src/ui/views/RepoList.tsx(7 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/InfoModal.tsxsrc/ui/views/RepoList.tsxsrc/index.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/InfoModal.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/InfoModal.tsxsrc/ui/views/RepoList.tsxsrc/index.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/ui/**/*.tsx : Use <Box minHeight={...}> spacers for consistent terminal spacing
Applied to files:
src/ui/views/RepoList.tsx
⏰ 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 (4)
src/index.tsx (1)
85-97: LGTM: clear, friendly exit banner functionFunction is self-contained and readable.
src/ui/views/RepoList.tsx (3)
96-99: LGTM: state for sponsor reminderLooks good and typed properly.
321-321: Correct placement of tracking callCalled only after successful archive/unarchive updates; that’s the right spot.
532-532: Correct placement of tracking callCalled only after successful delete; that’s the right spot.
| // 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); | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
🛠️ 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.
| {/* 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> | ||
| )} |
There was a problem hiding this comment.
🛠️ 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.
| </Box> | ||
|
|
||
| {/* Help footer - 4 lines */} | ||
| {/* Help footer - 5 lines */} |
There was a problem hiding this comment.
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.
| {/* 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.
Summary
This PR adds sponsorship prompts throughout the gh-manager-cli app to encourage users to support the project.
Features Added
1. Post-Exit Sponsor Message
2. Info Modal Sponsor Section
3. Help Footer Sponsor Line
4. Periodic Sponsor Reminder
Testing
Implementation Details
Summary by CodeRabbit