chore: remove dead code (59 unused files + ~125 unused exports) (#1835)

* chore: remove dead/unreferenced files across ui, vscode

Remove 59 unused source files (components, hooks, lib utils, stores,
barrels, and orphaned vscode github modules) that are not imported by
any entry-reachable code. Also drop a stale test mock for the removed
execCommands module.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* refactor: remove unused exported symbols (types, functions, consts, hooks)

Remove exported symbols whose identifier is referenced nowhere in the
repository (verified via repo-wide search), across ui types/contracts,
lib utilities, sync layer, stores, and components. Also drop the few
imports/private helpers orphaned by these removals.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* refactor: remove more unused exports (desktop, shortcuts, worktree, vscode)

Continue removing repo-wide unreferenced exported functions, consts and
types across lib/desktop, shortcuts, worktreeSessionCreator, sync, and
vscode gitService, with cascading orphaned helpers/imports cleaned up.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* chore: add dead-code cleanup tooling

* refactor: checkpoint dead-code cleanup

* refactor: remove dead-code suppressions

---------

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
Serhii Dziupin
2026-06-26 19:27:53 +03:00
committed by GitHub
co-authored by Serhii Dziupin Bohdan Triapitsyn
parent 4a37b9a005
commit 00821700de
324 changed files with 444 additions and 14876 deletions
@@ -1,575 +0,0 @@
import * as React from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { toast } from '@/components/ui';
import { Icon } from "@/components/icon/Icon";
import { cn } from '@/lib/utils';
import { deleteGitBranch, getGitBranches, git, renameBranch } from '@/lib/gitApi';
import type { GitBranch, GitWorktreeInfo } from '@/lib/api/types';
import type { WorktreeMetadata } from '@/types/worktree';
import { createWorktreeWithDefaults } from '@/lib/worktrees/worktreeCreate';
import { getRootBranch } from '@/lib/worktrees/worktreeStatus';
import { getWorktreeSetupCommands } from '@/lib/openchamberConfig';
import { sessionEvents } from '@/lib/sessionEvents';
import { useSessions } from '@/sync/sync-context';
import { useI18n } from '@/lib/i18n';
export interface BranchPickerProject {
id: string;
path: string;
normalizedPath: string;
label?: string;
}
interface BranchPickerDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
project: BranchPickerProject | null;
}
const displayProjectName = (project: BranchPickerProject): string =>
project.label || project.normalizedPath.split('/').pop() || project.normalizedPath;
const normalizeBranchName = (value: string | null | undefined): string => {
return String(value || '')
.trim()
.replace(/^refs\/heads\//, '')
.replace(/^heads\//, '')
.replace(/^remotes\//, '');
};
const normalizePath = (value: string | null | undefined): string => {
const raw = String(value || '').trim().replace(/\\/g, '/');
if (!raw) {
return '';
}
if (raw === '/') {
return '/';
}
return raw.length > 1 ? raw.replace(/\/+$/, '') : raw;
};
export function BranchPickerDialog({ open, onOpenChange, project }: BranchPickerDialogProps) {
const { t } = useI18n();
const sessions = useSessions();
const [searchQuery, setSearchQuery] = React.useState('');
const [branches, setBranches] = React.useState<GitBranch | null>(null);
const [worktrees, setWorktrees] = React.useState<GitWorktreeInfo[]>([]);
const [rootBranchName, setRootBranchName] = React.useState<string | null>(null);
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState<string | null>(null);
const [creatingWorktreeBranch, setCreatingWorktreeBranch] = React.useState<string | null>(null);
const [deletingBranch, setDeletingBranch] = React.useState<string | null>(null);
const [confirmingDelete, setConfirmingDelete] = React.useState<string | null>(null);
const [forceDeleteBranch, setForceDeleteBranch] = React.useState<string | null>(null);
const [editingBranch, setEditingBranch] = React.useState<string | null>(null);
const [editValue, setEditValue] = React.useState('');
const [renamingBranchKey, setRenamingBranchKey] = React.useState<string | null>(null);
const refresh = React.useCallback(async () => {
if (!project) return;
setLoading(true);
setError(null);
try {
const [b, w, rootBranch] = await Promise.all([
getGitBranches(project.path),
git.worktree.list(project.path),
getRootBranch(project.path).catch(() => null),
]);
setBranches(b);
setWorktrees(w);
setRootBranchName(rootBranch);
} catch (err) {
setError(err instanceof Error ? err.message : t('branchPickerDialog.error.failedToLoad'));
setBranches(null);
setWorktrees([]);
setRootBranchName(null);
} finally {
setLoading(false);
}
}, [project, t]);
React.useEffect(() => {
if (!open) {
setSearchQuery('');
setConfirmingDelete(null);
setForceDeleteBranch(null);
setEditingBranch(null);
setEditValue('');
setRenamingBranchKey(null);
setCreatingWorktreeBranch(null);
return;
}
void refresh();
}, [open, refresh]);
const filterBranches = (list: string[], query: string): string[] => {
if (!query.trim()) return list;
const lower = query.toLowerCase();
return list.filter((b) => b.toLowerCase().includes(lower));
};
const beginRename = React.useCallback((branchName: string) => {
setEditingBranch(branchName);
setEditValue(branchName);
}, []);
const cancelRename = React.useCallback(() => {
setEditingBranch(null);
setEditValue('');
setRenamingBranchKey(null);
}, []);
const cancelDelete = React.useCallback(() => {
setConfirmingDelete(null);
setForceDeleteBranch(null);
}, []);
const commitRename = React.useCallback(async (oldName: string) => {
if (!project) return;
const newName = editValue.trim();
if (!newName || newName === oldName) {
cancelRename();
return;
}
setRenamingBranchKey(oldName);
try {
const result = await renameBranch(project.path, oldName, newName);
if (!result?.success) {
throw new Error(t('branchPickerDialog.error.renameRejected'));
}
await refresh();
cancelRename();
toast.success(t('branchPickerDialog.toast.branchRenamed'), { description: `${oldName} -> ${newName}` });
} catch (err) {
toast.error(t('branchPickerDialog.toast.failedToRenameBranch'), {
description: err instanceof Error ? err.message : t('branchPickerDialog.error.renameFailed'),
});
setRenamingBranchKey(null);
}
}, [project, editValue, refresh, cancelRename, t]);
const handleDeleteBranch = React.useCallback(async (branchName: string) => {
if (!project) return;
setDeletingBranch(branchName);
try {
const force = forceDeleteBranch === branchName;
const result = await deleteGitBranch(project.path, { branch: branchName, force });
if (!result?.success) {
throw new Error(t('branchPickerDialog.error.deleteRejected'));
}
await refresh();
toast.success(t('branchPickerDialog.toast.branchDeleted'), { description: branchName });
setConfirmingDelete(null);
setForceDeleteBranch(null);
} catch (err) {
const message = err instanceof Error ? err.message : t('branchPickerDialog.error.deleteFailed');
// If branch isn't merged, prompt for force delete on next confirm.
if (/not fully merged/i.test(message) && forceDeleteBranch !== branchName) {
setForceDeleteBranch(branchName);
toast.error(t('branchPickerDialog.toast.branchNotMerged'), {
description: t('branchPickerDialog.toast.confirmAgainToForceDelete'),
});
} else {
toast.error(t('branchPickerDialog.toast.failedToDeleteBranch'), { description: message });
}
} finally {
setDeletingBranch(null);
}
}, [project, refresh, forceDeleteBranch, t]);
const handleCreateWorktreeForBranch = React.useCallback(async (branchName: string) => {
if (!project) {
return;
}
setCreatingWorktreeBranch(branchName);
try {
const setupCommands = await getWorktreeSetupCommands({
id: project.id,
path: project.path,
});
await createWorktreeWithDefaults(
{
id: project.id,
path: project.path,
},
{
preferredName: branchName,
mode: 'existing',
existingBranch: branchName,
branchName,
worktreeName: branchName,
setupCommands,
}
);
await refresh();
toast.success(t('branchPickerDialog.toast.worktreeCreated'), { description: branchName });
} catch (err) {
toast.error(t('branchPickerDialog.toast.failedToCreateWorktree'), {
description: err instanceof Error ? err.message : t('branchPickerDialog.error.createWorktreeFailed'),
});
} finally {
setCreatingWorktreeBranch(null);
}
}, [project, refresh, t]);
const handleRemoveWorktree = React.useCallback((worktree: GitWorktreeInfo | null) => {
if (!project || !worktree) {
return;
}
const normalizedWorktreePath = normalizePath(worktree.path);
const directSessions = sessions.filter((session) => {
const sessionPath = normalizePath(session.directory ?? null);
return Boolean(sessionPath) && sessionPath === normalizedWorktreePath;
});
const directSessionIds = new Set(directSessions.map((session) => session.id));
const findSubsessions = (parentIds: Set<string>): typeof sessions => {
const subsessions = sessions.filter((session) => {
const parentID = (session as { parentID?: string | null }).parentID;
if (!parentID) {
return false;
}
return parentIds.has(parentID);
});
if (subsessions.length === 0) {
return [];
}
const subsessionIds = new Set(subsessions.map((session) => session.id));
return [...subsessions, ...findSubsessions(subsessionIds)];
};
const allSubsessions = findSubsessions(directSessionIds);
const seenIds = new Set<string>();
const allSessions = [...directSessions, ...allSubsessions].filter((session) => {
if (seenIds.has(session.id)) {
return false;
}
seenIds.add(session.id);
return true;
});
const normalizedBranch = normalizeBranchName(worktree.branch);
const worktreeMetadata: WorktreeMetadata = {
source: 'sdk',
name: worktree.name,
path: worktree.path,
projectDirectory: project.path,
branch: normalizedBranch,
label: normalizedBranch || worktree.name,
};
sessionEvents.requestDelete({
sessions: allSessions,
mode: 'worktree',
worktree: worktreeMetadata,
});
}, [project, sessions]);
const worktreeByBranch = new Map<string, GitWorktreeInfo>();
for (const worktree of worktrees) {
const branchName = normalizeBranchName(worktree.branch);
if (branchName && !worktreeByBranch.has(branchName)) {
worktreeByBranch.set(branchName, worktree);
}
}
const normalizedRootBranch = normalizeBranchName(rootBranchName);
const allBranches = branches?.all || [];
const filteredBranches = filterBranches(allBranches, searchQuery);
const localBranches = filteredBranches.filter((b) => !b.startsWith('remotes/'));
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl max-h-[70vh] flex flex-col overflow-hidden gap-3">
<DialogHeader className="flex-shrink-0">
<DialogTitle className="flex items-center gap-2">
<Icon name="git-branch" className="h-5 w-5" />
{t('branchPickerDialog.title')}
</DialogTitle>
<DialogDescription>
{project ? t('branchPickerDialog.description.localBranchesForProject', { project: displayProjectName(project) }) : t('branchPickerDialog.description.selectProject')}
</DialogDescription>
</DialogHeader>
<div className="relative flex-shrink-0">
<Icon name="search" className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder={t('branchPickerDialog.search.placeholder')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9"
/>
</div>
<div className="flex-1 min-h-0 overflow-y-auto">
<div className="space-y-1">
{!project ? (
<div className="text-center py-8 text-muted-foreground">{t('branchPickerDialog.state.noProjectSelected')}</div>
) : loading ? (
<div className="px-2 py-2 text-muted-foreground text-sm">{t('branchPickerDialog.state.loadingBranches')}</div>
) : error ? (
<div className="px-2 py-2 text-destructive text-sm">{error}</div>
) : localBranches.length === 0 ? (
<div className="px-2 py-2 text-muted-foreground text-sm">
{searchQuery ? t('branchPickerDialog.state.noMatchingBranches') : t('branchPickerDialog.state.noBranchesFound')}
</div>
) : (
localBranches.map((branchName) => {
const details = branches?.branches[branchName];
const normalizedBranchName = normalizeBranchName(branchName);
const isCurrent = Boolean(details?.current);
const isDeleting = deletingBranch === branchName;
const isRenaming = renamingBranchKey === branchName;
const attachedWorktree = worktreeByBranch.get(normalizedBranchName) ?? null;
const hasAttachedWorktree = Boolean(attachedWorktree);
const isProjectRootBranch = Boolean(
normalizedBranchName &&
normalizedRootBranch &&
normalizedBranchName === normalizedRootBranch
);
const isEditing = editingBranch === branchName;
const isConfirming = confirmingDelete === branchName;
const isForceDelete = forceDeleteBranch === branchName;
const isCreatingWorktree = creatingWorktreeBranch === branchName;
const disableCreateWorktree = Boolean(
hasAttachedWorktree || isCreatingWorktree || isDeleting || isRenaming || isEditing
);
const disableDelete = Boolean(
isCurrent || isDeleting || isRenaming || isEditing || isCreatingWorktree || isProjectRootBranch
);
const disableRename = Boolean(
isDeleting || isRenaming || isEditing || isCreatingWorktree || isProjectRootBranch
);
const disableWorktreeDelete = Boolean(
isDeleting || isRenaming || isEditing || isCreatingWorktree || isProjectRootBranch || !attachedWorktree
);
return (
<div
key={branchName}
className="flex items-center gap-2 px-2.5 py-1.5 hover:bg-interactive-hover/30 rounded-md overflow-hidden"
>
<Icon name="git-branch" className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<div className="flex-1 min-w-0 overflow-hidden">
<div className="flex items-center gap-1.5 min-w-0">
{isEditing ? (
<form
className="flex w-full items-center min-w-0"
onSubmit={(event) => {
event.preventDefault();
void commitRename(branchName);
}}
>
<input
value={editValue}
onChange={(event) => setEditValue(event.target.value)}
className="flex-1 min-w-0 h-5 bg-transparent text-sm leading-none outline-none placeholder:text-muted-foreground"
autoFocus
placeholder={t('branchPickerDialog.search.renameBranchPlaceholder')}
onKeyDown={(event) => {
if (event.key === 'Escape') {
event.preventDefault();
cancelRename();
}
if (event.key === 'Enter') {
event.preventDefault();
void commitRename(branchName);
}
}}
/>
</form>
) : (
<span className={cn('text-sm truncate', isCurrent && 'font-medium text-primary')}>
{branchName}
</span>
)}
{isCurrent && (
<span className="text-xs bg-primary/10 text-primary px-1.5 py-0.5 rounded flex-shrink-0 whitespace-nowrap">
{t('branchPickerDialog.badge.head')}
</span>
)}
{hasAttachedWorktree && !isEditing && (
<span className="text-xs bg-muted/40 text-muted-foreground px-1.5 py-0.5 rounded flex-shrink-0 whitespace-nowrap">
{t('branchPickerDialog.badge.worktree')}
</span>
)}
</div>
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
{details?.commit ? (
<span className="font-mono">{details.commit.slice(0, 7)}</span>
) : null}
{typeof details?.ahead === 'number' && details.ahead > 0 ? (
<span className="text-[color:var(--status-success)]">{details.ahead}</span>
) : null}
{typeof details?.behind === 'number' && details.behind > 0 ? (
<span className="text-[color:var(--status-warning)]">{details.behind}</span>
) : null}
</div>
</div>
{!isEditing && !isConfirming ? (
<div className="flex items-center gap-1 flex-shrink-0">
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => void handleCreateWorktreeForBranch(branchName)}
disabled={disableCreateWorktree}
className="inline-flex h-7 w-7 items-center justify-center rounded-md hover:bg-interactive-hover/40 text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50"
aria-label={t('branchPickerDialog.actions.createWorktreeAria')}
>
{isCreatingWorktree ? (
<Icon name="loader-4" className="h-4 w-4 animate-spin" />
) : (
<Icon name="split-cells-horizontal" className="h-4 w-4" />
)}
</button>
</TooltipTrigger>
<TooltipContent side="left">
{hasAttachedWorktree ? t('branchPickerDialog.tooltip.worktreeAlreadyExists') : t('branchPickerDialog.tooltip.createWorktree')}
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => beginRename(branchName)}
disabled={disableRename}
className="inline-flex h-7 w-7 items-center justify-center rounded-md hover:bg-interactive-hover/40 text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50"
aria-label={t('branchPickerDialog.actions.renameAria')}
>
<Icon name="pencil" className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="left">
{isProjectRootBranch ? t('branchPickerDialog.tooltip.renameDisabledForRoot') : t('branchPickerDialog.tooltip.rename')}
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => {
if (hasAttachedWorktree) {
handleRemoveWorktree(attachedWorktree);
return;
}
setConfirmingDelete(branchName);
}}
disabled={hasAttachedWorktree ? disableWorktreeDelete : disableDelete}
className="inline-flex h-7 w-7 items-center justify-center rounded-md hover:bg-destructive/10 text-muted-foreground hover:text-destructive transition-colors disabled:opacity-50"
aria-label={hasAttachedWorktree ? t('branchPickerDialog.actions.deleteWorktreeAria') : t('branchPickerDialog.actions.deleteAria')}
>
{isDeleting ? (
<Icon name="loader-4" className="h-4 w-4 animate-spin" />
) : (
<Icon name="delete-bin" className="h-4 w-4" />
)}
</button>
</TooltipTrigger>
<TooltipContent side="left">
{hasAttachedWorktree
? isProjectRootBranch
? t('branchPickerDialog.tooltip.deleteWorktreeRootProtected')
: t('branchPickerDialog.tooltip.deleteWorktree')
: isCurrent
? t('branchPickerDialog.tooltip.deleteCurrentBranch')
: isProjectRootBranch
? t('branchPickerDialog.tooltip.deleteDisabledForRoot')
: t('branchPickerDialog.tooltip.delete')}
</TooltipContent>
</Tooltip>
</div>
) : null}
{isEditing ? (
<div className="flex items-center gap-1 flex-shrink-0">
<button
type="button"
onClick={() => void commitRename(branchName)}
disabled={isRenaming}
className="inline-flex h-7 w-7 items-center justify-center rounded-md hover:bg-interactive-hover/40 text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50"
aria-label={t('branchPickerDialog.actions.confirmRenameAria')}
>
{isRenaming ? (
<Icon name="loader-4" className="h-4 w-4 animate-spin" />
) : (
<Icon name="check" className="h-4 w-4" />
)}
</button>
<button
type="button"
onClick={cancelRename}
className="inline-flex h-7 w-7 items-center justify-center rounded-md hover:bg-interactive-hover/40 text-muted-foreground hover:text-foreground transition-colors"
aria-label={t('branchPickerDialog.actions.cancelRenameAria')}
>
<Icon name="close" className="h-4 w-4" />
</button>
</div>
) : null}
{!isEditing && isConfirming && !hasAttachedWorktree ? (
<div className="flex items-center gap-1 flex-shrink-0">
<span className={cn(
'text-xs mr-1',
isForceDelete ? 'text-destructive' : 'text-muted-foreground'
)}>
{isForceDelete ? t('branchPickerDialog.actions.forceDeletePrompt') : t('branchPickerDialog.actions.deletePrompt')}
</span>
<button
type="button"
onClick={() => void handleDeleteBranch(branchName)}
disabled={isDeleting}
className={cn(
'inline-flex h-7 w-7 items-center justify-center rounded-md transition-colors disabled:opacity-50',
isForceDelete
? 'bg-destructive/10 text-destructive hover:bg-destructive/15'
: 'hover:bg-destructive/10 text-muted-foreground hover:text-destructive'
)}
aria-label={t('branchPickerDialog.actions.confirmDeleteAria')}
>
{isDeleting ? (
<Icon name="loader-4" className="h-4 w-4 animate-spin" />
) : (
<Icon name="check" className="h-4 w-4" />
)}
</button>
<button
type="button"
onClick={cancelDelete}
className="inline-flex h-7 w-7 items-center justify-center rounded-md hover:bg-interactive-hover/40 text-muted-foreground hover:text-foreground transition-colors"
aria-label={t('branchPickerDialog.actions.cancelDeleteAria')}
>
<Icon name="close" className="h-4 w-4" />
</button>
</div>
) : null}
</div>
);
})
)}
</div>
</div>
</DialogContent>
</Dialog>
);
}
@@ -1,314 +0,0 @@
import React from 'react';
import { cn } from '@/lib/utils';
import { opencodeClient, type FilesystemEntry } from '@/lib/opencode/client';
import { useDebouncedValue } from '@/hooks/useDebouncedValue';
import { Icon } from "@/components/icon/Icon";
interface DirectoryAutocompleteProps {
inputValue: string;
homeDirectory: string | null;
onSelectSuggestion: (path: string) => void;
visible: boolean;
onClose: () => void;
showHidden: boolean;
}
export interface DirectoryAutocompleteHandle {
handleKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => boolean;
}
export const DirectoryAutocomplete = React.forwardRef<DirectoryAutocompleteHandle, DirectoryAutocompleteProps>(({
inputValue,
homeDirectory,
onSelectSuggestion,
visible,
onClose,
showHidden,
}, ref) => {
const [suggestions, setSuggestions] = React.useState<FilesystemEntry[]>([]);
const [loading, setLoading] = React.useState(false);
const [selectedIndex, setSelectedIndex] = React.useState(0);
const containerRef = React.useRef<HTMLDivElement | null>(null);
const itemRefs = React.useRef<(HTMLDivElement | null)[]>([]);
// Fuzzy matching score - returns null if no match, higher score = better match
const fuzzyScore = React.useCallback((query: string, candidate: string): number | null => {
const q = query.trim().toLowerCase();
if (!q) {
return 0;
}
const c = candidate.toLowerCase();
let score = 0;
let lastIndex = -1;
let consecutive = 0;
for (let i = 0; i < q.length; i += 1) {
const ch = q[i];
if (!ch || ch === ' ') {
continue;
}
const idx = c.indexOf(ch, lastIndex + 1);
if (idx === -1) {
return null; // Character not found - no match
}
const gap = idx - lastIndex - 1;
if (gap === 0) {
consecutive += 1;
} else {
consecutive = 0;
}
score += 10; // Base score per matched char
score += Math.max(0, 18 - idx); // Bonus for early matches
score -= Math.max(0, gap); // Penalty for gaps
// Bonus for match at start or after separator
if (idx === 0) {
score += 12;
} else {
const prev = c[idx - 1];
if (prev === '/' || prev === '_' || prev === '-' || prev === '.' || prev === ' ') {
score += 10;
}
}
score += consecutive > 0 ? 12 : 0; // Bonus for consecutive matches
lastIndex = idx;
}
score += Math.max(0, 24 - Math.round(c.length / 3)); // Shorter names score higher
return score;
}, []);
// Expand ~ to home directory
const expandPath = React.useCallback((path: string): string => {
if (path.startsWith('~') && homeDirectory) {
return path.replace(/^~/, homeDirectory);
}
return path;
}, [homeDirectory]);
// Get the directory part of the path for listing
const getParentDir = React.useCallback((path: string): string => {
const expanded = expandPath(path);
// If ends with /, list that directory
if (expanded.endsWith('/')) {
return expanded;
}
// Otherwise, get parent directory
const lastSlash = expanded.lastIndexOf('/');
if (lastSlash === -1) return '';
if (lastSlash === 0) return '/';
return expanded.substring(0, lastSlash + 1);
}, [expandPath]);
// Get the partial name being typed (for filtering)
const getPartialName = React.useCallback((path: string): string => {
const expanded = expandPath(path);
if (expanded.endsWith('/')) return '';
const lastSlash = expanded.lastIndexOf('/');
if (lastSlash === -1) return expanded;
return expanded.substring(lastSlash + 1);
}, [expandPath]);
const debouncedInputValue = useDebouncedValue(inputValue, 150);
// Fetch directory suggestions
React.useEffect(() => {
if (!visible || !debouncedInputValue) {
setSuggestions([]);
return;
}
const parentDir = getParentDir(debouncedInputValue);
const partialName = getPartialName(debouncedInputValue).toLowerCase();
if (!parentDir) {
setSuggestions([]);
return;
}
let cancelled = false;
setLoading(true);
opencodeClient.listLocalDirectory(parentDir)
.then((entries) => {
if (cancelled) return;
// Filter to directories only, respect hidden setting
const directories = entries.filter((entry) => {
if (!entry.isDirectory) return false;
if (!showHidden && entry.name.startsWith('.')) return false;
return true;
});
// Apply fuzzy matching and sort by score
const scored = partialName
? directories
.map((entry) => {
const score = fuzzyScore(partialName, entry.name);
return score !== null ? { entry, score } : null;
})
.filter((item): item is { entry: FilesystemEntry; score: number } => item !== null)
.sort((a, b) => b.score - a.score || a.entry.name.localeCompare(b.entry.name))
.map((item) => item.entry)
: directories.sort((a, b) => a.name.localeCompare(b.name));
setSuggestions(scored.slice(0, 10)); // Limit suggestions
setSelectedIndex(0);
})
.catch(() => {
if (!cancelled) {
setSuggestions([]);
}
})
.finally(() => {
if (!cancelled) {
setLoading(false);
}
});
return () => {
cancelled = true;
};
}, [visible, debouncedInputValue, getParentDir, getPartialName, showHidden, fuzzyScore]);
// Scroll selected item into view
React.useEffect(() => {
itemRefs.current[selectedIndex]?.scrollIntoView({
behavior: 'smooth',
block: 'nearest'
});
}, [selectedIndex]);
// Handle outside click
React.useEffect(() => {
if (!visible) return;
const handlePointerDown = (event: MouseEvent | TouchEvent) => {
const target = event.target as Node | null;
if (!target || !containerRef.current) return;
if (containerRef.current.contains(target)) return;
onClose();
};
document.addEventListener('pointerdown', handlePointerDown, true);
return () => {
document.removeEventListener('pointerdown', handlePointerDown, true);
};
}, [visible, onClose]);
const handleSelectSuggestion = React.useCallback((entry: FilesystemEntry) => {
// Append the selected directory name to current path, with trailing slash
const path = entry.path.endsWith('/') ? entry.path : entry.path + '/';
onSelectSuggestion(path);
}, [onSelectSuggestion]);
// Expose key handler to parent
React.useImperativeHandle(ref, () => ({
handleKeyDown: (e: React.KeyboardEvent<HTMLInputElement>): boolean => {
if (!visible || suggestions.length === 0) {
return false;
}
const total = suggestions.length;
if (e.key === 'Tab') {
e.preventDefault();
if (e.shiftKey) {
// Shift+Tab: previous suggestion
setSelectedIndex((prev) => (prev - 1 + total) % total);
} else {
// Tab: next suggestion or select if only one
if (total === 1) {
const selected = suggestions[0];
if (selected) {
handleSelectSuggestion(selected);
}
} else {
setSelectedIndex((prev) => (prev + 1) % total);
}
}
return true;
}
if (e.key === 'ArrowDown') {
e.preventDefault();
setSelectedIndex((prev) => (prev + 1) % total);
return true;
}
if (e.key === 'ArrowUp') {
e.preventDefault();
setSelectedIndex((prev) => (prev - 1 + total) % total);
return true;
}
if (e.key === 'Enter') {
e.preventDefault();
// Select current item and close autocomplete
const safeIndex = ((selectedIndex % total) + total) % total;
const selected = suggestions[safeIndex];
if (selected) {
handleSelectSuggestion(selected);
}
onClose();
return true; // Consume the event, don't let parent confirm yet
}
if (e.key === 'Escape') {
e.preventDefault();
onClose();
return true;
}
return false;
}
}), [visible, suggestions, selectedIndex, handleSelectSuggestion, onClose]);
if (!visible || (suggestions.length === 0 && !loading)) {
return null;
}
return (
<div
ref={containerRef}
className="absolute z-[100] w-full max-h-48 bg-background border border-border rounded-lg shadow-none top-full mt-1 left-0 flex flex-col overflow-hidden"
>
{loading ? (
<div className="flex items-center justify-center py-3">
<Icon name="refresh" className="h-4 w-4 animate-spin text-muted-foreground" />
</div>
) : (
<div className="overflow-y-auto py-1">
{suggestions.map((entry, index) => {
const isSelected = selectedIndex === index;
return (
<div
key={entry.path}
ref={(el) => { itemRefs.current[index] = el; }}
className={cn(
"flex items-center gap-2 px-3 py-1.5 cursor-pointer typography-ui-label",
isSelected && "bg-interactive-selection"
)}
onClick={() => { handleSelectSuggestion(entry); onClose(); }}
onMouseEnter={() => setSelectedIndex(index)}
>
<Icon name="folder" className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<span className="truncate">{entry.name}</span>
</div>
);
})}
</div>
)}
<div className="px-3 py-1.5 border-t typography-meta text-muted-foreground bg-sidebar/50">
Tab cycle navigate Enter select
</div>
</div>
);
});
DirectoryAutocomplete.displayName = 'DirectoryAutocomplete';
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,6 @@
import type { Session } from '@opencode-ai/sdk/v2';
export const RECENT_SESSION_MAX_AGE_MS = 48 * 60 * 60 * 1000;
const RECENT_SESSION_MAX_AGE_MS = 48 * 60 * 60 * 1000;
const isSubtaskSession = (session: Session): boolean => {
return Boolean((session as Session & { parentID?: string | null }).parentID);
@@ -22,7 +22,7 @@ const getSessionUpdatedAt = (session: Session): number => {
return 0;
};
export const sortSessionsByUpdated = (sessions: Session[]): Session[] => {
const sortSessionsByUpdated = (sessions: Session[]): Session[] => {
return [...sessions].sort((a, b) => getSessionUpdatedAt(b) - getSessionUpdatedAt(a));
};
@@ -42,5 +42,3 @@ export const deriveRecentSessions = (
});
return sortSessionsByUpdated(recent);
};
export const getSessionUpdatedAtMs = getSessionUpdatedAt;
@@ -3,7 +3,7 @@ import type { Session } from '@opencode-ai/sdk/v2';
import type { WorktreeMetadata } from '@/types/worktree';
import { dedupeSessionsById, getArchivedScopeKey, isSessionRelatedToProject, normalizePath, resolveArchivedFolderName } from '../utils';
export type ProjectForArchivedFolders = {
type ProjectForArchivedFolders = {
normalizedPath: string;
};
@@ -146,20 +146,6 @@ export const compareSessionsByPinnedAndTime = (
return getSessionUpdatedAt(b) - getSessionUpdatedAt(a);
};
export const compareSessionsByPinnedAndCreated = (
a: Session,
b: Session,
pinnedSessionIds: Set<string>,
): number => {
const aPinned = pinnedSessionIds.has(a.id);
const bPinned = pinnedSessionIds.has(b.id);
if (aPinned !== bPinned) {
return aPinned ? -1 : 1;
}
return getSessionCreatedAt(b) - getSessionCreatedAt(a);
};
export const dedupeSessionsById = (sessions: Session[]): Session[] => {
const byId = new Map<string, Session>();
sessions.forEach((session) => {