@hienlh/ppm 0.2.20 → 0.2.21
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/CHANGELOG.md +31 -0
- package/CLAUDE.md +18 -1
- package/bun.lock +57 -59
- package/dist/ppm +0 -0
- package/dist/web/assets/chat-tab-C_U7EwM9.js +6 -0
- package/dist/web/assets/code-editor-DuarTBEe.js +1 -0
- package/dist/web/assets/diff-viewer-sBWBgb7U.js +4 -0
- package/dist/web/assets/git-graph-fOKEZiot.js +1 -0
- package/dist/web/assets/index-3zt5mBwZ.css +2 -0
- package/dist/web/assets/index-CaUQy3Zs.js +21 -0
- package/dist/web/assets/input-CTnwfHVN.js +41 -0
- package/dist/web/assets/settings-tab-C5aWMqIA.js +1 -0
- package/dist/web/assets/{terminal-tab-sGlqTp7k.js → terminal-tab-BEFAYT4S.js} +1 -1
- package/dist/web/assets/use-monaco-theme-BxaccPmI.js +11 -0
- package/dist/web/index.html +35 -8
- package/dist/web/sw.js +1 -1
- package/docs/codebase-summary.md +13 -8
- package/docs/project-roadmap.md +22 -4
- package/docs/system-architecture.md +59 -0
- package/package.json +6 -14
- package/src/providers/claude-agent-sdk.ts +2 -2
- package/src/providers/registry.ts +12 -11
- package/src/server/routes/projects.ts +43 -0
- package/src/server/routes/settings.ts +42 -8
- package/src/server/ws/chat.ts +2 -2
- package/src/services/config.service.ts +5 -1
- package/src/services/project.service.ts +1 -0
- package/src/types/config.ts +37 -0
- package/src/types/project.ts +1 -0
- package/src/web/Users/hienlh/Projects/ppm/dist/web/monacoeditorwork/css.worker.bundle.js +54268 -0
- package/src/web/Users/hienlh/Projects/ppm/dist/web/monacoeditorwork/editor.worker.bundle.js +14316 -0
- package/src/web/Users/hienlh/Projects/ppm/dist/web/monacoeditorwork/html.worker.bundle.js +30452 -0
- package/src/web/Users/hienlh/Projects/ppm/dist/web/monacoeditorwork/json.worker.bundle.js +22095 -0
- package/src/web/Users/hienlh/Projects/ppm/dist/web/monacoeditorwork/ts.worker.bundle.js +225957 -0
- package/src/web/app.tsx +43 -5
- package/src/web/components/chat/chat-history-panel.tsx +106 -0
- package/src/web/components/chat/chat-tab.tsx +27 -19
- package/src/web/components/editor/code-editor.tsx +78 -197
- package/src/web/components/editor/diff-viewer.tsx +59 -176
- package/src/web/components/layout/add-project-form.tsx +151 -0
- package/src/web/components/layout/command-palette.tsx +3 -1
- package/src/web/components/layout/editor-panel.tsx +6 -4
- package/src/web/components/layout/mobile-drawer.tsx +48 -180
- package/src/web/components/layout/mobile-nav.tsx +89 -6
- package/src/web/components/layout/panel-layout.tsx +16 -10
- package/src/web/components/layout/project-bar.tsx +329 -0
- package/src/web/components/layout/project-bottom-sheet.tsx +345 -0
- package/src/web/components/layout/sidebar.tsx +56 -142
- package/src/web/components/layout/tab-bar.tsx +1 -6
- package/src/web/components/layout/tab-content.tsx +0 -10
- package/src/web/components/ui/dialog.tsx +1 -1
- package/src/web/lib/project-avatar.ts +45 -0
- package/src/web/lib/project-palette.ts +18 -0
- package/src/web/lib/use-monaco-theme.ts +29 -0
- package/src/web/stores/panel-store.ts +96 -9
- package/src/web/stores/project-store.ts +87 -3
- package/src/web/stores/settings-store.ts +31 -4
- package/src/web/stores/tab-store.ts +0 -2
- package/vite.config.ts +6 -2
- package/dist/web/assets/arrow-up-from-line-DjfWTP75.js +0 -1
- package/dist/web/assets/button-CQ5h5gxS.js +0 -41
- package/dist/web/assets/chat-tab-Cfw__7vJ.js +0 -6
- package/dist/web/assets/code-editor-D8Pz69sx.js +0 -2
- package/dist/web/assets/dialog-BL9i7XEo.js +0 -5
- package/dist/web/assets/diff-viewer-CWS5n7ur.js +0 -4
- package/dist/web/assets/dist-0XHv8Vwc.js +0 -1
- package/dist/web/assets/dist-Ca3N8Xbh.js +0 -46
- package/dist/web/assets/git-graph-DwA62J8-.js +0 -1
- package/dist/web/assets/git-status-panel-DaB-zzSF.js +0 -1
- package/dist/web/assets/index-BYIXPY6U.css +0 -2
- package/dist/web/assets/index-DbTCLiox.js +0 -17
- package/dist/web/assets/project-list-Z4lhtp6P.js +0 -1
- package/dist/web/assets/refresh-cw-S6I91MHO.js +0 -1
- package/dist/web/assets/settings-tab-BW6MGcir.js +0 -1
- package/dist/web/assets/trash-2-CGlFXde_.js +0 -1
- package/dist/web/assets/x-C0Rw5Giw.js +0 -1
- /package/dist/web/assets/{api-client-DzH9zCD7.js → api-client-BCjah751.js} +0 -0
- /package/dist/web/assets/{columns-2-DsiY76NQ.js → columns-2-DFQ3yid7.js} +0 -0
- /package/dist/web/assets/{copy-D_Q54D-v.js → copy-B-kLwqzg.js} +0 -0
- /package/dist/web/assets/{external-link-C6Y-D528.js → external-link-Dim3NH6h.js} +0 -0
- /package/dist/web/assets/{marked.esm-Cv8mjgnt.js → marked.esm-DhBtkBa8.js} +0 -0
- /package/dist/web/assets/{utils-D6me7KDg.js → utils-B-_GCz7E.js} +0 -0
|
@@ -1,43 +1,22 @@
|
|
|
1
|
-
import { useEffect, useState, useMemo
|
|
2
|
-
import {
|
|
3
|
-
import { EditorView, lineNumbers } from "@codemirror/view";
|
|
4
|
-
import { EditorState } from "@codemirror/state";
|
|
5
|
-
import { MergeView } from "@codemirror/merge";
|
|
6
|
-
import { javascript } from "@codemirror/lang-javascript";
|
|
7
|
-
import { python } from "@codemirror/lang-python";
|
|
8
|
-
import { html } from "@codemirror/lang-html";
|
|
9
|
-
import { css } from "@codemirror/lang-css";
|
|
10
|
-
import { json } from "@codemirror/lang-json";
|
|
11
|
-
import { markdown } from "@codemirror/lang-markdown";
|
|
12
|
-
import type { Extension } from "@codemirror/state";
|
|
1
|
+
import { useEffect, useState, useMemo } from "react";
|
|
2
|
+
import { DiffEditor } from "@monaco-editor/react";
|
|
13
3
|
import { api, projectUrl } from "@/lib/api-client";
|
|
14
4
|
import { useSettingsStore } from "@/stores/settings-store";
|
|
5
|
+
import { useMonacoTheme } from "@/lib/use-monaco-theme";
|
|
15
6
|
import { Loader2, FileCode, PanelLeftOpen, PanelRightOpen, Columns2, WrapText } from "lucide-react";
|
|
16
7
|
|
|
17
|
-
function
|
|
8
|
+
function getMonacoLanguage(filename: string): string {
|
|
18
9
|
const ext = filename.split(".").pop()?.toLowerCase() ?? "";
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return html();
|
|
30
|
-
case "css":
|
|
31
|
-
case "scss":
|
|
32
|
-
return css();
|
|
33
|
-
case "json":
|
|
34
|
-
return json();
|
|
35
|
-
case "md":
|
|
36
|
-
case "mdx":
|
|
37
|
-
return markdown();
|
|
38
|
-
default:
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
10
|
+
const map: Record<string, string> = {
|
|
11
|
+
js: "javascript", jsx: "javascript",
|
|
12
|
+
ts: "typescript", tsx: "typescript",
|
|
13
|
+
py: "python", html: "html",
|
|
14
|
+
css: "css", scss: "scss",
|
|
15
|
+
json: "json", md: "markdown", mdx: "markdown",
|
|
16
|
+
yaml: "yaml", yml: "yaml",
|
|
17
|
+
sh: "shell", bash: "shell",
|
|
18
|
+
};
|
|
19
|
+
return map[ext] ?? "plaintext";
|
|
41
20
|
}
|
|
42
21
|
|
|
43
22
|
interface DiffViewerProps {
|
|
@@ -60,25 +39,21 @@ export function DiffViewer({ metadata }: DiffViewerProps) {
|
|
|
60
39
|
const [fileContents, setFileContents] = useState<{ original: string; modified: string } | null>(null);
|
|
61
40
|
const [loading, setLoading] = useState(!isInline);
|
|
62
41
|
const [error, setError] = useState<string | null>(null);
|
|
63
|
-
/** "both" | "left" | "right" — controls which diff panel is expanded */
|
|
64
42
|
const [expandMode, setExpandMode] = useState<"both" | "left" | "right">("both");
|
|
65
43
|
const { wordWrap, toggleWordWrap } = useSettingsStore();
|
|
66
|
-
const
|
|
67
|
-
const mergeViewRef = useRef<MergeView | null>(null);
|
|
44
|
+
const monacoTheme = useMonacoTheme();
|
|
68
45
|
|
|
69
46
|
useEffect(() => {
|
|
70
|
-
if (isInline) return;
|
|
47
|
+
if (isInline) return;
|
|
71
48
|
if (!projectName) return;
|
|
72
49
|
setLoading(true);
|
|
73
50
|
setError(null);
|
|
74
51
|
|
|
75
52
|
if (file1 && file2) {
|
|
76
|
-
const params = new URLSearchParams();
|
|
77
|
-
params.set("file1", file1);
|
|
78
|
-
params.set("file2", file2);
|
|
53
|
+
const params = new URLSearchParams({ file1, file2 });
|
|
79
54
|
api
|
|
80
55
|
.get<{ original: string; modified: string }>(
|
|
81
|
-
`${projectUrl(projectName)}/files/compare?${params
|
|
56
|
+
`${projectUrl(projectName)}/files/compare?${params}`,
|
|
82
57
|
)
|
|
83
58
|
.then((data) => { setFileContents(data); setLoading(false); })
|
|
84
59
|
.catch((err) => { setError(err instanceof Error ? err.message : "Failed to compare files"); setLoading(false); });
|
|
@@ -87,15 +62,14 @@ export function DiffViewer({ metadata }: DiffViewerProps) {
|
|
|
87
62
|
|
|
88
63
|
let url: string;
|
|
89
64
|
if (filePath) {
|
|
90
|
-
const params = new URLSearchParams();
|
|
91
|
-
params.set("file", filePath);
|
|
65
|
+
const params = new URLSearchParams({ file: filePath });
|
|
92
66
|
if (ref1) params.set("ref", ref1);
|
|
93
|
-
url = `${projectUrl(projectName)}/git/file-diff?${params
|
|
67
|
+
url = `${projectUrl(projectName)}/git/file-diff?${params}`;
|
|
94
68
|
} else if (ref1 || ref2) {
|
|
95
69
|
const params = new URLSearchParams();
|
|
96
70
|
if (ref1) params.set("ref1", ref1);
|
|
97
71
|
if (ref2) params.set("ref2", ref2);
|
|
98
|
-
url = `${projectUrl(projectName)}/git/diff?${params
|
|
72
|
+
url = `${projectUrl(projectName)}/git/diff?${params}`;
|
|
99
73
|
} else {
|
|
100
74
|
url = `${projectUrl(projectName)}/git/diff`;
|
|
101
75
|
}
|
|
@@ -104,7 +78,7 @@ export function DiffViewer({ metadata }: DiffViewerProps) {
|
|
|
104
78
|
.get<{ diff: string }>(url)
|
|
105
79
|
.then((data) => { setDiffText(data.diff); setLoading(false); })
|
|
106
80
|
.catch((err) => { setError(err instanceof Error ? err.message : "Failed to load diff"); setLoading(false); });
|
|
107
|
-
}, [filePath, projectName, ref1, ref2, file1, file2]);
|
|
81
|
+
}, [filePath, projectName, ref1, ref2, file1, file2, isInline]);
|
|
108
82
|
|
|
109
83
|
const { original, modified } = useMemo(() => {
|
|
110
84
|
if (isInline) return { original: inlineOriginal ?? "", modified: inlineModified ?? "" };
|
|
@@ -113,105 +87,11 @@ export function DiffViewer({ metadata }: DiffViewerProps) {
|
|
|
113
87
|
return parseDiff(diffText);
|
|
114
88
|
}, [diffText, isInline, inlineOriginal, inlineModified, isFileCompare, fileContents]);
|
|
115
89
|
|
|
116
|
-
const
|
|
90
|
+
const language = useMemo(() => {
|
|
117
91
|
const langFile = filePath ?? file2 ?? file1;
|
|
118
|
-
|
|
119
|
-
const ext = getLanguageExtension(langFile);
|
|
120
|
-
return ext ? [ext] : [];
|
|
92
|
+
return langFile ? getMonacoLanguage(langFile) : "plaintext";
|
|
121
93
|
}, [filePath, file1, file2]);
|
|
122
94
|
|
|
123
|
-
// Create MergeView when content is ready
|
|
124
|
-
useEffect(() => {
|
|
125
|
-
const container = containerRef.current;
|
|
126
|
-
if (!container || loading || error) return;
|
|
127
|
-
if (!original && !modified) return;
|
|
128
|
-
|
|
129
|
-
// Clean up previous
|
|
130
|
-
if (mergeViewRef.current) {
|
|
131
|
-
mergeViewRef.current.destroy();
|
|
132
|
-
mergeViewRef.current = null;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const isMobile = window.innerWidth < 768;
|
|
136
|
-
const sharedExts: Extension[] = [
|
|
137
|
-
...langExts,
|
|
138
|
-
oneDark,
|
|
139
|
-
EditorView.editable.of(false),
|
|
140
|
-
EditorState.readOnly.of(true),
|
|
141
|
-
lineNumbers(),
|
|
142
|
-
...(wordWrap ? [EditorView.lineWrapping] : []),
|
|
143
|
-
EditorView.theme({
|
|
144
|
-
"&": { fontSize: "13px", fontFamily: "var(--font-mono)" },
|
|
145
|
-
// Character-level highlight: bold background, NO underline
|
|
146
|
-
"& .cm-changedText": {
|
|
147
|
-
textDecoration: "none !important",
|
|
148
|
-
borderBottom: "none !important",
|
|
149
|
-
textDecorationLine: "none !important",
|
|
150
|
-
backgroundColor: "rgba(16, 185, 129, 0.4) !important",
|
|
151
|
-
borderRadius: "2px",
|
|
152
|
-
},
|
|
153
|
-
"& .cm-deletedChunk .cm-changedText": {
|
|
154
|
-
backgroundColor: "rgba(239, 68, 68, 0.4) !important",
|
|
155
|
-
},
|
|
156
|
-
}),
|
|
157
|
-
];
|
|
158
|
-
|
|
159
|
-
const mv = new MergeView({
|
|
160
|
-
parent: container,
|
|
161
|
-
a: { doc: original, extensions: sharedExts },
|
|
162
|
-
b: { doc: modified, extensions: sharedExts },
|
|
163
|
-
orientation: "a-b",
|
|
164
|
-
revertControls: undefined,
|
|
165
|
-
highlightChanges: true, // Highlight changed characters within a line
|
|
166
|
-
gutter: true,
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
mergeViewRef.current = mv;
|
|
170
|
-
|
|
171
|
-
// Sync horizontal scroll between both editors
|
|
172
|
-
const scrollerA = mv.a.dom.querySelector(".cm-scroller") as HTMLElement | null;
|
|
173
|
-
const scrollerB = mv.b.dom.querySelector(".cm-scroller") as HTMLElement | null;
|
|
174
|
-
let syncing = false;
|
|
175
|
-
const syncScroll = (source: HTMLElement, target: HTMLElement) => {
|
|
176
|
-
if (syncing) return;
|
|
177
|
-
syncing = true;
|
|
178
|
-
target.scrollLeft = source.scrollLeft;
|
|
179
|
-
syncing = false;
|
|
180
|
-
};
|
|
181
|
-
const onScrollA = () => scrollerA && scrollerB && syncScroll(scrollerA, scrollerB);
|
|
182
|
-
const onScrollB = () => scrollerA && scrollerB && syncScroll(scrollerB, scrollerA);
|
|
183
|
-
scrollerA?.addEventListener("scroll", onScrollA);
|
|
184
|
-
scrollerB?.addEventListener("scroll", onScrollB);
|
|
185
|
-
|
|
186
|
-
return () => {
|
|
187
|
-
scrollerA?.removeEventListener("scroll", onScrollA);
|
|
188
|
-
scrollerB?.removeEventListener("scroll", onScrollB);
|
|
189
|
-
mv.destroy();
|
|
190
|
-
mergeViewRef.current = null;
|
|
191
|
-
};
|
|
192
|
-
}, [original, modified, langExts, loading, error, wordWrap]);
|
|
193
|
-
|
|
194
|
-
// Apply expand mode by hiding left/right editor panels via CSS
|
|
195
|
-
useEffect(() => {
|
|
196
|
-
const mv = mergeViewRef.current;
|
|
197
|
-
if (!mv) return;
|
|
198
|
-
// MergeView exposes .a (left) and .b (right) EditorView instances
|
|
199
|
-
const leftPanel = mv.a.dom.closest(".cm-mergeViewEditor") as HTMLElement | null ?? mv.a.dom.parentElement;
|
|
200
|
-
const rightPanel = mv.b.dom.closest(".cm-mergeViewEditor") as HTMLElement | null ?? mv.b.dom.parentElement;
|
|
201
|
-
const container = containerRef.current;
|
|
202
|
-
const gutter = container?.querySelector<HTMLElement>(".cm-mergeViewGutter");
|
|
203
|
-
|
|
204
|
-
if (leftPanel) {
|
|
205
|
-
leftPanel.style.display = expandMode === "right" ? "none" : "";
|
|
206
|
-
leftPanel.style.flex = expandMode === "left" ? "1" : "";
|
|
207
|
-
}
|
|
208
|
-
if (rightPanel) {
|
|
209
|
-
rightPanel.style.display = expandMode === "left" ? "none" : "";
|
|
210
|
-
rightPanel.style.flex = expandMode === "right" ? "1" : "";
|
|
211
|
-
}
|
|
212
|
-
if (gutter) gutter.style.display = expandMode !== "both" ? "none" : "";
|
|
213
|
-
}, [expandMode, original, modified]);
|
|
214
|
-
|
|
215
95
|
if (!projectName && !isInline) {
|
|
216
96
|
return (
|
|
217
97
|
<div className="flex items-center justify-center h-full text-muted-foreground text-sm">
|
|
@@ -231,9 +111,7 @@ export function DiffViewer({ metadata }: DiffViewerProps) {
|
|
|
231
111
|
|
|
232
112
|
if (error) {
|
|
233
113
|
return (
|
|
234
|
-
<div className="flex items-center justify-center h-full text-destructive text-sm">
|
|
235
|
-
{error}
|
|
236
|
-
</div>
|
|
114
|
+
<div className="flex items-center justify-center h-full text-destructive text-sm">{error}</div>
|
|
237
115
|
);
|
|
238
116
|
}
|
|
239
117
|
|
|
@@ -247,26 +125,26 @@ export function DiffViewer({ metadata }: DiffViewerProps) {
|
|
|
247
125
|
);
|
|
248
126
|
}
|
|
249
127
|
|
|
128
|
+
// expandMode left/right → inline diff (Monaco has no single-side mode)
|
|
129
|
+
const renderSideBySide = expandMode === "both";
|
|
130
|
+
|
|
250
131
|
const expandToggle = (
|
|
251
132
|
<div className="flex items-center gap-0.5 shrink-0">
|
|
252
|
-
<button
|
|
253
|
-
type="button"
|
|
133
|
+
<button type="button"
|
|
254
134
|
onClick={() => setExpandMode(expandMode === "left" ? "both" : "left")}
|
|
255
135
|
className={`p-1 rounded hover:bg-muted transition-colors ${expandMode === "left" ? "bg-muted text-foreground" : ""}`}
|
|
256
136
|
title="Expand original"
|
|
257
137
|
>
|
|
258
138
|
<PanelLeftOpen className="size-3.5" />
|
|
259
139
|
</button>
|
|
260
|
-
<button
|
|
261
|
-
type="button"
|
|
140
|
+
<button type="button"
|
|
262
141
|
onClick={() => setExpandMode("both")}
|
|
263
142
|
className={`p-1 rounded hover:bg-muted transition-colors ${expandMode === "both" ? "bg-muted text-foreground" : ""}`}
|
|
264
143
|
title="Side by side"
|
|
265
144
|
>
|
|
266
145
|
<Columns2 className="size-3.5" />
|
|
267
146
|
</button>
|
|
268
|
-
<button
|
|
269
|
-
type="button"
|
|
147
|
+
<button type="button"
|
|
270
148
|
onClick={() => setExpandMode(expandMode === "right" ? "both" : "right")}
|
|
271
149
|
className={`p-1 rounded hover:bg-muted transition-colors ${expandMode === "right" ? "bg-muted text-foreground" : ""}`}
|
|
272
150
|
title="Expand modified"
|
|
@@ -274,10 +152,7 @@ export function DiffViewer({ metadata }: DiffViewerProps) {
|
|
|
274
152
|
<PanelRightOpen className="size-3.5" />
|
|
275
153
|
</button>
|
|
276
154
|
<div className="w-px h-3.5 bg-border mx-0.5 shrink-0" />
|
|
277
|
-
<button
|
|
278
|
-
type="button"
|
|
279
|
-
onClick={toggleWordWrap}
|
|
280
|
-
title="Toggle word wrap"
|
|
155
|
+
<button type="button" onClick={toggleWordWrap} title="Toggle word wrap"
|
|
281
156
|
className={`p-1 rounded hover:bg-muted transition-colors ${wordWrap ? "bg-muted text-foreground" : ""}`}
|
|
282
157
|
>
|
|
283
158
|
<WrapText className="size-3.5" />
|
|
@@ -300,18 +175,31 @@ export function DiffViewer({ metadata }: DiffViewerProps) {
|
|
|
300
175
|
)}
|
|
301
176
|
</span>
|
|
302
177
|
)}
|
|
303
|
-
{/* Desktop: expand toggle in header */}
|
|
304
178
|
<div className="hidden md:block">{expandToggle}</div>
|
|
305
179
|
</div>
|
|
306
180
|
|
|
307
|
-
{/*
|
|
308
|
-
<div
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
181
|
+
{/* Monaco DiffEditor */}
|
|
182
|
+
<div className="flex-1 overflow-hidden">
|
|
183
|
+
<DiffEditor
|
|
184
|
+
height="100%"
|
|
185
|
+
language={language}
|
|
186
|
+
original={original}
|
|
187
|
+
modified={modified}
|
|
188
|
+
theme={monacoTheme}
|
|
189
|
+
options={{
|
|
190
|
+
fontSize: 13,
|
|
191
|
+
fontFamily: "Menlo, Monaco, Consolas, monospace",
|
|
192
|
+
wordWrap: wordWrap ? "on" : "off",
|
|
193
|
+
renderSideBySide,
|
|
194
|
+
readOnly: true,
|
|
195
|
+
automaticLayout: true,
|
|
196
|
+
scrollBeyondLastLine: false,
|
|
197
|
+
}}
|
|
198
|
+
loading={<Loader2 className="size-5 animate-spin text-text-subtle" />}
|
|
199
|
+
/>
|
|
200
|
+
</div>
|
|
313
201
|
|
|
314
|
-
{/* Mobile
|
|
202
|
+
{/* Mobile expand toggle */}
|
|
315
203
|
<div className="md:hidden flex justify-center border-t py-1 bg-background shrink-0">
|
|
316
204
|
{expandToggle}
|
|
317
205
|
</div>
|
|
@@ -327,16 +215,11 @@ function parseDiff(diff: string): { original: string; modified: string } {
|
|
|
327
215
|
|
|
328
216
|
for (const line of lines) {
|
|
329
217
|
if (
|
|
330
|
-
line.startsWith("diff --git") ||
|
|
331
|
-
line.startsWith("
|
|
332
|
-
line.startsWith("
|
|
333
|
-
line.startsWith("new
|
|
334
|
-
line.startsWith("
|
|
335
|
-
line.startsWith("old mode") ||
|
|
336
|
-
line.startsWith("new mode") ||
|
|
337
|
-
line.startsWith("---") ||
|
|
338
|
-
line.startsWith("+++") ||
|
|
339
|
-
line.startsWith("Binary files") ||
|
|
218
|
+
line.startsWith("diff --git") || line.startsWith("diff --no-index") ||
|
|
219
|
+
line.startsWith("index ") || line.startsWith("new file") ||
|
|
220
|
+
line.startsWith("deleted file") || line.startsWith("old mode") ||
|
|
221
|
+
line.startsWith("new mode") || line.startsWith("---") ||
|
|
222
|
+
line.startsWith("+++") || line.startsWith("Binary files") ||
|
|
340
223
|
line.startsWith("\\ No newline")
|
|
341
224
|
) continue;
|
|
342
225
|
|
|
@@ -347,7 +230,7 @@ function parseDiff(diff: string): { original: string; modified: string } {
|
|
|
347
230
|
originalLines.push(line.slice(1));
|
|
348
231
|
} else if (line.startsWith("+")) {
|
|
349
232
|
modifiedLines.push(line.slice(1));
|
|
350
|
-
} else
|
|
233
|
+
} else {
|
|
351
234
|
const content = line.startsWith(" ") ? line.slice(1) : line;
|
|
352
235
|
originalLines.push(content);
|
|
353
236
|
modifiedLines.push(content);
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from "react";
|
|
2
|
+
import { Loader2, FolderOpen } from "lucide-react";
|
|
3
|
+
import { useProjectStore } from "@/stores/project-store";
|
|
4
|
+
import { api } from "@/lib/api-client";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
|
|
7
|
+
interface SuggestedDir {
|
|
8
|
+
path: string;
|
|
9
|
+
name: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface AddProjectFormProps {
|
|
13
|
+
onSuccess: () => void;
|
|
14
|
+
onCancel: () => void;
|
|
15
|
+
/** Extra class for the submit button row */
|
|
16
|
+
footerClassName?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function AddProjectForm({ onSuccess, onCancel, footerClassName }: AddProjectFormProps) {
|
|
20
|
+
const { addProject } = useProjectStore();
|
|
21
|
+
const [path, setPath] = useState("");
|
|
22
|
+
const [name, setName] = useState("");
|
|
23
|
+
const [suggestions, setSuggestions] = useState<SuggestedDir[]>([]);
|
|
24
|
+
const [showSuggestions, setShowSuggestions] = useState(false);
|
|
25
|
+
const [loading, setLoading] = useState(false);
|
|
26
|
+
const [submitting, setSubmitting] = useState(false);
|
|
27
|
+
const [error, setError] = useState("");
|
|
28
|
+
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
29
|
+
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
30
|
+
|
|
31
|
+
// Fetch suggestions when path changes
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
34
|
+
if (!path.trim()) { setSuggestions([]); setShowSuggestions(false); return; }
|
|
35
|
+
debounceRef.current = setTimeout(async () => {
|
|
36
|
+
setLoading(true);
|
|
37
|
+
try {
|
|
38
|
+
const results = await api.get<SuggestedDir[]>(`/api/projects/suggest-dirs?q=${encodeURIComponent(path)}`);
|
|
39
|
+
setSuggestions(results ?? []);
|
|
40
|
+
setShowSuggestions(true);
|
|
41
|
+
} catch {
|
|
42
|
+
setSuggestions([]);
|
|
43
|
+
} finally {
|
|
44
|
+
setLoading(false);
|
|
45
|
+
}
|
|
46
|
+
}, 250);
|
|
47
|
+
}, [path]);
|
|
48
|
+
|
|
49
|
+
// Close suggestions on outside click
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
function handle(e: MouseEvent) {
|
|
52
|
+
if (wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) {
|
|
53
|
+
setShowSuggestions(false);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
document.addEventListener("mousedown", handle);
|
|
57
|
+
return () => document.removeEventListener("mousedown", handle);
|
|
58
|
+
}, []);
|
|
59
|
+
|
|
60
|
+
function selectSuggestion(dir: SuggestedDir) {
|
|
61
|
+
setPath(dir.path);
|
|
62
|
+
if (!name) setName(dir.name);
|
|
63
|
+
setShowSuggestions(false);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function handleSubmit(e?: React.FormEvent) {
|
|
67
|
+
e?.preventDefault();
|
|
68
|
+
if (!path.trim()) { setError("Path is required"); return; }
|
|
69
|
+
setError("");
|
|
70
|
+
setSubmitting(true);
|
|
71
|
+
try {
|
|
72
|
+
await addProject(path.trim(), name.trim() || undefined);
|
|
73
|
+
onSuccess();
|
|
74
|
+
} catch (err) {
|
|
75
|
+
setError(err instanceof Error ? err.message : "Failed to add project");
|
|
76
|
+
} finally {
|
|
77
|
+
setSubmitting(false);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<form onSubmit={handleSubmit} className="flex flex-col gap-3">
|
|
83
|
+
{/* Path input with suggestions */}
|
|
84
|
+
<div ref={wrapperRef} className="relative">
|
|
85
|
+
<label className="block text-xs font-medium text-foreground mb-1">Project path</label>
|
|
86
|
+
<div className="relative flex items-center">
|
|
87
|
+
<FolderOpen className="absolute left-2.5 size-3.5 text-text-subtle pointer-events-none" />
|
|
88
|
+
<input
|
|
89
|
+
type="text"
|
|
90
|
+
value={path}
|
|
91
|
+
onChange={(e) => { setPath(e.target.value); setError(""); }}
|
|
92
|
+
onFocus={() => suggestions.length > 0 && setShowSuggestions(true)}
|
|
93
|
+
placeholder="/path/to/project"
|
|
94
|
+
className="w-full pl-8 pr-3 py-2 rounded-md border border-border bg-background text-sm focus:outline-none focus:ring-1 focus:ring-primary"
|
|
95
|
+
autoFocus
|
|
96
|
+
autoComplete="off"
|
|
97
|
+
/>
|
|
98
|
+
{loading && <Loader2 className="absolute right-2.5 size-3.5 text-text-subtle animate-spin" />}
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
{/* Suggestions dropdown */}
|
|
102
|
+
{showSuggestions && suggestions.length > 0 && (
|
|
103
|
+
<div className="absolute top-full left-0 right-0 mt-1 z-10 bg-popover border border-border rounded-md shadow-md max-h-48 overflow-y-auto">
|
|
104
|
+
{suggestions.map((dir) => (
|
|
105
|
+
<button
|
|
106
|
+
key={dir.path}
|
|
107
|
+
type="button"
|
|
108
|
+
onMouseDown={() => selectSuggestion(dir)}
|
|
109
|
+
className="w-full flex flex-col items-start px-3 py-2 text-left hover:bg-accent/50 transition-colors"
|
|
110
|
+
>
|
|
111
|
+
<span className="text-sm font-medium truncate w-full">{dir.name}</span>
|
|
112
|
+
<span className="text-xs text-text-subtle truncate w-full">{dir.path}</span>
|
|
113
|
+
</button>
|
|
114
|
+
))}
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
{/* Optional name */}
|
|
120
|
+
<div>
|
|
121
|
+
<label className="block text-xs font-medium text-foreground mb-1">Display name <span className="text-muted-foreground">(optional)</span></label>
|
|
122
|
+
<input
|
|
123
|
+
type="text"
|
|
124
|
+
value={name}
|
|
125
|
+
onChange={(e) => setName(e.target.value)}
|
|
126
|
+
placeholder="my-project"
|
|
127
|
+
className="w-full px-3 py-2 rounded-md border border-border bg-background text-sm focus:outline-none focus:ring-1 focus:ring-primary"
|
|
128
|
+
/>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
{error && <p className="text-xs text-destructive">{error}</p>}
|
|
132
|
+
|
|
133
|
+
<div className={cn("flex justify-end gap-2 pt-1", footerClassName)}>
|
|
134
|
+
<button
|
|
135
|
+
type="button"
|
|
136
|
+
onClick={onCancel}
|
|
137
|
+
className="px-3 py-1.5 text-sm text-text-secondary hover:text-foreground transition-colors"
|
|
138
|
+
>
|
|
139
|
+
Cancel
|
|
140
|
+
</button>
|
|
141
|
+
<button
|
|
142
|
+
type="submit"
|
|
143
|
+
disabled={submitting || !path.trim()}
|
|
144
|
+
className="px-3 py-1.5 text-sm bg-primary text-white rounded-md hover:bg-primary/90 transition-colors disabled:opacity-50"
|
|
145
|
+
>
|
|
146
|
+
{submitting ? "Adding…" : "Add Project"}
|
|
147
|
+
</button>
|
|
148
|
+
</div>
|
|
149
|
+
</form>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "lucide-react";
|
|
11
11
|
import { useTabStore, type TabType } from "@/stores/tab-store";
|
|
12
12
|
import { useProjectStore } from "@/stores/project-store";
|
|
13
|
+
import { useSettingsStore } from "@/stores/settings-store";
|
|
13
14
|
import { useFileStore, type FileNode } from "@/stores/file-store";
|
|
14
15
|
|
|
15
16
|
interface CommandItem {
|
|
@@ -45,6 +46,7 @@ export function CommandPalette({ open, onClose }: { open: boolean; onClose: () =
|
|
|
45
46
|
const openTab = useTabStore((s) => s.openTab);
|
|
46
47
|
const activeProject = useProjectStore((s) => s.activeProject);
|
|
47
48
|
const fileTree = useFileStore((s) => s.tree);
|
|
49
|
+
const setSidebarActiveTab = useSettingsStore((s) => s.setSidebarActiveTab);
|
|
48
50
|
|
|
49
51
|
// Action commands
|
|
50
52
|
const actionCommands = useMemo<CommandItem[]>(() => {
|
|
@@ -60,7 +62,7 @@ export function CommandPalette({ open, onClose }: { open: boolean; onClose: () =
|
|
|
60
62
|
{ id: "terminal", label: "New Terminal", icon: Terminal, action: openNewTab("terminal", "Terminal"), keywords: "bash shell console", group: "action" },
|
|
61
63
|
{ id: "chat", label: "New AI Chat", icon: MessageSquare, action: openNewTab("chat", "AI Chat"), keywords: "ai assistant claude", group: "action" },
|
|
62
64
|
{ id: "git-graph", label: "Git Graph", icon: GitBranch, action: openNewTab("git-graph", "Git Graph"), keywords: "branch history log", group: "action" },
|
|
63
|
-
{ id: "git-status", label: "Git Status", icon: GitCommitHorizontal, action:
|
|
65
|
+
{ id: "git-status", label: "Git Status", icon: GitCommitHorizontal, action: () => { setSidebarActiveTab("git"); onClose(); }, keywords: "changes diff staged", group: "action" },
|
|
64
66
|
{ id: "settings", label: "Settings", icon: Settings, action: openNewTab("settings", "Settings"), keywords: "config preferences", group: "action" },
|
|
65
67
|
];
|
|
66
68
|
}, [activeProject, openTab, onClose]);
|
|
@@ -7,24 +7,26 @@ import { SplitDropOverlay } from "./split-drop-overlay";
|
|
|
7
7
|
import { cn } from "@/lib/utils";
|
|
8
8
|
|
|
9
9
|
const TAB_COMPONENTS: Record<TabType, React.LazyExoticComponent<React.ComponentType<{ metadata?: Record<string, unknown>; tabId?: string }>>> = {
|
|
10
|
-
projects: lazy(() => import("@/components/projects/project-list").then((m) => ({ default: m.ProjectList }))),
|
|
11
10
|
terminal: lazy(() => import("@/components/terminal/terminal-tab").then((m) => ({ default: m.TerminalTab }))),
|
|
12
11
|
chat: lazy(() => import("@/components/chat/chat-tab").then((m) => ({ default: m.ChatTab }))),
|
|
13
12
|
editor: lazy(() => import("@/components/editor/code-editor").then((m) => ({ default: m.CodeEditor }))),
|
|
14
13
|
"git-graph": lazy(() => import("@/components/git/git-graph").then((m) => ({ default: m.GitGraph }))),
|
|
15
|
-
"git-status": lazy(() => import("@/components/git/git-status-panel").then((m) => ({ default: m.GitStatusPanel }))),
|
|
16
14
|
"git-diff": lazy(() => import("@/components/editor/diff-viewer").then((m) => ({ default: m.DiffViewer }))),
|
|
17
15
|
settings: lazy(() => import("@/components/settings/settings-tab").then((m) => ({ default: m.SettingsTab }))),
|
|
18
16
|
};
|
|
19
17
|
|
|
20
18
|
interface EditorPanelProps {
|
|
21
19
|
panelId: string;
|
|
20
|
+
projectName: string;
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
export function EditorPanel({ panelId }: EditorPanelProps) {
|
|
23
|
+
export function EditorPanel({ panelId, projectName }: EditorPanelProps) {
|
|
25
24
|
const panel = usePanelStore((s) => s.panels[panelId]);
|
|
26
25
|
const isFocused = usePanelStore((s) => s.focusedPanelId === panelId);
|
|
27
|
-
const panelCount = usePanelStore((s) =>
|
|
26
|
+
const panelCount = usePanelStore((s) => {
|
|
27
|
+
const grid = s.currentProject === projectName ? s.grid : (s.projectGrids[projectName] ?? [[]]);
|
|
28
|
+
return grid.flat().length;
|
|
29
|
+
});
|
|
28
30
|
|
|
29
31
|
if (!panel) return null;
|
|
30
32
|
|