@atercates/claude-deck 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +123 -0
- package/app/api/claude/hidden/route.ts +66 -0
- package/app/api/claude/projects/[name]/sessions/route.ts +71 -0
- package/app/api/claude/projects/route.ts +44 -0
- package/app/api/code-search/available/route.ts +12 -0
- package/app/api/code-search/route.ts +47 -0
- package/app/api/dev-servers/[id]/logs/route.ts +23 -0
- package/app/api/dev-servers/[id]/restart/route.ts +20 -0
- package/app/api/dev-servers/[id]/route.ts +51 -0
- package/app/api/dev-servers/[id]/stop/route.ts +20 -0
- package/app/api/dev-servers/detect/route.ts +39 -0
- package/app/api/dev-servers/route.ts +48 -0
- package/app/api/exec/route.ts +60 -0
- package/app/api/files/content/route.ts +76 -0
- package/app/api/files/route.ts +37 -0
- package/app/api/files/upload-temp/route.ts +41 -0
- package/app/api/git/check/route.ts +54 -0
- package/app/api/git/clone/route.ts +99 -0
- package/app/api/git/commit/route.ts +75 -0
- package/app/api/git/discard/route.ts +38 -0
- package/app/api/git/file-content/route.ts +64 -0
- package/app/api/git/history/[hash]/diff/route.ts +38 -0
- package/app/api/git/history/[hash]/route.ts +34 -0
- package/app/api/git/history/route.ts +27 -0
- package/app/api/git/multi-status/route.ts +46 -0
- package/app/api/git/pr/route.ts +164 -0
- package/app/api/git/push/route.ts +64 -0
- package/app/api/git/stage/route.ts +40 -0
- package/app/api/git/status/route.ts +51 -0
- package/app/api/git/unstage/route.ts +46 -0
- package/app/api/groups/[...path]/route.ts +136 -0
- package/app/api/groups/route.ts +93 -0
- package/app/api/orchestrate/spawn/route.ts +45 -0
- package/app/api/orchestrate/workers/[id]/route.ts +89 -0
- package/app/api/orchestrate/workers/route.ts +31 -0
- package/app/api/projects/[id]/detect/route.ts +27 -0
- package/app/api/projects/[id]/dev-servers/[dsId]/route.ts +66 -0
- package/app/api/projects/[id]/dev-servers/route.ts +51 -0
- package/app/api/projects/[id]/repositories/[repoId]/route.ts +67 -0
- package/app/api/projects/[id]/repositories/route.ts +74 -0
- package/app/api/projects/[id]/route.ts +108 -0
- package/app/api/projects/detect/route.ts +33 -0
- package/app/api/projects/route.ts +59 -0
- package/app/api/sessions/[id]/claude-session/route.ts +42 -0
- package/app/api/sessions/[id]/fork/route.ts +74 -0
- package/app/api/sessions/[id]/mcp-config/route.ts +34 -0
- package/app/api/sessions/[id]/messages/route.ts +60 -0
- package/app/api/sessions/[id]/pr/route.ts +188 -0
- package/app/api/sessions/[id]/preview/route.ts +42 -0
- package/app/api/sessions/[id]/route.ts +229 -0
- package/app/api/sessions/[id]/send-keys/route.ts +119 -0
- package/app/api/sessions/[id]/summarize/route.ts +331 -0
- package/app/api/sessions/init-script/route.ts +84 -0
- package/app/api/sessions/route.ts +209 -0
- package/app/api/sessions/status/route.ts +237 -0
- package/app/api/system/route.ts +9 -0
- package/app/api/tmux/kill-all/route.ts +57 -0
- package/app/api/tmux/rename/route.ts +30 -0
- package/app/globals.css +174 -0
- package/app/icon.svg +11 -0
- package/app/layout.tsx +122 -0
- package/app/page.tsx +629 -0
- package/components/ChatMessage.tsx +65 -0
- package/components/ChatView.tsx +276 -0
- package/components/ClaudeProjects/ClaudeProjectCard.tsx +195 -0
- package/components/ClaudeProjects/ClaudeProjectsSection.tsx +89 -0
- package/components/ClaudeProjects/ClaudeSessionCard.tsx +100 -0
- package/components/ClaudeProjects/index.ts +1 -0
- package/components/CodeSearch/CodeSearchResults.tsx +177 -0
- package/components/ConductorPanel.tsx +256 -0
- package/components/DevServers/DevServerCard.tsx +311 -0
- package/components/DevServers/DevServersSection.tsx +91 -0
- package/components/DevServers/ServerLogsModal.tsx +151 -0
- package/components/DevServers/StartServerDialog.tsx +359 -0
- package/components/DevServers/index.ts +4 -0
- package/components/DiffViewer/DiffModal.tsx +151 -0
- package/components/DiffViewer/UnifiedDiff.tsx +185 -0
- package/components/DiffViewer/index.tsx +2 -0
- package/components/DirectoryPicker.tsx +355 -0
- package/components/FileExplorer/FileEditor.tsx +276 -0
- package/components/FileExplorer/FileTabs.tsx +118 -0
- package/components/FileExplorer/FileTree.tsx +214 -0
- package/components/FileExplorer/HtmlRenderer.tsx +16 -0
- package/components/FileExplorer/MarkdownRenderer.tsx +18 -0
- package/components/FileExplorer/index.tsx +520 -0
- package/components/FilePicker.tsx +339 -0
- package/components/FolderPicker.tsx +201 -0
- package/components/GitDrawer/FileEditDialog.tsx +400 -0
- package/components/GitDrawer/index.tsx +464 -0
- package/components/GitPanel/CommitForm.tsx +205 -0
- package/components/GitPanel/CommitHistory.tsx +174 -0
- package/components/GitPanel/CommitItem.tsx +196 -0
- package/components/GitPanel/FileChanges.tsx +414 -0
- package/components/GitPanel/GitPanelTabs.tsx +39 -0
- package/components/GitPanel/index.tsx +817 -0
- package/components/MessageInput.tsx +82 -0
- package/components/NewClaudeSessionDialog.tsx +166 -0
- package/components/NewSessionDialog/AdvancedSettings.tsx +78 -0
- package/components/NewSessionDialog/AgentSelector.tsx +37 -0
- package/components/NewSessionDialog/CreatingOverlay.tsx +94 -0
- package/components/NewSessionDialog/NewSessionDialog.types.ts +136 -0
- package/components/NewSessionDialog/ProjectSelector.tsx +146 -0
- package/components/NewSessionDialog/WorkingDirectoryInput.tsx +55 -0
- package/components/NewSessionDialog/WorktreeSection.tsx +92 -0
- package/components/NewSessionDialog/hooks/useNewSessionForm.ts +370 -0
- package/components/NewSessionDialog/index.tsx +106 -0
- package/components/NotificationSettings.tsx +127 -0
- package/components/PRCreationModal.tsx +272 -0
- package/components/Pane/DesktopTabBar.tsx +353 -0
- package/components/Pane/MobileTabBar.tsx +210 -0
- package/components/Pane/OpenInVSCode.tsx +69 -0
- package/components/Pane/PaneSkeletons.tsx +57 -0
- package/components/Pane/index.tsx +558 -0
- package/components/PaneLayout.tsx +60 -0
- package/components/Projects/DevServersSection.tsx +140 -0
- package/components/Projects/DirectoryField.tsx +92 -0
- package/components/Projects/NewProjectDialog.tsx +188 -0
- package/components/Projects/NewProjectDialog.types.ts +46 -0
- package/components/Projects/ProjectCard.tsx +276 -0
- package/components/Projects/ProjectSettingsDialog.tsx +811 -0
- package/components/Projects/hooks/useNewProjectForm.ts +249 -0
- package/components/Projects/index.ts +3 -0
- package/components/Providers.tsx +49 -0
- package/components/QuickSwitcher.tsx +306 -0
- package/components/SessionList/KillAllConfirm.tsx +46 -0
- package/components/SessionList/SelectionToolbar.tsx +164 -0
- package/components/SessionList/SessionList.types.ts +37 -0
- package/components/SessionList/SessionListHeader.tsx +71 -0
- package/components/SessionList/hooks/useSessionListMutations.ts +269 -0
- package/components/SessionList/index.tsx +189 -0
- package/components/ShellDrawer/index.tsx +106 -0
- package/components/SidebarFooter.tsx +55 -0
- package/components/Terminal/KeybarToggleButton.tsx +45 -0
- package/components/Terminal/ScrollToBottomButton.tsx +32 -0
- package/components/Terminal/SearchBar.tsx +71 -0
- package/components/Terminal/TerminalToolbar.tsx +551 -0
- package/components/Terminal/VirtualKeyboard.tsx +711 -0
- package/components/Terminal/constants.ts +20 -0
- package/components/Terminal/hooks/index.ts +5 -0
- package/components/Terminal/hooks/resize-handlers.ts +140 -0
- package/components/Terminal/hooks/terminal-init.ts +151 -0
- package/components/Terminal/hooks/touch-scroll.ts +155 -0
- package/components/Terminal/hooks/useTerminalConnection.ts +282 -0
- package/components/Terminal/hooks/useTerminalConnection.types.ts +39 -0
- package/components/Terminal/hooks/useTerminalSearch.ts +103 -0
- package/components/Terminal/hooks/websocket-connection.ts +274 -0
- package/components/Terminal/index.tsx +320 -0
- package/components/ThemeToggle.tsx +168 -0
- package/components/TmuxSessions.tsx +132 -0
- package/components/ToolCallDisplay.tsx +71 -0
- package/components/WorkerCard.tsx +245 -0
- package/components/a/ABadge.tsx +115 -0
- package/components/a/AButton.tsx +163 -0
- package/components/a/ADialog.tsx +93 -0
- package/components/a/ADropdownMenu.tsx +279 -0
- package/components/a/AIconButton.tsx +190 -0
- package/components/a/ASheet.tsx +150 -0
- package/components/a/ATooltip.tsx +77 -0
- package/components/a/index.ts +64 -0
- package/components/mobile/SwipeSidebar.tsx +122 -0
- package/components/ui/badge.tsx +41 -0
- package/components/ui/button.tsx +60 -0
- package/components/ui/context-menu.tsx +197 -0
- package/components/ui/dialog.tsx +143 -0
- package/components/ui/dropdown-menu.tsx +257 -0
- package/components/ui/input.tsx +21 -0
- package/components/ui/scroll-area.tsx +52 -0
- package/components/ui/select.tsx +159 -0
- package/components/ui/skeleton.tsx +111 -0
- package/components/ui/switch.tsx +31 -0
- package/components/ui/textarea.tsx +21 -0
- package/components/ui/tooltip.tsx +32 -0
- package/components/views/DesktopView.tsx +244 -0
- package/components/views/MobileView.tsx +110 -0
- package/components/views/types.ts +75 -0
- package/contexts/PaneContext.tsx +336 -0
- package/data/claude/index.ts +9 -0
- package/data/claude/keys.ts +6 -0
- package/data/claude/queries.ts +120 -0
- package/data/claude/useClaudeUpdates.ts +37 -0
- package/data/code-search/index.ts +2 -0
- package/data/code-search/keys.ts +7 -0
- package/data/code-search/queries.ts +61 -0
- package/data/dev-servers/index.ts +8 -0
- package/data/dev-servers/keys.ts +4 -0
- package/data/dev-servers/queries.ts +104 -0
- package/data/files/index.ts +3 -0
- package/data/files/keys.ts +4 -0
- package/data/files/queries.ts +25 -0
- package/data/git/keys.ts +15 -0
- package/data/git/queries.ts +395 -0
- package/data/groups/index.ts +1 -0
- package/data/groups/mutations.ts +95 -0
- package/data/projects/index.ts +10 -0
- package/data/projects/keys.ts +4 -0
- package/data/projects/queries.ts +193 -0
- package/data/repositories/index.ts +7 -0
- package/data/repositories/keys.ts +5 -0
- package/data/repositories/queries.ts +122 -0
- package/data/sessions/index.ts +12 -0
- package/data/sessions/keys.ts +8 -0
- package/data/sessions/queries.ts +218 -0
- package/data/statuses/index.ts +1 -0
- package/data/statuses/queries.ts +69 -0
- package/hooks/useCopyToClipboard.ts +48 -0
- package/hooks/useDevServersManager.ts +73 -0
- package/hooks/useDirectoryBrowser.ts +90 -0
- package/hooks/useDrawerAnimation.ts +27 -0
- package/hooks/useFileDrop.ts +87 -0
- package/hooks/useFileEditor.ts +184 -0
- package/hooks/useGroups.ts +37 -0
- package/hooks/useHomePath.ts +34 -0
- package/hooks/useKeyRepeat.ts +55 -0
- package/hooks/useKeybarVisibility.ts +42 -0
- package/hooks/useNotifications.ts +257 -0
- package/hooks/useProjects.ts +53 -0
- package/hooks/useSessionStatuses.ts +30 -0
- package/hooks/useSessions.ts +86 -0
- package/hooks/useSpeechRecognition.ts +124 -0
- package/hooks/useViewport.ts +32 -0
- package/hooks/useViewportHeight.ts +50 -0
- package/lib/async-operations.ts +35 -0
- package/lib/banner.ts +81 -0
- package/lib/claude/jsonl-cache.ts +86 -0
- package/lib/claude/jsonl-reader.ts +271 -0
- package/lib/claude/process-manager.ts +278 -0
- package/lib/claude/stream-parser.ts +173 -0
- package/lib/claude/types.ts +154 -0
- package/lib/claude/watcher.ts +71 -0
- package/lib/client/session-registry.ts +111 -0
- package/lib/code-search.ts +121 -0
- package/lib/db/index.ts +48 -0
- package/lib/db/migrations.ts +45 -0
- package/lib/db/queries.ts +460 -0
- package/lib/db/schema.ts +114 -0
- package/lib/db/types.ts +92 -0
- package/lib/db.ts +2 -0
- package/lib/dev-servers.ts +509 -0
- package/lib/diff-parser.ts +221 -0
- package/lib/env-setup.ts +285 -0
- package/lib/file-upload.ts +34 -0
- package/lib/file-utils.ts +50 -0
- package/lib/files.ts +207 -0
- package/lib/git-history.ts +294 -0
- package/lib/git-status.ts +391 -0
- package/lib/git.ts +257 -0
- package/lib/mcp-config.ts +81 -0
- package/lib/multi-repo-git.ts +179 -0
- package/lib/notifications.ts +219 -0
- package/lib/orchestration.ts +448 -0
- package/lib/panes.ts +232 -0
- package/lib/ports.ts +97 -0
- package/lib/pr-generation.ts +307 -0
- package/lib/pr.ts +234 -0
- package/lib/projects.ts +578 -0
- package/lib/providers/registry.ts +70 -0
- package/lib/providers.ts +121 -0
- package/lib/query-client.ts +14 -0
- package/lib/rangeSelectionUtils.ts +65 -0
- package/lib/status-detector.ts +375 -0
- package/lib/terminal-themes.ts +265 -0
- package/lib/theme-config.ts +327 -0
- package/lib/utils.ts +6 -0
- package/lib/worktrees.ts +262 -0
- package/mcp/orchestration-server.ts +438 -0
- package/package.json +139 -0
- package/postcss.config.mjs +7 -0
- package/public/icon.svg +10 -0
- package/public/icons/icon-128x128.png +0 -0
- package/public/icons/icon-144x144.png +0 -0
- package/public/icons/icon-152x152.png +0 -0
- package/public/icons/icon-192x192.png +0 -0
- package/public/icons/icon-384x384.png +0 -0
- package/public/icons/icon-512x512.png +0 -0
- package/public/icons/icon-72x72.png +0 -0
- package/public/icons/icon-96x96.png +0 -0
- package/public/manifest.json +61 -0
- package/public/sw.js +64 -0
- package/scripts/agent-os +91 -0
- package/scripts/install.sh +48 -0
- package/scripts/lib/ai-clis.sh +132 -0
- package/scripts/lib/commands.sh +487 -0
- package/scripts/lib/common.sh +89 -0
- package/scripts/lib/prerequisites.sh +462 -0
- package/scripts/setup.sh +134 -0
- package/server.ts +155 -0
- package/stores/fileOpen.ts +26 -0
- package/stores/index.ts +1 -0
- package/stores/initialPrompt.ts +24 -0
- package/stores/sessionSelection.ts +48 -0
- package/styles/themes.css +603 -0
- package/tsconfig.json +33 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback, useRef } from "react";
|
|
4
|
+
import {
|
|
5
|
+
X,
|
|
6
|
+
Folder,
|
|
7
|
+
FileIcon,
|
|
8
|
+
FileImage,
|
|
9
|
+
ChevronLeft,
|
|
10
|
+
Loader2,
|
|
11
|
+
Home,
|
|
12
|
+
ChevronRight,
|
|
13
|
+
Upload,
|
|
14
|
+
Clipboard,
|
|
15
|
+
Search,
|
|
16
|
+
} from "lucide-react";
|
|
17
|
+
import { Input } from "@/components/ui/input";
|
|
18
|
+
import { Button } from "@/components/ui/button";
|
|
19
|
+
import { cn } from "@/lib/utils";
|
|
20
|
+
import { uploadFileToTemp } from "@/lib/file-upload";
|
|
21
|
+
import { useFileDrop } from "@/hooks/useFileDrop";
|
|
22
|
+
import { useViewport } from "@/hooks/useViewport";
|
|
23
|
+
import { useDirectoryBrowser } from "@/hooks/useDirectoryBrowser";
|
|
24
|
+
import type { FileNode } from "@/lib/file-utils";
|
|
25
|
+
|
|
26
|
+
const IMAGE_EXTENSIONS = [
|
|
27
|
+
"png",
|
|
28
|
+
"jpg",
|
|
29
|
+
"jpeg",
|
|
30
|
+
"gif",
|
|
31
|
+
"webp",
|
|
32
|
+
"svg",
|
|
33
|
+
"bmp",
|
|
34
|
+
"ico",
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
interface FilePickerProps {
|
|
38
|
+
initialPath?: string;
|
|
39
|
+
onSelect: (path: string) => void;
|
|
40
|
+
onClose: () => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isImageFile(node: FileNode) {
|
|
44
|
+
if (node.type !== "file") return false;
|
|
45
|
+
const ext = node.extension?.toLowerCase() || "";
|
|
46
|
+
return IMAGE_EXTENSIONS.includes(ext);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function FilePicker({
|
|
50
|
+
initialPath,
|
|
51
|
+
onSelect,
|
|
52
|
+
onClose,
|
|
53
|
+
}: FilePickerProps) {
|
|
54
|
+
const {
|
|
55
|
+
currentPath,
|
|
56
|
+
filteredFiles,
|
|
57
|
+
loading,
|
|
58
|
+
error,
|
|
59
|
+
search,
|
|
60
|
+
setSearch,
|
|
61
|
+
pathSegments,
|
|
62
|
+
navigateTo,
|
|
63
|
+
navigateUp,
|
|
64
|
+
navigateHome,
|
|
65
|
+
} = useDirectoryBrowser({ initialPath });
|
|
66
|
+
|
|
67
|
+
const [uploading, setUploading] = useState(false);
|
|
68
|
+
const dropZoneRef = useRef<HTMLDivElement>(null);
|
|
69
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
70
|
+
const { isMobile } = useViewport();
|
|
71
|
+
|
|
72
|
+
// Handle dropped/pasted/selected file
|
|
73
|
+
const handleFile = useCallback(
|
|
74
|
+
async (file: File) => {
|
|
75
|
+
setUploading(true);
|
|
76
|
+
try {
|
|
77
|
+
const path = await uploadFileToTemp(file);
|
|
78
|
+
if (path) {
|
|
79
|
+
onSelect(path);
|
|
80
|
+
}
|
|
81
|
+
} catch (err) {
|
|
82
|
+
console.error("Failed to upload file:", err);
|
|
83
|
+
} finally {
|
|
84
|
+
setUploading(false);
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
[onSelect]
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Drag and drop (desktop only)
|
|
91
|
+
const { isDragging, dragHandlers } = useFileDrop(dropZoneRef, handleFile, {
|
|
92
|
+
disabled: uploading || isMobile,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Clipboard paste handler
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
const handlePaste = (e: ClipboardEvent) => {
|
|
98
|
+
const items = e.clipboardData?.items;
|
|
99
|
+
if (!items) return;
|
|
100
|
+
|
|
101
|
+
for (const item of items) {
|
|
102
|
+
if (item.kind === "file") {
|
|
103
|
+
const file = item.getAsFile();
|
|
104
|
+
if (file) {
|
|
105
|
+
e.preventDefault();
|
|
106
|
+
handleFile(file);
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
document.addEventListener("paste", handlePaste);
|
|
114
|
+
return () => document.removeEventListener("paste", handlePaste);
|
|
115
|
+
}, [handleFile]);
|
|
116
|
+
|
|
117
|
+
const handleItemClick = (node: FileNode) => {
|
|
118
|
+
if (node.type === "directory") {
|
|
119
|
+
navigateTo(node.path);
|
|
120
|
+
} else if (node.type === "file") {
|
|
121
|
+
onSelect(node.path);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<div className="bg-background fixed inset-0 z-50 flex flex-col">
|
|
127
|
+
{/* Header */}
|
|
128
|
+
<div className="border-border bg-background/95 flex items-center gap-2 border-b p-3 backdrop-blur-sm">
|
|
129
|
+
<Button
|
|
130
|
+
variant="ghost"
|
|
131
|
+
size="icon-sm"
|
|
132
|
+
onClick={onClose}
|
|
133
|
+
className="h-9 w-9"
|
|
134
|
+
>
|
|
135
|
+
<X className="h-5 w-5" />
|
|
136
|
+
</Button>
|
|
137
|
+
<div className="min-w-0 flex-1">
|
|
138
|
+
<h3 className="text-sm font-medium">Select File</h3>
|
|
139
|
+
<p className="text-muted-foreground truncate text-xs">
|
|
140
|
+
{currentPath}
|
|
141
|
+
</p>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
{/* Navigation bar */}
|
|
146
|
+
<div className="border-border flex items-center gap-1 overflow-x-auto border-b px-3 py-2">
|
|
147
|
+
<Button
|
|
148
|
+
variant="ghost"
|
|
149
|
+
size="icon-sm"
|
|
150
|
+
onClick={navigateHome}
|
|
151
|
+
className="h-8 w-8 shrink-0"
|
|
152
|
+
title="Home"
|
|
153
|
+
>
|
|
154
|
+
<Home className="h-4 w-4" />
|
|
155
|
+
</Button>
|
|
156
|
+
<Button
|
|
157
|
+
variant="ghost"
|
|
158
|
+
size="icon-sm"
|
|
159
|
+
onClick={navigateUp}
|
|
160
|
+
className="h-8 w-8 shrink-0"
|
|
161
|
+
title="Go up"
|
|
162
|
+
>
|
|
163
|
+
<ChevronLeft className="h-4 w-4" />
|
|
164
|
+
</Button>
|
|
165
|
+
<div className="text-muted-foreground flex items-center gap-0.5 overflow-x-auto text-xs">
|
|
166
|
+
<span>/</span>
|
|
167
|
+
{pathSegments.map((segment, i) => (
|
|
168
|
+
<button
|
|
169
|
+
key={i}
|
|
170
|
+
onClick={() =>
|
|
171
|
+
navigateTo("/" + pathSegments.slice(0, i + 1).join("/"))
|
|
172
|
+
}
|
|
173
|
+
className="hover:text-foreground flex shrink-0 items-center transition-colors"
|
|
174
|
+
>
|
|
175
|
+
<span className="max-w-[100px] truncate">{segment}</span>
|
|
176
|
+
{i < pathSegments.length - 1 && (
|
|
177
|
+
<ChevronRight className="mx-0.5 h-3 w-3" />
|
|
178
|
+
)}
|
|
179
|
+
</button>
|
|
180
|
+
))}
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
{/* Upload zone */}
|
|
185
|
+
{isMobile ? (
|
|
186
|
+
<div className="mx-3 mt-3 flex items-center justify-center gap-2">
|
|
187
|
+
<input
|
|
188
|
+
ref={fileInputRef}
|
|
189
|
+
type="file"
|
|
190
|
+
className="hidden"
|
|
191
|
+
onChange={(e) => {
|
|
192
|
+
const file = e.target.files?.[0];
|
|
193
|
+
if (file) handleFile(file);
|
|
194
|
+
}}
|
|
195
|
+
/>
|
|
196
|
+
<Button
|
|
197
|
+
variant="outline"
|
|
198
|
+
size="sm"
|
|
199
|
+
onClick={() => fileInputRef.current?.click()}
|
|
200
|
+
disabled={uploading}
|
|
201
|
+
className="gap-2"
|
|
202
|
+
>
|
|
203
|
+
{uploading ? (
|
|
204
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
205
|
+
) : (
|
|
206
|
+
<Upload className="h-4 w-4" />
|
|
207
|
+
)}
|
|
208
|
+
{uploading ? "Uploading..." : "Upload file"}
|
|
209
|
+
</Button>
|
|
210
|
+
<span className="text-muted-foreground text-xs">
|
|
211
|
+
or select a file below
|
|
212
|
+
</span>
|
|
213
|
+
</div>
|
|
214
|
+
) : (
|
|
215
|
+
<div
|
|
216
|
+
ref={dropZoneRef}
|
|
217
|
+
{...dragHandlers}
|
|
218
|
+
className={cn(
|
|
219
|
+
"border-border mx-3 mt-3 flex flex-col items-center justify-center rounded-lg border-2 border-dashed p-4 transition-colors",
|
|
220
|
+
isDragging && "border-primary bg-primary/10",
|
|
221
|
+
uploading && "opacity-50"
|
|
222
|
+
)}
|
|
223
|
+
>
|
|
224
|
+
{uploading ? (
|
|
225
|
+
<div className="flex items-center gap-2">
|
|
226
|
+
<Loader2 className="h-5 w-5 animate-spin" />
|
|
227
|
+
<span className="text-sm">Uploading...</span>
|
|
228
|
+
</div>
|
|
229
|
+
) : isDragging ? (
|
|
230
|
+
<div className="flex items-center gap-2">
|
|
231
|
+
<Upload className="text-primary h-5 w-5" />
|
|
232
|
+
<span className="text-primary text-sm font-medium">
|
|
233
|
+
Drop file here
|
|
234
|
+
</span>
|
|
235
|
+
</div>
|
|
236
|
+
) : (
|
|
237
|
+
<div className="flex flex-col items-center gap-1 text-center">
|
|
238
|
+
<div className="text-muted-foreground flex items-center gap-2">
|
|
239
|
+
<Upload className="h-4 w-4" />
|
|
240
|
+
<span className="text-sm">Drop file here</span>
|
|
241
|
+
</div>
|
|
242
|
+
<div className="text-muted-foreground flex items-center gap-1 text-xs">
|
|
243
|
+
<Clipboard className="h-3 w-3" />
|
|
244
|
+
<span>or paste from clipboard</span>
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
)}
|
|
248
|
+
</div>
|
|
249
|
+
)}
|
|
250
|
+
|
|
251
|
+
{/* Search */}
|
|
252
|
+
<div className="px-3 py-2">
|
|
253
|
+
<div className="relative">
|
|
254
|
+
<Search className="text-muted-foreground absolute top-1/2 left-2.5 h-4 w-4 -translate-y-1/2" />
|
|
255
|
+
<Input
|
|
256
|
+
type="text"
|
|
257
|
+
placeholder="Search files..."
|
|
258
|
+
value={search}
|
|
259
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
260
|
+
className="h-9 pl-9"
|
|
261
|
+
/>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
{/* Content */}
|
|
266
|
+
<div className="flex-1 overflow-y-auto">
|
|
267
|
+
{loading ? (
|
|
268
|
+
<div className="flex h-32 items-center justify-center">
|
|
269
|
+
<Loader2 className="text-muted-foreground h-6 w-6 animate-spin" />
|
|
270
|
+
</div>
|
|
271
|
+
) : error ? (
|
|
272
|
+
<div className="text-muted-foreground flex h-32 flex-col items-center justify-center p-4">
|
|
273
|
+
<p className="text-center text-sm">{error}</p>
|
|
274
|
+
<Button
|
|
275
|
+
variant="outline"
|
|
276
|
+
size="sm"
|
|
277
|
+
onClick={navigateUp}
|
|
278
|
+
className="mt-2"
|
|
279
|
+
>
|
|
280
|
+
Go back
|
|
281
|
+
</Button>
|
|
282
|
+
</div>
|
|
283
|
+
) : filteredFiles.length === 0 ? (
|
|
284
|
+
<div className="text-muted-foreground flex h-32 items-center justify-center">
|
|
285
|
+
<p className="text-sm">
|
|
286
|
+
{search ? "No matching files" : "Empty directory"}
|
|
287
|
+
</p>
|
|
288
|
+
</div>
|
|
289
|
+
) : (
|
|
290
|
+
<div className="grid grid-cols-2 gap-2 p-3 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5">
|
|
291
|
+
{filteredFiles.map((node) => {
|
|
292
|
+
const isImg = isImageFile(node);
|
|
293
|
+
const isDir = node.type === "directory";
|
|
294
|
+
const isFile = node.type === "file";
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<button
|
|
298
|
+
key={node.path}
|
|
299
|
+
onClick={() => handleItemClick(node)}
|
|
300
|
+
className={cn(
|
|
301
|
+
"flex flex-col items-center gap-2 rounded-lg border p-3 text-center transition-colors",
|
|
302
|
+
"hover:bg-muted/50 hover:border-primary/50 cursor-pointer",
|
|
303
|
+
isImg && "border-primary/30 bg-primary/5"
|
|
304
|
+
)}
|
|
305
|
+
>
|
|
306
|
+
{isDir ? (
|
|
307
|
+
<Folder className="text-primary/70 h-10 w-10" />
|
|
308
|
+
) : isImg ? (
|
|
309
|
+
<div className="bg-muted flex h-10 w-10 items-center justify-center overflow-hidden rounded">
|
|
310
|
+
<FileImage className="text-primary h-6 w-6" />
|
|
311
|
+
</div>
|
|
312
|
+
) : isFile ? (
|
|
313
|
+
<div className="bg-muted/50 flex h-10 w-10 items-center justify-center rounded">
|
|
314
|
+
<FileIcon className="text-muted-foreground h-6 w-6" />
|
|
315
|
+
</div>
|
|
316
|
+
) : (
|
|
317
|
+
<div className="bg-muted/50 flex h-10 w-10 items-center justify-center rounded">
|
|
318
|
+
<span className="text-muted-foreground text-xs">
|
|
319
|
+
{node.extension?.toUpperCase() || "?"}
|
|
320
|
+
</span>
|
|
321
|
+
</div>
|
|
322
|
+
)}
|
|
323
|
+
<span className="w-full truncate text-xs">{node.name}</span>
|
|
324
|
+
</button>
|
|
325
|
+
);
|
|
326
|
+
})}
|
|
327
|
+
</div>
|
|
328
|
+
)}
|
|
329
|
+
</div>
|
|
330
|
+
|
|
331
|
+
{/* Footer hint */}
|
|
332
|
+
<div className="border-border border-t p-3 text-center">
|
|
333
|
+
<p className="text-muted-foreground text-xs">
|
|
334
|
+
Select any file or navigate into folders
|
|
335
|
+
</p>
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
);
|
|
339
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
import {
|
|
5
|
+
X,
|
|
6
|
+
Folder,
|
|
7
|
+
ChevronLeft,
|
|
8
|
+
Loader2,
|
|
9
|
+
Home,
|
|
10
|
+
ChevronRight,
|
|
11
|
+
Check,
|
|
12
|
+
GitBranch,
|
|
13
|
+
Search,
|
|
14
|
+
} from "lucide-react";
|
|
15
|
+
import { Input } from "@/components/ui/input";
|
|
16
|
+
import { Button } from "@/components/ui/button";
|
|
17
|
+
import { useDirectoryBrowser } from "@/hooks/useDirectoryBrowser";
|
|
18
|
+
|
|
19
|
+
const DIRS_ONLY = (f: { type: string }) => f.type === "directory";
|
|
20
|
+
|
|
21
|
+
interface FolderPickerProps {
|
|
22
|
+
initialPath?: string;
|
|
23
|
+
onSelect: (path: string) => void;
|
|
24
|
+
onClose: () => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function FolderPicker({
|
|
28
|
+
initialPath,
|
|
29
|
+
onSelect,
|
|
30
|
+
onClose,
|
|
31
|
+
}: FolderPickerProps) {
|
|
32
|
+
const {
|
|
33
|
+
currentPath,
|
|
34
|
+
filteredFiles,
|
|
35
|
+
loading,
|
|
36
|
+
error,
|
|
37
|
+
search,
|
|
38
|
+
setSearch,
|
|
39
|
+
pathSegments,
|
|
40
|
+
navigateTo,
|
|
41
|
+
navigateUp,
|
|
42
|
+
navigateHome,
|
|
43
|
+
} = useDirectoryBrowser({ initialPath, filter: DIRS_ONLY });
|
|
44
|
+
|
|
45
|
+
// Git repo check for current directory
|
|
46
|
+
const [isGitRepo, setIsGitRepo] = useState(false);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
fetch("/api/git/check", {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: { "Content-Type": "application/json" },
|
|
52
|
+
body: JSON.stringify({ path: currentPath }),
|
|
53
|
+
})
|
|
54
|
+
.then((res) => res.json())
|
|
55
|
+
.then((data) => setIsGitRepo(data.isGitRepo || false))
|
|
56
|
+
.catch(() => setIsGitRepo(false));
|
|
57
|
+
}, [currentPath]);
|
|
58
|
+
|
|
59
|
+
const folderName = pathSegments[pathSegments.length - 1] || "root";
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className="bg-background fixed inset-0 z-[100] flex flex-col">
|
|
63
|
+
{/* Header */}
|
|
64
|
+
<div className="bg-background/95 flex items-center gap-2 p-3 shadow-sm backdrop-blur-sm">
|
|
65
|
+
<Button
|
|
66
|
+
variant="ghost"
|
|
67
|
+
size="icon-sm"
|
|
68
|
+
onClick={onClose}
|
|
69
|
+
className="h-9 w-9"
|
|
70
|
+
>
|
|
71
|
+
<X className="h-5 w-5" />
|
|
72
|
+
</Button>
|
|
73
|
+
<div className="min-w-0 flex-1">
|
|
74
|
+
<h3 className="text-sm font-medium">Select Folder</h3>
|
|
75
|
+
<p className="text-muted-foreground truncate text-xs">
|
|
76
|
+
{currentPath}
|
|
77
|
+
</p>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
{/* Search */}
|
|
82
|
+
<div className="px-3 py-2">
|
|
83
|
+
<div className="relative">
|
|
84
|
+
<Search className="text-muted-foreground absolute top-1/2 left-2.5 h-4 w-4 -translate-y-1/2" />
|
|
85
|
+
<Input
|
|
86
|
+
type="text"
|
|
87
|
+
placeholder="Search folders..."
|
|
88
|
+
value={search}
|
|
89
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
90
|
+
className="h-9 pl-9"
|
|
91
|
+
/>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
{/* Navigation bar */}
|
|
96
|
+
<div className="flex items-center gap-1 overflow-x-auto px-3 pb-2">
|
|
97
|
+
<Button
|
|
98
|
+
variant="ghost"
|
|
99
|
+
size="icon-sm"
|
|
100
|
+
onClick={navigateHome}
|
|
101
|
+
className="h-8 w-8 shrink-0"
|
|
102
|
+
title="Home"
|
|
103
|
+
>
|
|
104
|
+
<Home className="h-4 w-4" />
|
|
105
|
+
</Button>
|
|
106
|
+
<Button
|
|
107
|
+
variant="ghost"
|
|
108
|
+
size="icon-sm"
|
|
109
|
+
onClick={navigateUp}
|
|
110
|
+
className="h-8 w-8 shrink-0"
|
|
111
|
+
title="Go up"
|
|
112
|
+
>
|
|
113
|
+
<ChevronLeft className="h-4 w-4" />
|
|
114
|
+
</Button>
|
|
115
|
+
<div className="text-muted-foreground flex items-center gap-0.5 overflow-x-auto text-xs">
|
|
116
|
+
<span>/</span>
|
|
117
|
+
{pathSegments.map((segment, i) => (
|
|
118
|
+
<button
|
|
119
|
+
key={i}
|
|
120
|
+
onClick={() =>
|
|
121
|
+
navigateTo("/" + pathSegments.slice(0, i + 1).join("/"))
|
|
122
|
+
}
|
|
123
|
+
className="hover:text-foreground flex shrink-0 items-center transition-colors"
|
|
124
|
+
>
|
|
125
|
+
<span className="max-w-[100px] truncate">{segment}</span>
|
|
126
|
+
{i < pathSegments.length - 1 && (
|
|
127
|
+
<ChevronRight className="mx-0.5 h-3 w-3" />
|
|
128
|
+
)}
|
|
129
|
+
</button>
|
|
130
|
+
))}
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
{/* Content */}
|
|
135
|
+
<div className="flex-1 overflow-y-auto">
|
|
136
|
+
{loading ? (
|
|
137
|
+
<div className="flex h-32 items-center justify-center">
|
|
138
|
+
<Loader2 className="text-muted-foreground h-6 w-6 animate-spin" />
|
|
139
|
+
</div>
|
|
140
|
+
) : error ? (
|
|
141
|
+
<div className="text-muted-foreground flex h-32 flex-col items-center justify-center p-4">
|
|
142
|
+
<p className="text-center text-sm">{error}</p>
|
|
143
|
+
<Button
|
|
144
|
+
variant="outline"
|
|
145
|
+
size="sm"
|
|
146
|
+
onClick={navigateUp}
|
|
147
|
+
className="mt-2"
|
|
148
|
+
>
|
|
149
|
+
Go back
|
|
150
|
+
</Button>
|
|
151
|
+
</div>
|
|
152
|
+
) : filteredFiles.length === 0 ? (
|
|
153
|
+
<div className="text-muted-foreground flex h-32 items-center justify-center">
|
|
154
|
+
<p className="text-sm">
|
|
155
|
+
{search ? "No matching folders" : "No subfolders"}
|
|
156
|
+
</p>
|
|
157
|
+
</div>
|
|
158
|
+
) : (
|
|
159
|
+
<div className="space-y-0.5 px-2 pt-1">
|
|
160
|
+
{filteredFiles.map((node) => (
|
|
161
|
+
<button
|
|
162
|
+
key={node.path}
|
|
163
|
+
onClick={() => navigateTo(node.path)}
|
|
164
|
+
className="hover:bg-muted/50 flex w-full items-center gap-3 rounded-md px-3 py-3 text-left transition-colors"
|
|
165
|
+
>
|
|
166
|
+
<Folder className="text-muted-foreground h-5 w-5 shrink-0" />
|
|
167
|
+
<span className="min-w-0 flex-1 truncate text-sm">
|
|
168
|
+
{node.name}
|
|
169
|
+
</span>
|
|
170
|
+
<ChevronRight className="text-muted-foreground h-4 w-4 shrink-0" />
|
|
171
|
+
</button>
|
|
172
|
+
))}
|
|
173
|
+
</div>
|
|
174
|
+
)}
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
{/* Footer with select button */}
|
|
178
|
+
<div className="flex items-center justify-between gap-3 p-3 shadow-[0_-2px_8px_rgba(0,0,0,0.08)]">
|
|
179
|
+
<div className="min-w-0 flex-1">
|
|
180
|
+
<div className="flex items-center gap-2">
|
|
181
|
+
<Folder className="text-primary h-5 w-5 shrink-0" />
|
|
182
|
+
<span className="truncate font-medium">{folderName}</span>
|
|
183
|
+
{isGitRepo && (
|
|
184
|
+
<span className="bg-muted text-muted-foreground flex shrink-0 items-center gap-1 rounded px-1.5 py-0.5 text-xs">
|
|
185
|
+
<GitBranch className="h-3 w-3" />
|
|
186
|
+
Git
|
|
187
|
+
</span>
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
<Button
|
|
192
|
+
onClick={() => onSelect(currentPath)}
|
|
193
|
+
className="shrink-0 gap-2"
|
|
194
|
+
>
|
|
195
|
+
<Check className="h-4 w-4" />
|
|
196
|
+
Select
|
|
197
|
+
</Button>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
);
|
|
201
|
+
}
|