@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,811 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
import {
|
|
5
|
+
Dialog,
|
|
6
|
+
DialogContent,
|
|
7
|
+
DialogHeader,
|
|
8
|
+
DialogTitle,
|
|
9
|
+
DialogFooter,
|
|
10
|
+
} from "@/components/ui/dialog";
|
|
11
|
+
import { Button } from "@/components/ui/button";
|
|
12
|
+
import { Input } from "@/components/ui/input";
|
|
13
|
+
import { Textarea } from "@/components/ui/textarea";
|
|
14
|
+
import {
|
|
15
|
+
Select,
|
|
16
|
+
SelectContent,
|
|
17
|
+
SelectItem,
|
|
18
|
+
SelectTrigger,
|
|
19
|
+
SelectValue,
|
|
20
|
+
} from "@/components/ui/select";
|
|
21
|
+
import {
|
|
22
|
+
Plus,
|
|
23
|
+
Trash2,
|
|
24
|
+
Loader2,
|
|
25
|
+
RefreshCw,
|
|
26
|
+
Server,
|
|
27
|
+
GitBranch,
|
|
28
|
+
Star,
|
|
29
|
+
FolderOpen,
|
|
30
|
+
} from "lucide-react";
|
|
31
|
+
import { FolderPicker } from "@/components/FolderPicker";
|
|
32
|
+
import { useUpdateProject } from "@/data/projects";
|
|
33
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
34
|
+
import { devServerKeys } from "@/data/dev-servers";
|
|
35
|
+
import { repositoryKeys } from "@/data/repositories";
|
|
36
|
+
import type { AgentType } from "@/lib/providers";
|
|
37
|
+
import type {
|
|
38
|
+
ProjectWithRepositories,
|
|
39
|
+
DetectedDevServer,
|
|
40
|
+
} from "@/lib/projects";
|
|
41
|
+
|
|
42
|
+
const AGENT_OPTIONS: { value: AgentType; label: string }[] = [
|
|
43
|
+
{ value: "claude", label: "Claude Code" },
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const MODEL_OPTIONS = [
|
|
47
|
+
{ value: "sonnet", label: "Sonnet" },
|
|
48
|
+
{ value: "opus", label: "Opus" },
|
|
49
|
+
{ value: "haiku", label: "Haiku" },
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
interface DevServerConfig {
|
|
53
|
+
id: string;
|
|
54
|
+
name: string;
|
|
55
|
+
type: "node" | "docker";
|
|
56
|
+
command: string;
|
|
57
|
+
port?: number;
|
|
58
|
+
portEnvVar?: string;
|
|
59
|
+
isNew?: boolean;
|
|
60
|
+
isDeleted?: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface RepositoryConfig {
|
|
64
|
+
id: string;
|
|
65
|
+
name: string;
|
|
66
|
+
path: string;
|
|
67
|
+
isPrimary: boolean;
|
|
68
|
+
isNew?: boolean;
|
|
69
|
+
isDeleted?: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface ProjectSettingsDialogProps {
|
|
73
|
+
project: ProjectWithRepositories | null;
|
|
74
|
+
open: boolean;
|
|
75
|
+
onClose: () => void;
|
|
76
|
+
onSave: () => void;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function ProjectSettingsDialog({
|
|
80
|
+
project,
|
|
81
|
+
open,
|
|
82
|
+
onClose,
|
|
83
|
+
onSave,
|
|
84
|
+
}: ProjectSettingsDialogProps) {
|
|
85
|
+
const [name, setName] = useState("");
|
|
86
|
+
const [workingDirectory, setWorkingDirectory] = useState("");
|
|
87
|
+
const [agentType, setAgentType] = useState<AgentType>("claude");
|
|
88
|
+
const [defaultModel, setDefaultModel] = useState("sonnet");
|
|
89
|
+
const [initialPrompt, setInitialPrompt] = useState("");
|
|
90
|
+
const [devServers, setDevServers] = useState<DevServerConfig[]>([]);
|
|
91
|
+
const [repositories, setRepositories] = useState<RepositoryConfig[]>([]);
|
|
92
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
93
|
+
const [isDetecting, setIsDetecting] = useState(false);
|
|
94
|
+
const [isDetectingRepos, setIsDetectingRepos] = useState(false);
|
|
95
|
+
const [error, setError] = useState<string | null>(null);
|
|
96
|
+
const [folderPickerRepoId, setFolderPickerRepoId] = useState<string | null>(
|
|
97
|
+
null
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const updateProject = useUpdateProject();
|
|
101
|
+
const queryClient = useQueryClient();
|
|
102
|
+
|
|
103
|
+
// Initialize form when project changes
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (project) {
|
|
106
|
+
setName(project.name);
|
|
107
|
+
setWorkingDirectory(project.working_directory);
|
|
108
|
+
setAgentType(project.agent_type);
|
|
109
|
+
setDefaultModel(project.default_model);
|
|
110
|
+
setInitialPrompt(project.initial_prompt || "");
|
|
111
|
+
setDevServers(
|
|
112
|
+
project.devServers.map((ds) => ({
|
|
113
|
+
id: ds.id,
|
|
114
|
+
name: ds.name,
|
|
115
|
+
type: ds.type,
|
|
116
|
+
command: ds.command,
|
|
117
|
+
port: ds.port || undefined,
|
|
118
|
+
portEnvVar: ds.port_env_var || undefined,
|
|
119
|
+
}))
|
|
120
|
+
);
|
|
121
|
+
setRepositories(
|
|
122
|
+
(project.repositories || []).map((repo) => ({
|
|
123
|
+
id: repo.id,
|
|
124
|
+
name: repo.name,
|
|
125
|
+
path: repo.path,
|
|
126
|
+
isPrimary: repo.is_primary,
|
|
127
|
+
}))
|
|
128
|
+
);
|
|
129
|
+
// Reset folder picker state when project changes
|
|
130
|
+
setFolderPickerRepoId(null);
|
|
131
|
+
}
|
|
132
|
+
}, [project]);
|
|
133
|
+
|
|
134
|
+
// Reset folder picker when dialog closes
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (!open) {
|
|
137
|
+
setFolderPickerRepoId(null);
|
|
138
|
+
}
|
|
139
|
+
}, [open]);
|
|
140
|
+
|
|
141
|
+
// Detect dev servers
|
|
142
|
+
const detectDevServers = async () => {
|
|
143
|
+
if (!workingDirectory) return;
|
|
144
|
+
|
|
145
|
+
setIsDetecting(true);
|
|
146
|
+
try {
|
|
147
|
+
const res = await fetch("/api/projects/detect", {
|
|
148
|
+
method: "POST",
|
|
149
|
+
headers: { "Content-Type": "application/json" },
|
|
150
|
+
body: JSON.stringify({ workingDirectory }),
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (res.ok) {
|
|
154
|
+
const data = await res.json();
|
|
155
|
+
const detected = (data.detected || []) as DetectedDevServer[];
|
|
156
|
+
|
|
157
|
+
// Add detected servers that don't already exist
|
|
158
|
+
const existingCommands = new Set(devServers.map((ds) => ds.command));
|
|
159
|
+
const newServers = detected
|
|
160
|
+
.filter((d) => !existingCommands.has(d.command))
|
|
161
|
+
.map((d, i) => ({
|
|
162
|
+
id: `new_${Date.now()}_${i}`,
|
|
163
|
+
name: d.name,
|
|
164
|
+
type: d.type,
|
|
165
|
+
command: d.command,
|
|
166
|
+
port: d.port,
|
|
167
|
+
portEnvVar: d.portEnvVar,
|
|
168
|
+
isNew: true,
|
|
169
|
+
}));
|
|
170
|
+
|
|
171
|
+
setDevServers((prev) => [...prev, ...newServers]);
|
|
172
|
+
}
|
|
173
|
+
} catch (err) {
|
|
174
|
+
console.error("Failed to detect dev servers:", err);
|
|
175
|
+
} finally {
|
|
176
|
+
setIsDetecting(false);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Add new dev server config
|
|
181
|
+
const addDevServer = () => {
|
|
182
|
+
setDevServers((prev) => [
|
|
183
|
+
...prev,
|
|
184
|
+
{
|
|
185
|
+
id: `new_${Date.now()}`,
|
|
186
|
+
name: "",
|
|
187
|
+
type: "node",
|
|
188
|
+
command: "",
|
|
189
|
+
isNew: true,
|
|
190
|
+
},
|
|
191
|
+
]);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// Remove dev server config
|
|
195
|
+
const removeDevServer = (id: string) => {
|
|
196
|
+
setDevServers(
|
|
197
|
+
(prev) =>
|
|
198
|
+
prev
|
|
199
|
+
.map((ds) =>
|
|
200
|
+
ds.id === id
|
|
201
|
+
? ds.isNew
|
|
202
|
+
? null // Remove new items completely
|
|
203
|
+
: { ...ds, isDeleted: true } // Mark existing for deletion
|
|
204
|
+
: ds
|
|
205
|
+
)
|
|
206
|
+
.filter(Boolean) as DevServerConfig[]
|
|
207
|
+
);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// Update dev server config
|
|
211
|
+
const updateDevServer = (id: string, updates: Partial<DevServerConfig>) => {
|
|
212
|
+
setDevServers((prev) =>
|
|
213
|
+
prev.map((ds) => (ds.id === id ? { ...ds, ...updates } : ds))
|
|
214
|
+
);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// Detect git repositories in working directory
|
|
218
|
+
const detectRepositories = async () => {
|
|
219
|
+
if (!workingDirectory) return;
|
|
220
|
+
|
|
221
|
+
setIsDetectingRepos(true);
|
|
222
|
+
try {
|
|
223
|
+
// Check if the working directory itself is a git repo
|
|
224
|
+
const res = await fetch(
|
|
225
|
+
`/api/git/status?path=${encodeURIComponent(workingDirectory)}`
|
|
226
|
+
);
|
|
227
|
+
if (res.ok) {
|
|
228
|
+
const existingPaths = new Set(repositories.map((r) => r.path));
|
|
229
|
+
if (!existingPaths.has(workingDirectory)) {
|
|
230
|
+
// Extract repo name from path
|
|
231
|
+
const pathParts = workingDirectory.split("/").filter(Boolean);
|
|
232
|
+
const repoName = pathParts[pathParts.length - 1] || "Repository";
|
|
233
|
+
|
|
234
|
+
setRepositories((prev) => [
|
|
235
|
+
...prev,
|
|
236
|
+
{
|
|
237
|
+
id: `new_${Date.now()}`,
|
|
238
|
+
name: repoName,
|
|
239
|
+
path: workingDirectory,
|
|
240
|
+
isPrimary: prev.length === 0,
|
|
241
|
+
isNew: true,
|
|
242
|
+
},
|
|
243
|
+
]);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
} catch {
|
|
247
|
+
// Not a git repo, that's okay
|
|
248
|
+
} finally {
|
|
249
|
+
setIsDetectingRepos(false);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// Add new repository config - opens folder picker directly
|
|
254
|
+
const addRepository = () => {
|
|
255
|
+
const newId = `new_${Date.now()}`;
|
|
256
|
+
setRepositories((prev) => [
|
|
257
|
+
...prev,
|
|
258
|
+
{
|
|
259
|
+
id: newId,
|
|
260
|
+
name: "",
|
|
261
|
+
path: "",
|
|
262
|
+
isPrimary: prev.filter((r) => !r.isDeleted).length === 0,
|
|
263
|
+
isNew: true,
|
|
264
|
+
},
|
|
265
|
+
]);
|
|
266
|
+
// Open folder picker for the new repository
|
|
267
|
+
setFolderPickerRepoId(newId);
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// Remove repository config
|
|
271
|
+
const removeRepository = (id: string) => {
|
|
272
|
+
setRepositories(
|
|
273
|
+
(prev) =>
|
|
274
|
+
prev
|
|
275
|
+
.map((repo) =>
|
|
276
|
+
repo.id === id
|
|
277
|
+
? repo.isNew
|
|
278
|
+
? null // Remove new items completely
|
|
279
|
+
: { ...repo, isDeleted: true } // Mark existing for deletion
|
|
280
|
+
: repo
|
|
281
|
+
)
|
|
282
|
+
.filter(Boolean) as RepositoryConfig[]
|
|
283
|
+
);
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
// Update repository config
|
|
287
|
+
const updateRepository = (id: string, updates: Partial<RepositoryConfig>) => {
|
|
288
|
+
setRepositories((prev) =>
|
|
289
|
+
prev.map((repo) => (repo.id === id ? { ...repo, ...updates } : repo))
|
|
290
|
+
);
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
// Set a repository as primary
|
|
294
|
+
const setRepositoryPrimary = (id: string) => {
|
|
295
|
+
setRepositories((prev) =>
|
|
296
|
+
prev.map((repo) => ({
|
|
297
|
+
...repo,
|
|
298
|
+
isPrimary: repo.id === id,
|
|
299
|
+
}))
|
|
300
|
+
);
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
304
|
+
e.preventDefault();
|
|
305
|
+
if (!project) return;
|
|
306
|
+
setError(null);
|
|
307
|
+
|
|
308
|
+
if (!name.trim()) {
|
|
309
|
+
setError("Project name is required");
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
setIsLoading(true);
|
|
314
|
+
try {
|
|
315
|
+
// Update project settings using mutation (properly invalidates cache)
|
|
316
|
+
await updateProject.mutateAsync({
|
|
317
|
+
projectId: project.id,
|
|
318
|
+
name: name.trim(),
|
|
319
|
+
workingDirectory,
|
|
320
|
+
agentType,
|
|
321
|
+
defaultModel,
|
|
322
|
+
initialPrompt: initialPrompt.trim() || null,
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// Handle dev server changes
|
|
326
|
+
for (const ds of devServers) {
|
|
327
|
+
if (ds.isDeleted && !ds.isNew) {
|
|
328
|
+
// Delete existing dev server
|
|
329
|
+
await fetch(`/api/projects/${project.id}/dev-servers/${ds.id}`, {
|
|
330
|
+
method: "DELETE",
|
|
331
|
+
});
|
|
332
|
+
} else if (
|
|
333
|
+
ds.isNew &&
|
|
334
|
+
!ds.isDeleted &&
|
|
335
|
+
ds.name.trim() &&
|
|
336
|
+
ds.command.trim()
|
|
337
|
+
) {
|
|
338
|
+
// Create new dev server
|
|
339
|
+
await fetch(`/api/projects/${project.id}/dev-servers`, {
|
|
340
|
+
method: "POST",
|
|
341
|
+
headers: { "Content-Type": "application/json" },
|
|
342
|
+
body: JSON.stringify({
|
|
343
|
+
name: ds.name.trim(),
|
|
344
|
+
type: ds.type,
|
|
345
|
+
command: ds.command.trim(),
|
|
346
|
+
port: ds.port || undefined,
|
|
347
|
+
portEnvVar: ds.portEnvVar || undefined,
|
|
348
|
+
}),
|
|
349
|
+
});
|
|
350
|
+
} else if (!ds.isNew && !ds.isDeleted) {
|
|
351
|
+
// Update existing dev server
|
|
352
|
+
await fetch(`/api/projects/${project.id}/dev-servers/${ds.id}`, {
|
|
353
|
+
method: "PATCH",
|
|
354
|
+
headers: { "Content-Type": "application/json" },
|
|
355
|
+
body: JSON.stringify({
|
|
356
|
+
name: ds.name.trim(),
|
|
357
|
+
type: ds.type,
|
|
358
|
+
command: ds.command.trim(),
|
|
359
|
+
port: ds.port || undefined,
|
|
360
|
+
portEnvVar: ds.portEnvVar || undefined,
|
|
361
|
+
}),
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Handle repository changes
|
|
367
|
+
for (const repo of repositories) {
|
|
368
|
+
if (repo.isDeleted && !repo.isNew) {
|
|
369
|
+
// Delete existing repository
|
|
370
|
+
await fetch(`/api/projects/${project.id}/repositories/${repo.id}`, {
|
|
371
|
+
method: "DELETE",
|
|
372
|
+
});
|
|
373
|
+
} else if (
|
|
374
|
+
repo.isNew &&
|
|
375
|
+
!repo.isDeleted &&
|
|
376
|
+
repo.name.trim() &&
|
|
377
|
+
repo.path.trim()
|
|
378
|
+
) {
|
|
379
|
+
// Create new repository
|
|
380
|
+
await fetch(`/api/projects/${project.id}/repositories`, {
|
|
381
|
+
method: "POST",
|
|
382
|
+
headers: { "Content-Type": "application/json" },
|
|
383
|
+
body: JSON.stringify({
|
|
384
|
+
name: repo.name.trim(),
|
|
385
|
+
path: repo.path.trim(),
|
|
386
|
+
isPrimary: repo.isPrimary,
|
|
387
|
+
}),
|
|
388
|
+
});
|
|
389
|
+
} else if (!repo.isNew && !repo.isDeleted) {
|
|
390
|
+
// Update existing repository
|
|
391
|
+
await fetch(`/api/projects/${project.id}/repositories/${repo.id}`, {
|
|
392
|
+
method: "PATCH",
|
|
393
|
+
headers: { "Content-Type": "application/json" },
|
|
394
|
+
body: JSON.stringify({
|
|
395
|
+
name: repo.name.trim(),
|
|
396
|
+
path: repo.path.trim(),
|
|
397
|
+
isPrimary: repo.isPrimary,
|
|
398
|
+
}),
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Invalidate dev servers cache so list updates
|
|
404
|
+
queryClient.invalidateQueries({ queryKey: devServerKeys.list() });
|
|
405
|
+
// Invalidate repositories cache
|
|
406
|
+
queryClient.invalidateQueries({
|
|
407
|
+
queryKey: repositoryKeys.list(project.id),
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
handleClose();
|
|
411
|
+
onSave();
|
|
412
|
+
} catch (err) {
|
|
413
|
+
console.error("Failed to update project:", err);
|
|
414
|
+
setError("Failed to update project");
|
|
415
|
+
} finally {
|
|
416
|
+
setIsLoading(false);
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const handleClose = () => {
|
|
421
|
+
setError(null);
|
|
422
|
+
setFolderPickerRepoId(null);
|
|
423
|
+
onClose();
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
const visibleDevServers = devServers.filter((ds) => !ds.isDeleted);
|
|
427
|
+
const visibleRepositories = repositories.filter((repo) => !repo.isDeleted);
|
|
428
|
+
|
|
429
|
+
if (!project) return null;
|
|
430
|
+
|
|
431
|
+
return (
|
|
432
|
+
<>
|
|
433
|
+
<Dialog
|
|
434
|
+
open={open && !folderPickerRepoId}
|
|
435
|
+
onOpenChange={(o) => !o && handleClose()}
|
|
436
|
+
>
|
|
437
|
+
<DialogContent className="max-h-[90vh] max-w-lg overflow-y-auto">
|
|
438
|
+
<DialogHeader>
|
|
439
|
+
<DialogTitle>Project Settings</DialogTitle>
|
|
440
|
+
</DialogHeader>
|
|
441
|
+
|
|
442
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
443
|
+
{/* Project Name */}
|
|
444
|
+
<div className="space-y-2">
|
|
445
|
+
<label className="text-sm font-medium">Project Name</label>
|
|
446
|
+
<Input
|
|
447
|
+
value={name}
|
|
448
|
+
onChange={(e) => setName(e.target.value)}
|
|
449
|
+
placeholder="my-awesome-project"
|
|
450
|
+
/>
|
|
451
|
+
</div>
|
|
452
|
+
|
|
453
|
+
{/* Working Directory */}
|
|
454
|
+
<div className="space-y-2">
|
|
455
|
+
<label className="text-sm font-medium">Working Directory</label>
|
|
456
|
+
<Input
|
|
457
|
+
value={workingDirectory}
|
|
458
|
+
onChange={(e) => setWorkingDirectory(e.target.value)}
|
|
459
|
+
placeholder="~/projects/my-app"
|
|
460
|
+
/>
|
|
461
|
+
</div>
|
|
462
|
+
|
|
463
|
+
{/* Agent Type */}
|
|
464
|
+
<div className="space-y-2">
|
|
465
|
+
<label className="text-sm font-medium">Default Agent</label>
|
|
466
|
+
<Select
|
|
467
|
+
value={agentType}
|
|
468
|
+
onValueChange={(v) => setAgentType(v as AgentType)}
|
|
469
|
+
>
|
|
470
|
+
<SelectTrigger>
|
|
471
|
+
<SelectValue />
|
|
472
|
+
</SelectTrigger>
|
|
473
|
+
<SelectContent>
|
|
474
|
+
{AGENT_OPTIONS.map((opt) => (
|
|
475
|
+
<SelectItem key={opt.value} value={opt.value}>
|
|
476
|
+
{opt.label}
|
|
477
|
+
</SelectItem>
|
|
478
|
+
))}
|
|
479
|
+
</SelectContent>
|
|
480
|
+
</Select>
|
|
481
|
+
</div>
|
|
482
|
+
|
|
483
|
+
{/* Default Model */}
|
|
484
|
+
<div className="space-y-2">
|
|
485
|
+
<label className="text-sm font-medium">Default Model</label>
|
|
486
|
+
<Select value={defaultModel} onValueChange={setDefaultModel}>
|
|
487
|
+
<SelectTrigger>
|
|
488
|
+
<SelectValue />
|
|
489
|
+
</SelectTrigger>
|
|
490
|
+
<SelectContent>
|
|
491
|
+
{MODEL_OPTIONS.map((opt) => (
|
|
492
|
+
<SelectItem key={opt.value} value={opt.value}>
|
|
493
|
+
{opt.label}
|
|
494
|
+
</SelectItem>
|
|
495
|
+
))}
|
|
496
|
+
</SelectContent>
|
|
497
|
+
</Select>
|
|
498
|
+
</div>
|
|
499
|
+
|
|
500
|
+
{/* Initial Prompt */}
|
|
501
|
+
<div className="space-y-2">
|
|
502
|
+
<label className="text-sm font-medium">Initial Prompt</label>
|
|
503
|
+
<Textarea
|
|
504
|
+
value={initialPrompt}
|
|
505
|
+
onChange={(e) => setInitialPrompt(e.target.value)}
|
|
506
|
+
placeholder="This prompt will be prepended to all sessions in this project..."
|
|
507
|
+
rows={3}
|
|
508
|
+
className="resize-none"
|
|
509
|
+
/>
|
|
510
|
+
<p className="text-muted-foreground text-xs">
|
|
511
|
+
This prompt will be automatically prepended to all new sessions
|
|
512
|
+
created in this project.
|
|
513
|
+
</p>
|
|
514
|
+
</div>
|
|
515
|
+
|
|
516
|
+
{/* Dev Servers */}
|
|
517
|
+
<div className="space-y-3">
|
|
518
|
+
<div className="flex items-center justify-between">
|
|
519
|
+
<label className="flex items-center gap-2 text-sm font-medium">
|
|
520
|
+
<Server className="h-4 w-4" />
|
|
521
|
+
Dev Servers
|
|
522
|
+
</label>
|
|
523
|
+
<div className="flex gap-1">
|
|
524
|
+
<Button
|
|
525
|
+
type="button"
|
|
526
|
+
variant="outline"
|
|
527
|
+
size="sm"
|
|
528
|
+
onClick={detectDevServers}
|
|
529
|
+
disabled={isDetecting || !workingDirectory}
|
|
530
|
+
>
|
|
531
|
+
{isDetecting ? (
|
|
532
|
+
<Loader2 className="mr-1 h-3 w-3 animate-spin" />
|
|
533
|
+
) : (
|
|
534
|
+
<RefreshCw className="mr-1 h-3 w-3" />
|
|
535
|
+
)}
|
|
536
|
+
Detect
|
|
537
|
+
</Button>
|
|
538
|
+
<Button
|
|
539
|
+
type="button"
|
|
540
|
+
variant="outline"
|
|
541
|
+
size="sm"
|
|
542
|
+
onClick={addDevServer}
|
|
543
|
+
>
|
|
544
|
+
<Plus className="mr-1 h-3 w-3" />
|
|
545
|
+
Add
|
|
546
|
+
</Button>
|
|
547
|
+
</div>
|
|
548
|
+
</div>
|
|
549
|
+
|
|
550
|
+
{visibleDevServers.length === 0 ? (
|
|
551
|
+
<p className="text-muted-foreground py-2 text-sm">
|
|
552
|
+
No dev servers configured.
|
|
553
|
+
</p>
|
|
554
|
+
) : (
|
|
555
|
+
<div className="space-y-2">
|
|
556
|
+
{visibleDevServers.map((ds) => (
|
|
557
|
+
<div
|
|
558
|
+
key={ds.id}
|
|
559
|
+
className="bg-accent/30 space-y-2 rounded-lg p-3"
|
|
560
|
+
>
|
|
561
|
+
<div className="flex items-center gap-2">
|
|
562
|
+
<Input
|
|
563
|
+
value={ds.name}
|
|
564
|
+
onChange={(e) =>
|
|
565
|
+
updateDevServer(ds.id, { name: e.target.value })
|
|
566
|
+
}
|
|
567
|
+
placeholder="Server name"
|
|
568
|
+
className="h-8 flex-1"
|
|
569
|
+
/>
|
|
570
|
+
<Select
|
|
571
|
+
value={ds.type}
|
|
572
|
+
onValueChange={(v) =>
|
|
573
|
+
updateDevServer(ds.id, {
|
|
574
|
+
type: v as "node" | "docker",
|
|
575
|
+
})
|
|
576
|
+
}
|
|
577
|
+
>
|
|
578
|
+
<SelectTrigger className="h-8 w-24">
|
|
579
|
+
<SelectValue />
|
|
580
|
+
</SelectTrigger>
|
|
581
|
+
<SelectContent>
|
|
582
|
+
<SelectItem value="node">Node</SelectItem>
|
|
583
|
+
<SelectItem value="docker">Docker</SelectItem>
|
|
584
|
+
</SelectContent>
|
|
585
|
+
</Select>
|
|
586
|
+
<Button
|
|
587
|
+
type="button"
|
|
588
|
+
variant="ghost"
|
|
589
|
+
size="icon-sm"
|
|
590
|
+
onClick={() => removeDevServer(ds.id)}
|
|
591
|
+
className="text-red-500 hover:text-red-600"
|
|
592
|
+
>
|
|
593
|
+
<Trash2 className="h-3 w-3" />
|
|
594
|
+
</Button>
|
|
595
|
+
</div>
|
|
596
|
+
<Input
|
|
597
|
+
value={ds.command}
|
|
598
|
+
onChange={(e) =>
|
|
599
|
+
updateDevServer(ds.id, { command: e.target.value })
|
|
600
|
+
}
|
|
601
|
+
placeholder={
|
|
602
|
+
ds.type === "docker" ? "Service name" : "npm run dev"
|
|
603
|
+
}
|
|
604
|
+
className="h-8"
|
|
605
|
+
/>
|
|
606
|
+
<div className="flex gap-2">
|
|
607
|
+
<Input
|
|
608
|
+
type="number"
|
|
609
|
+
value={ds.port || ""}
|
|
610
|
+
onChange={(e) =>
|
|
611
|
+
updateDevServer(ds.id, {
|
|
612
|
+
port: e.target.value
|
|
613
|
+
? parseInt(e.target.value)
|
|
614
|
+
: undefined,
|
|
615
|
+
})
|
|
616
|
+
}
|
|
617
|
+
placeholder="Port"
|
|
618
|
+
className="h-8 w-24"
|
|
619
|
+
/>
|
|
620
|
+
<Input
|
|
621
|
+
value={ds.portEnvVar || ""}
|
|
622
|
+
onChange={(e) =>
|
|
623
|
+
updateDevServer(ds.id, {
|
|
624
|
+
portEnvVar: e.target.value,
|
|
625
|
+
})
|
|
626
|
+
}
|
|
627
|
+
placeholder="Port env var (e.g., PORT)"
|
|
628
|
+
className="h-8 flex-1"
|
|
629
|
+
/>
|
|
630
|
+
</div>
|
|
631
|
+
</div>
|
|
632
|
+
))}
|
|
633
|
+
</div>
|
|
634
|
+
)}
|
|
635
|
+
</div>
|
|
636
|
+
|
|
637
|
+
{/* Repositories */}
|
|
638
|
+
<div className="space-y-3">
|
|
639
|
+
<div className="flex items-center justify-between">
|
|
640
|
+
<label className="flex items-center gap-2 text-sm font-medium">
|
|
641
|
+
<GitBranch className="h-4 w-4" />
|
|
642
|
+
Git Repositories
|
|
643
|
+
</label>
|
|
644
|
+
<div className="flex gap-1">
|
|
645
|
+
<Button
|
|
646
|
+
type="button"
|
|
647
|
+
variant="outline"
|
|
648
|
+
size="sm"
|
|
649
|
+
onClick={detectRepositories}
|
|
650
|
+
disabled={isDetectingRepos || !workingDirectory}
|
|
651
|
+
>
|
|
652
|
+
{isDetectingRepos ? (
|
|
653
|
+
<Loader2 className="mr-1 h-3 w-3 animate-spin" />
|
|
654
|
+
) : (
|
|
655
|
+
<RefreshCw className="mr-1 h-3 w-3" />
|
|
656
|
+
)}
|
|
657
|
+
Detect
|
|
658
|
+
</Button>
|
|
659
|
+
<Button
|
|
660
|
+
type="button"
|
|
661
|
+
variant="outline"
|
|
662
|
+
size="sm"
|
|
663
|
+
onClick={addRepository}
|
|
664
|
+
>
|
|
665
|
+
<Plus className="mr-1 h-3 w-3" />
|
|
666
|
+
Add
|
|
667
|
+
</Button>
|
|
668
|
+
</div>
|
|
669
|
+
</div>
|
|
670
|
+
|
|
671
|
+
{visibleRepositories.length === 0 ? (
|
|
672
|
+
<p className="text-muted-foreground py-2 text-sm">
|
|
673
|
+
No repositories configured. Git changes will use the working
|
|
674
|
+
directory.
|
|
675
|
+
</p>
|
|
676
|
+
) : (
|
|
677
|
+
<div className="space-y-2">
|
|
678
|
+
{visibleRepositories.map((repo) => (
|
|
679
|
+
<div
|
|
680
|
+
key={repo.id}
|
|
681
|
+
className="bg-accent/30 space-y-2 rounded-lg p-3"
|
|
682
|
+
>
|
|
683
|
+
<div className="flex items-center gap-2">
|
|
684
|
+
<Input
|
|
685
|
+
value={repo.name}
|
|
686
|
+
onChange={(e) =>
|
|
687
|
+
updateRepository(repo.id, { name: e.target.value })
|
|
688
|
+
}
|
|
689
|
+
placeholder="Repository name"
|
|
690
|
+
className="h-8 flex-1"
|
|
691
|
+
/>
|
|
692
|
+
<Button
|
|
693
|
+
type="button"
|
|
694
|
+
variant={repo.isPrimary ? "default" : "ghost"}
|
|
695
|
+
size="icon-sm"
|
|
696
|
+
onClick={() => setRepositoryPrimary(repo.id)}
|
|
697
|
+
title={
|
|
698
|
+
repo.isPrimary
|
|
699
|
+
? "Primary repository"
|
|
700
|
+
: "Set as primary"
|
|
701
|
+
}
|
|
702
|
+
className={repo.isPrimary ? "text-yellow-500" : ""}
|
|
703
|
+
>
|
|
704
|
+
<Star
|
|
705
|
+
className={`h-3 w-3 ${repo.isPrimary ? "fill-current" : ""}`}
|
|
706
|
+
/>
|
|
707
|
+
</Button>
|
|
708
|
+
<Button
|
|
709
|
+
type="button"
|
|
710
|
+
variant="ghost"
|
|
711
|
+
size="icon-sm"
|
|
712
|
+
onClick={() => removeRepository(repo.id)}
|
|
713
|
+
className="text-red-500 hover:text-red-600"
|
|
714
|
+
>
|
|
715
|
+
<Trash2 className="h-3 w-3" />
|
|
716
|
+
</Button>
|
|
717
|
+
</div>
|
|
718
|
+
<div className="flex gap-2">
|
|
719
|
+
<Input
|
|
720
|
+
value={repo.path}
|
|
721
|
+
onChange={(e) =>
|
|
722
|
+
updateRepository(repo.id, { path: e.target.value })
|
|
723
|
+
}
|
|
724
|
+
placeholder="~/path/to/repository"
|
|
725
|
+
className="h-8 flex-1"
|
|
726
|
+
/>
|
|
727
|
+
<Button
|
|
728
|
+
type="button"
|
|
729
|
+
variant="outline"
|
|
730
|
+
size="icon-sm"
|
|
731
|
+
onClick={() => setFolderPickerRepoId(repo.id)}
|
|
732
|
+
title="Browse folders"
|
|
733
|
+
>
|
|
734
|
+
<FolderOpen className="h-3.5 w-3.5" />
|
|
735
|
+
</Button>
|
|
736
|
+
</div>
|
|
737
|
+
</div>
|
|
738
|
+
))}
|
|
739
|
+
</div>
|
|
740
|
+
)}
|
|
741
|
+
<p className="text-muted-foreground text-xs">
|
|
742
|
+
Configure multiple git repositories to track changes across
|
|
743
|
+
repos.
|
|
744
|
+
</p>
|
|
745
|
+
</div>
|
|
746
|
+
|
|
747
|
+
{error && <p className="text-sm text-red-500">{error}</p>}
|
|
748
|
+
|
|
749
|
+
<DialogFooter>
|
|
750
|
+
<Button type="button" variant="outline" onClick={handleClose}>
|
|
751
|
+
Cancel
|
|
752
|
+
</Button>
|
|
753
|
+
<Button type="submit" disabled={isLoading}>
|
|
754
|
+
{isLoading ? "Saving..." : "Save Changes"}
|
|
755
|
+
</Button>
|
|
756
|
+
</DialogFooter>
|
|
757
|
+
</form>
|
|
758
|
+
</DialogContent>
|
|
759
|
+
</Dialog>
|
|
760
|
+
|
|
761
|
+
{/* Folder Picker for repository path */}
|
|
762
|
+
{folderPickerRepoId && (
|
|
763
|
+
<FolderPicker
|
|
764
|
+
key={folderPickerRepoId}
|
|
765
|
+
initialPath={
|
|
766
|
+
repositories.find((r) => r.id === folderPickerRepoId)?.path ||
|
|
767
|
+
workingDirectory ||
|
|
768
|
+
"~"
|
|
769
|
+
}
|
|
770
|
+
onSelect={(path) => {
|
|
771
|
+
// Capture repoId immediately - must be done before any state updates
|
|
772
|
+
const repoId = folderPickerRepoId;
|
|
773
|
+
if (!repoId) return;
|
|
774
|
+
|
|
775
|
+
// Auto-fill name from path if empty
|
|
776
|
+
const pathParts = path.split("/").filter(Boolean);
|
|
777
|
+
const name = pathParts[pathParts.length - 1] || "Repository";
|
|
778
|
+
|
|
779
|
+
// First close the picker
|
|
780
|
+
setFolderPickerRepoId(null);
|
|
781
|
+
|
|
782
|
+
// Then update the repository
|
|
783
|
+
setRepositories((prev) =>
|
|
784
|
+
prev.map((r) =>
|
|
785
|
+
r.id === repoId ? { ...r, path, name: r.name || name } : r
|
|
786
|
+
)
|
|
787
|
+
);
|
|
788
|
+
}}
|
|
789
|
+
onClose={() => {
|
|
790
|
+
// Capture repoId immediately
|
|
791
|
+
const repoId = folderPickerRepoId;
|
|
792
|
+
|
|
793
|
+
// First close the picker
|
|
794
|
+
setFolderPickerRepoId(null);
|
|
795
|
+
|
|
796
|
+
// If the repo has no path (user cancelled on new repo), remove it
|
|
797
|
+
if (repoId) {
|
|
798
|
+
setRepositories((prev) => {
|
|
799
|
+
const repo = prev.find((r) => r.id === repoId);
|
|
800
|
+
if (repo?.isNew && !repo.path) {
|
|
801
|
+
return prev.filter((r) => r.id !== repoId);
|
|
802
|
+
}
|
|
803
|
+
return prev;
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
}}
|
|
807
|
+
/>
|
|
808
|
+
)}
|
|
809
|
+
</>
|
|
810
|
+
);
|
|
811
|
+
}
|