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
116 changes: 63 additions & 53 deletions app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -338,38 +338,6 @@ export default function App() {
setActivePluginNavItem(itemId)
}, [])

const selectRepo = async () => {
if (switching) return

setSwitching(true)
setStatus({ type: 'info', message: 'Opening repository selector...' })

try {
const path = await window.electronAPI.selectRepo()
if (path) {
// Clear state before switching to prevent stale data mixing with new repo
setWorktrees([])
setBranches([])
setCommits([])
setPullRequests([])
setWorkingStatus(null)
setFileGraph(null)
setFileGraphLoading(false)
setRepoPath(path)
setStatus({ type: 'info', message: 'Loading repository...' })
await refresh(path)
setStatus({ type: 'success', message: 'Repository loaded' })
} else {
// User cancelled dialog - clear status
setStatus(null)
}
} catch (err) {
setStatus({ type: 'error', message: (err as Error).message })
} finally {
setSwitching(false)
}
}

const refresh = useCallback(async (repoPathForTitle: string | null = repoPath) => {
setLoading(true)
setError(null)
Expand Down Expand Up @@ -459,6 +427,60 @@ export default function App() {
}
}, [repoPath, setTitle, showCheckpoints])

const resetRepositoryViewState = useCallback(() => {
setWorktrees([])
setBranches([])
setCommits([])
setPullRequests([])
setGraphCommits([])
setStashes([])
setWorkingStatus(null)
setSelectedCommit(null)
setCommitDiff(null)
setSidebarFocus(null)
setGithubUrl(null)
setCurrentBranch('')
setError(null)
setPrError(null)
}, [])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale file graph data not cleared on repo switch

Medium Severity

resetRepositoryViewState omits setFileGraph(null) and setFileGraphLoading(false), which the old selectRepo explicitly cleared to "prevent stale data mixing with new repo." Since refresh only calls setFileGraphLoading(true) after the Phase 1 Promise.all resolves, there's a window where the new repo's branches and commits render alongside the previous repo's file graph data.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 80c5795. Configure here.


const loadRepositoryData = useCallback(async (path: string) => {
resetRepositoryViewState()
setRepoPath(path)
await refresh(path)
}, [refresh, resetRepositoryViewState])

const openRepositoryPath = useCallback(async (path: string) => {
const result = await window.conveyor.repo.openRepository(path)
if (!result.success) {
throw new Error(result.message || 'Failed to open repository')
}
await loadRepositoryData(path)
}, [loadRepositoryData])

const selectRepo = async () => {
if (switching) return

setSwitching(true)
setStatus({ type: 'info', message: 'Opening repository selector...' })

try {
const path = await window.electronAPI.selectRepo()
if (path) {
setStatus({ type: 'info', message: 'Loading repository...' })
await loadRepositoryData(path)
setStatus({ type: 'success', message: 'Repository loaded' })
} else {
// User cancelled dialog - clear status
setStatus(null)
}
} catch (err) {
setStatus({ type: 'error', message: (err as Error).message })
} finally {
setSwitching(false)
}
}

