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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ On first run, you'll be prompted to authenticate with GitHub (OAuth recommended)
- **Repository Actions**:
- View detailed info (`I`) - Shows repository metadata, language, size, and timestamps
- Open in browser (Enter/`O`)
- Copy repository URLs (`C`) - Interactive modal to copy SSH or HTTPS clone URLs with keyboard navigation
- Delete repository (`Del` or `Backspace`) with secure two-step confirmation
- Archive/unarchive repositories (`Ctrl+A`) with confirmation prompts
- Rename repository (`Ctrl+R`) with real-time validation
Expand Down Expand Up @@ -258,6 +259,11 @@ Launch the app, then use the keys below:
### Repository Actions
- **Repository info**: `I` to view detailed metadata (size, language, timestamps)
- **Cache info**: `K` to inspect Apollo cache status
- **Copy URLs**: `C` to open interactive modal for copying SSH or HTTPS clone URLs
- SSH selected by default with visual indicators (▶)
- Up/Down arrows to select between SSH and HTTPS
- Enter to copy selected URL, or use `S`/`H` shortcuts
- Multiple close options: Esc, Q, C
- **Archive/Unarchive**: `Ctrl+A` with confirmation prompt
- **Rename repository**: `Ctrl+R` to rename (with real-time validation)
- Enter new name → confirm (Enter)
Expand Down Expand Up @@ -429,6 +435,7 @@ Recently implemented:
- ✅ Density toggle for row spacing (compact/cozy/comfy)
- ✅ Repo actions (archive/unarchive, delete, rename, change visibility) with confirmations
- ✅ Repository renaming with real-time validation (`Ctrl+R`)
- ✅ Repository URL copying modal (`C`) with interactive SSH/HTTPS selection
- ✅ Organization support and switching (press `W`) with enterprise detection
- ✅ Enhanced server-side search with improved UX and organization context support
- ✅ Smart infinite scroll with 80% prefetch trigger
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"@octokit/graphql": "^9.0.1",
"apollo3-cache-persist": "^0.14.1",
"chalk": "^5.6.0",
"clipboardy": "^4.0.0",
"dotenv": "^17.2.1",
"env-paths": "^3.0.0",
"graphql": "^16.11.0",
Expand Down
27 changes: 27 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

104 changes: 100 additions & 4 deletions src/ui/RepoList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import type { RepoNode, RateLimitInfo } from '../types';
import { exec } from 'child_process';
import OrgSwitcher from './OrgSwitcher';
import { logger } from '../logger';
import { DeleteModal, ArchiveModal, SyncModal, InfoModal, LogoutModal, VisibilityModal, SortModal, ChangeVisibilityModal, RenameModal } from './components/modals';
import { DeleteModal, ArchiveModal, SyncModal, InfoModal, LogoutModal, VisibilityModal, SortModal, ChangeVisibilityModal, RenameModal, CopyUrlModal } from './components/modals';
import { RepoRow, FilterInput, RepoListHeader } from './components/repo';
import { SlowSpinner } from './components/common';
import { truncate, formatDate } from '../utils';
import { truncate, formatDate, copyToClipboard } from '../utils';