// Keep the repository store in sync so plugin apps/panels can rely on it.
// (This avoids a full migration of App state back to Zustand while restoring the plugin UI.)
useEffect(() => {
Expand Down Expand Up @@ -1416,7 +1438,7 @@ export default function App() {
/>
)
}

// Sidebar detail panel for branches, worktrees, stashes
if (sidebarFocus) {
return (
Expand Down Expand Up @@ -1448,8 +1470,7 @@ export default function App() {
if (repo.isCurrent) return
setStatus({ type: 'info', message: `Opening ${repo.name}...` })
try {
setRepoPath(repo.path)
await refresh(repo.path)
await openRepositoryPath(repo.path)
setStatus({ type: 'success', message: `Opened ${repo.name}` })
} catch (err) {
setStatus({ type: 'error', message: (err as Error).message })
Expand Down Expand Up @@ -1502,6 +1523,7 @@ export default function App() {
formatRelativeTime, formatDate, handlePRCheckout, handleBranchDoubleClick,
handleRemoteBranchDoubleClick, handleWorktreeDoubleClick, handleDeleteBranch, handleRenameBranch,
handleDeleteRemoteBranch, branches, repoPath, worktrees, pullRequests, handleSidebarFocus,
openRepositoryPath,
selectedCommit, loadingDiff, commitDiff, handleCommitCheckout
])

Expand Down Expand Up @@ -1535,8 +1557,7 @@ export default function App() {
// Switch to this repo
setStatus({ type: 'info', message: `Opening ${repo.name}...` })
try {
setRepoPath(repo.path)
await refresh(repo.path)
await openRepositoryPath(repo.path)
setStatus({ type: 'success', message: `Opened ${repo.name}` })
} catch (err) {
setStatus({ type: 'error', message: (err as Error).message })
Expand Down Expand Up @@ -1583,7 +1604,8 @@ export default function App() {
}), [
formatRelativeTime, formatDate, handleRadarItemClick, handleRadarPRClick, handleRadarBranchClick,
handleRadarWorktreeClick, handleRadarStashClick, handleContextMenu, handleSelectCommit, navigateToEditor,
renderEditorContent, setActiveCanvas, workingStatus, handleRadarUncommittedClick, setStatus, refresh, branches, handleSidebarFocus
renderEditorContent, setActiveCanvas, workingStatus, handleRadarUncommittedClick, setStatus, branches, handleSidebarFocus,
openRepositoryPath
])

const canvasUIState: CanvasUIState = useMemo(() => ({
Expand Down Expand Up @@ -1687,22 +1709,10 @@ export default function App() {
currentPath={repoPath}
onRepoChange={(path) => {
if (path === repoPath) return
// Clear state before switching to prevent stale data mixing with new repo
setWorktrees([])
setBranches([])
setCommits([])
setPullRequests([])
setGraphCommits([])
setStashes([])
setWorkingStatus(null)
setSelectedCommit(null)
setCommitDiff(null)
setSidebarFocus(null)
setError(null)
setPrError(null)
setRepoPath(path)
setStatus({ type: 'info', message: 'Switching repository...' })
refresh(path)
loadRepositoryData(path).catch((err) => {
setStatus({ type: 'error', message: (err as Error).message })
})
}}
/>
)}
Expand Down
58 changes: 53 additions & 5 deletions app/components/SettingsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ export const SettingsPanel = ({ themeMode: _themeMode, onThemeChange, onBack }:
reorderColumns,
addColumn,
removeColumn,
setColumnPanel,
updateColumn,
} = useCanvas()
const [newCanvasName, setNewCanvasName] = useState('')
const [selectedCanvasId, setSelectedCanvasId] = useState<string | null>(null)
Expand Down Expand Up @@ -257,6 +257,10 @@ export const SettingsPanel = ({ themeMode: _themeMode, onThemeChange, onBack }:
// Group themes by category
const categories = [...new Set(BUILT_IN_THEMES.map((t) => t.category))]

const getPanelLabel = (slotType: SlotType, panel: PanelType) => {
return PANEL_OPTIONS[slotType].find((option) => option.value === panel)?.label || panel
}

// Canvas handlers
const handleCreateCanvas = () => {
if (!newCanvasName.trim()) return
Expand Down Expand Up @@ -325,7 +329,7 @@ export const SettingsPanel = ({ themeMode: _themeMode, onThemeChange, onBack }:

const slotType = newColumnSlotType
const panel = DEFAULT_PANELS[slotType]
const panelLabel = PANEL_OPTIONS[slotType].find(p => p.value === panel)?.label || panel
const panelLabel = getPanelLabel(slotType, panel)

addColumn(canvasId, {
id: `col-${Date.now()}`,
Expand All @@ -345,7 +349,32 @@ export const SettingsPanel = ({ themeMode: _themeMode, onThemeChange, onBack }:
}

const handleChangePanel = (canvasId: string, columnId: string, panel: PanelType) => {
setColumnPanel(canvasId, columnId, panel)
const canvas = canvasState.canvases.find((c) => c.id === canvasId)
const column = canvas?.columns.find((col) => col.id === columnId)
if (!column) return

const updates: Partial<Column> = { panel }

if (!canvas?.isPreset) {
const currentLabel = column.label?.trim()
const previousDefaultLabel = getPanelLabel(column.slotType, column.panel)

if (!currentLabel || currentLabel === previousDefaultLabel) {
updates.label = getPanelLabel(column.slotType, panel)
}
}

updateColumn(canvasId, columnId, updates)
}

const handleColumnLabelChange = (canvasId: string, columnId: string, label: string) => {
updateColumn(canvasId, columnId, { label })
}

const handleColumnLabelBlur = (canvasId: string, column: Column) => {
const trimmedLabel = column.label?.trim()
if (trimmedLabel === column.label) return
updateColumn(canvasId, column.id, { label: trimmedLabel || undefined })
}

// Drag-drop handlers for column reordering
Expand Down Expand Up @@ -493,13 +522,20 @@ export const SettingsPanel = ({ themeMode: _themeMode, onThemeChange, onBack }:
{canvas.columns.map((column, index) => {
const isDragging = draggingColumn?.canvasId === canvas.id && draggingColumn?.index === index
const isDragOver = draggingColumn?.canvasId === canvas.id && dragOverIndex === index
const defaultLabel = getPanelLabel(column.slotType, column.panel)

return (
<div
key={column.id}
className={`canvas-column-item ${isDragging ? 'dragging' : ''} ${isDragOver ? 'drag-over' : ''}`}
draggable
onDragStart={() => handleDragStart(canvas.id, index)}
onDragStart={(e) => {
if ((e.target as HTMLElement).closest('input, select, button')) {
e.preventDefault()
return
}
handleDragStart(canvas.id, index)
}}
onDragOver={(e) => handleDragOver(e, index)}
onDragEnd={handleDragEnd}
onDragLeave={handleDragLeave}
Expand All @@ -517,7 +553,19 @@ export const SettingsPanel = ({ themeMode: _themeMode, onThemeChange, onBack }:
</div>

<span className="canvas-column-icon">{column.icon || SLOT_ICONS[column.slotType]}</span>
<span className="canvas-column-label">{column.label || column.id}</span>
{canvas.isPreset ? (
<span className="canvas-column-label">{column.label || column.id}</span>
) : (
<input
type="text"
className="canvas-column-label-input"
value={column.label || ''}
placeholder={defaultLabel}
onChange={(e) => handleColumnLabelChange(canvas.id, column.id, e.target.value)}
onBlur={() => handleColumnLabelBlur(canvas.id, column)}
onClick={(e) => e.stopPropagation()}
/>
)}

{/* Panel dropdown */}
<select
Expand Down
4 changes: 2 additions & 2 deletions app/components/plugins/PluginSettingsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
Sliders,
Plus,
Trash2,
Github,
Globe2,
Link,
Package,
Loader2,
Expand Down Expand Up @@ -299,7 +299,7 @@ export function PluginSettingsPanel() {
onClick={() => setInstallSource('github')}
disabled={isInstalling}
>
<Github size={14} />
<Globe2 size={14} />
GitHub
</button>
<button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
Trash2,
RefreshCw,
ExternalLink,
Github,
Globe2,
FolderGit2,
ChevronRight,
Globe,
Expand Down Expand Up @@ -258,7 +258,7 @@ export function RepositoryManagerPanel({ context, onClose }: PluginPanelProps) {
// Local repos show provider icon
switch (repo.provider.toLowerCase()) {
case 'github':
return <Github size={14} />
return <Globe2 size={14} />
default:
return <FolderGit2 size={14} />
}
Expand Down
22 changes: 22 additions & 0 deletions app/styles/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -7352,6 +7352,28 @@ body,
white-space: nowrap;
}

.canvas-column-label-input {
flex: 1;
min-width: 0;
padding: 4px 8px;
border: 1px solid var(--border-subtle);
border-radius: 4px;
background: var(--bg-secondary);
color: var(--text-primary);
font-size: 12px;
font-weight: 500;
}

.canvas-column-label-input:focus {
outline: none;
border-color: var(--accent);
}

.canvas-column-label-input::placeholder {
color: var(--text-muted);
font-weight: 400;
}

.canvas-column-hidden {
font-size: 10px;
padding: 1px 4px;
Expand Down
Loading