// Allow customizable repos per fetch via env var (1-50, default 15)
const getPageSize = () => {
Expand Down Expand Up @@ -148,6 +148,22 @@ export default function RepoList({ token, maxVisibleRows, onLogout, viewerLogin,
// Sort modal state
const [sortMode, setSortMode] = useState(false);

// Copy URL modal state
const [copyUrlMode, setCopyUrlMode] = useState(false);
const [copyUrlTarget, setCopyUrlTarget] = useState<RepoNode | null>(null);
const [copyToast, setCopyToast] = useState<string | null>(null);
const copyToastTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

// Cleanup toast timer on unmount
useEffect(() => {
return () => {
if (copyToastTimerRef.current) {
clearTimeout(copyToastTimerRef.current);
copyToastTimerRef.current = null;
}
};
}, []);

// Apply initial --org flag once (if provided)
const appliedInitialOrg = useRef(false);
useEffect(() => {
Expand Down Expand Up @@ -208,6 +224,54 @@ export default function RepoList({ token, maxVisibleRows, onLogout, viewerLogin,
setSyncFocus('confirm');
setSyncTrigger(false);
}

function openCopyUrlModal(repo: RepoNode) {
setCopyUrlMode(true);
setCopyUrlTarget(repo);
setCopyToast(null);
}

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
}
}
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: Copy URL Notifications Inconsistent

The new "Copy URL" feature has a few issues with its toast notifications. Copy failures display duplicate error messages (toast and modal). Success toasts are immediately cleared when the modal closes, and rapid copying can cause a race condition where older timeouts prematurely clear newer toasts, leading to inconsistent user feedback.

Fix in Cursor Fix in Web


// Single sync execution function to prevent duplicate operations
async function executeSync() {
Expand Down Expand Up @@ -998,6 +1062,11 @@ export default function RepoList({ token, maxVisibleRows, onLogout, viewerLogin,
return; // SortModal component handles its own keyboard input
}

// When copy URL modal is open, trap inputs for modal
if (copyUrlMode) {
return; // CopyUrlModal component handles its own keyboard input
}

// When in filter mode, only handle input for the TextInput
if (filterMode) {
if (key.escape) {
Expand Down Expand Up @@ -1193,6 +1262,15 @@ export default function RepoList({ token, maxVisibleRows, onLogout, viewerLogin,
setInfoMode(true);
return;
}

// Copy repository URL modal (C)
if (input && input.toUpperCase() === 'C') {
const repo = visibleItems[cursor];
if (repo) {
openCopyUrlModal(repo);
}
return;
}

// Organization switcher (W for Workspace/Who)
if (input && input.toUpperCase() === 'W') {
Expand Down Expand Up @@ -1397,7 +1475,7 @@ export default function RepoList({ token, maxVisibleRows, onLogout, viewerLogin,
}

const lowRate = rateLimit && rateLimit.remaining <= Math.ceil(rateLimit.limit * 0.1);
const modalOpen = deleteMode || archiveMode || syncMode || logoutMode || infoMode || visibilityMode || renameMode;
const modalOpen = deleteMode || archiveMode || syncMode || logoutMode || infoMode || visibilityMode || renameMode || sortMode || changeVisibilityMode || copyUrlMode;

// Memoize header to prevent re-renders - must be before any returns
const headerBar = useMemo(() => (
Expand Down Expand Up @@ -1936,6 +2014,15 @@ export default function RepoList({ token, maxVisibleRows, onLogout, viewerLogin,
error={changeVisibilityError}
/>
</Box>
) : copyUrlMode ? (
<Box height={contentHeight} alignItems="center" justifyContent="center">
<CopyUrlModal
repo={copyUrlTarget}
terminalWidth={terminalWidth}
onClose={closeCopyUrlModal}
onCopy={handleCopyUrl}
/>
</Box>
) : (
<>
{/* Context/Filter/sort status */}
Expand Down Expand Up @@ -2061,7 +2148,7 @@ export default function RepoList({ token, maxVisibleRows, onLogout, viewerLogin,
{/* Line 3: Action controls */}
<Box width={terminalWidth} justifyContent="center">
<Text color="gray" dimColor={modalOpen ? true : undefined}>
I Info • K Cache Info • Ctrl+R Rename • Ctrl+A Un/Archive • Ctrl+V Change Visibility • Del/Backspace Delete • Ctrl+S Sync Fork
I Info • C Copy URL • K Cache Info • Ctrl+R Rename • Ctrl+A Un/Archive • Ctrl+V Change Visibility • Del/Backspace Delete • Ctrl+S Sync Fork
</Text>
</Box>
</Box>
Expand All @@ -2079,6 +2166,15 @@ export default function RepoList({ token, maxVisibleRows, onLogout, viewerLogin,
)}
</Box>
)}

{/* Copy toast notification */}
{copyToast && (
<Box marginTop={1} justifyContent="center">
<Box borderStyle="round" borderColor={copyToast.includes('Failed') ? 'red' : 'green'} paddingX={2} paddingY={0}>
<Text color={copyToast.includes('Failed') ? 'red' : 'green'}>{copyToast}</Text>
</Box>
</Box>
)}
</Box>
);
}
Loading