@hienlh/ppm 0.5.1 → 0.5.3
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 +40 -0
- package/dist/web/assets/{api-client-ANLU-Irq.js → api-client-BxCvlogn.js} +1 -1
- package/dist/web/assets/chat-tab-AwRs7rWS.js +7 -0
- package/dist/web/assets/code-editor-BviTme00.js +1 -0
- package/dist/web/assets/diff-viewer-CCZM_VBl.js +4 -0
- package/dist/web/assets/git-graph-UCZZ6fX6.js +1 -0
- package/dist/web/assets/index-BxHR8fUA.css +2 -0
- package/dist/web/assets/index-yvVRZ65D.js +21 -0
- package/dist/web/assets/{input-D-F4ITU0.js → input-Bzyi1GeB.js} +1 -1
- package/dist/web/assets/{jsx-runtime-B4BJKQ1u.js → jsx-runtime-Bzk8w7Zh.js} +1 -1
- package/dist/web/assets/markdown-renderer-DzVh1Ft8.js +59 -0
- package/dist/web/assets/{rotate-ccw-BesidNnx.js → rotate-ccw-ZqeedZLA.js} +1 -1
- package/dist/web/assets/settings-store-DikslxSJ.js +1 -0
- package/dist/web/assets/settings-tab-C-AGuxll.js +1 -0
- package/dist/web/assets/tab-store-BNgVKR5w.js +1 -0
- package/dist/web/assets/terminal-tab-CnbdkUFt.js +36 -0
- package/dist/web/assets/{use-monaco-theme-CsNwoeyj.js → use-monaco-theme-BFv4d2_j.js} +2 -2
- package/dist/web/assets/{utils-bntUtdc7.js → utils-EM9hC5pN.js} +1 -1
- package/dist/web/index.html +8 -9
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/src/cli/commands/init.ts +2 -2
- package/src/cli/commands/status.ts +66 -1
- package/src/cli/commands/stop.ts +39 -2
- package/src/index.ts +4 -2
- package/src/providers/claude-agent-sdk.ts +30 -21
- package/src/server/helpers/resolve-project.ts +2 -2
- package/src/server/index.ts +2 -1
- package/src/server/routes/chat.ts +1 -0
- package/src/server/ws/chat.ts +4 -2
- package/src/services/claude-usage.service.ts +34 -0
- package/src/services/config.service.ts +11 -1
- package/src/types/api.ts +1 -2
- package/src/types/chat.ts +1 -2
- package/src/web/components/chat/attachment-chips.tsx +1 -1
- package/src/web/components/chat/chat-history-bar.tsx +7 -3
- package/src/web/components/chat/chat-tab.tsx +4 -2
- package/src/web/components/chat/message-input.tsx +13 -14
- package/src/web/components/chat/message-list.tsx +5 -4
- package/src/web/components/chat/tool-cards.tsx +3 -6
- package/src/web/components/editor/code-editor.tsx +2 -1
- package/src/web/components/editor/diff-viewer.tsx +43 -22
- package/src/web/components/explorer/file-tree.tsx +3 -3
- package/src/web/components/git/git-graph.tsx +2 -1
- package/src/web/components/git/git-status-panel.tsx +166 -89
- package/src/web/components/layout/command-palette.tsx +2 -1
- package/src/web/components/layout/mobile-drawer.tsx +2 -2
- package/src/web/components/layout/mobile-nav.tsx +1 -1
- package/src/web/components/layout/panel-layout.tsx +16 -16
- package/src/web/components/layout/split-drop-overlay.tsx +3 -3
- package/src/web/components/shared/markdown-renderer.tsx +16 -10
- package/src/web/hooks/use-chat.ts +10 -17
- package/src/web/hooks/use-terminal.ts +66 -23
- package/src/web/hooks/use-usage.ts +1 -14
- package/src/web/lib/utils.ts +5 -0
- package/src/web/stores/panel-store.ts +15 -14
- package/src/web/stores/panel-utils.ts +12 -10
- package/src/web/stores/settings-store.ts +1 -1
- package/dist/web/assets/chat-tab-d_HzPDhE.js +0 -7
- package/dist/web/assets/code-editor-DFAu3knd.js +0 -1
- package/dist/web/assets/diff-viewer-Bue0mOJY.js +0 -4
- package/dist/web/assets/git-graph-Cjq-lK5h.js +0 -1
- package/dist/web/assets/index-D_IIxtVN.js +0 -21
- package/dist/web/assets/index-DhsWierF.css +0 -2
- package/dist/web/assets/markdown-renderer-B9l76G5h.js +0 -59
- package/dist/web/assets/react-WvgCEYPV.js +0 -1
- package/dist/web/assets/settings-store-CGtTcr8r.js +0 -1
- package/dist/web/assets/settings-tab-BDPgdHPI.js +0 -1
- package/dist/web/assets/tab-store-Dq1kMOkJ.js +0 -1
- package/dist/web/assets/terminal-tab-BEOvTEai.js +0 -36
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
import { useFileStore, type FileNode } from "@/stores/file-store";
|
|
15
15
|
import { useProjectStore } from "@/stores/project-store";
|
|
16
16
|
import { useTabStore } from "@/stores/tab-store";
|
|
17
|
-
import { cn } from "@/lib/utils";
|
|
17
|
+
import { cn, basename } from "@/lib/utils";
|
|
18
18
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
19
19
|
import {
|
|
20
20
|
ContextMenu,
|
|
@@ -220,8 +220,8 @@ export function FileTree({ onFileOpen }: FileTreeProps = {}) {
|
|
|
220
220
|
if (action === "compare-selected" && selectedFiles.length === 2) {
|
|
221
221
|
const file1 = selectedFiles[0]!;
|
|
222
222
|
const file2 = selectedFiles[1]!;
|
|
223
|
-
const name1 = file1
|
|
224
|
-
const name2 = file2
|
|
223
|
+
const name1 = basename(file1);
|
|
224
|
+
const name2 = basename(file2);
|
|
225
225
|
openTab({
|
|
226
226
|
type: "git-diff",
|
|
227
227
|
title: `Compare ${name1} vs ${name2}`,
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
GripVertical,
|
|
15
15
|
} from "lucide-react";
|
|
16
16
|
import { api, projectUrl } from "@/lib/api-client";
|
|
17
|
+
import { basename } from "@/lib/utils";
|
|
17
18
|
import { useTabStore } from "@/stores/tab-store";
|
|
18
19
|
import { Button } from "@/components/ui/button";
|
|
19
20
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
@@ -582,7 +583,7 @@ export function GitGraph({ metadata }: GitGraphProps) {
|
|
|
582
583
|
className="flex items-center gap-2 py-0.5 text-xs hover:bg-muted/50 rounded px-1 cursor-pointer"
|
|
583
584
|
onClick={() => openTab({
|
|
584
585
|
type: "git-diff",
|
|
585
|
-
title: `Diff ${file.path
|
|
586
|
+
title: `Diff ${basename(file.path)}`,
|
|
586
587
|
closable: true,
|
|
587
588
|
metadata: {
|
|
588
589
|
projectName,
|
|
@@ -14,10 +14,17 @@ import {
|
|
|
14
14
|
FileText,
|
|
15
15
|
} from "lucide-react";
|
|
16
16
|
import { api, projectUrl } from "@/lib/api-client";
|
|
17
|
+
import { basename } from "@/lib/utils";
|
|
17
18
|
import { useTabStore } from "@/stores/tab-store";
|
|
18
19
|
import { useSettingsStore } from "@/stores/settings-store";
|
|
19
20
|
import { Button } from "@/components/ui/button";
|
|
20
21
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
22
|
+
import {
|
|
23
|
+
DropdownMenu,
|
|
24
|
+
DropdownMenuContent,
|
|
25
|
+
DropdownMenuItem,
|
|
26
|
+
DropdownMenuTrigger,
|
|
27
|
+
} from "@/components/ui/dropdown-menu";
|
|
21
28
|
import {
|
|
22
29
|
Dialog,
|
|
23
30
|
DialogContent,
|
|
@@ -31,6 +38,8 @@ import type { GitStatus, GitFileChange } from "../../../types/git";
|
|
|
31
38
|
interface GitStatusPanelProps {
|
|
32
39
|
metadata?: Record<string, unknown>;
|
|
33
40
|
tabId?: string;
|
|
41
|
+
/** Called after an action that opens a new tab (e.g. view diff, open file) */
|
|
42
|
+
onNavigate?: () => void;
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
type ViewMode = "flat" | "tree";
|
|
@@ -94,7 +103,7 @@ function collectFiles(node: TreeNode): GitFileChange[] {
|
|
|
94
103
|
return result;
|
|
95
104
|
}
|
|
96
105
|
|
|
97
|
-
export function GitStatusPanel({ metadata, tabId }: GitStatusPanelProps) {
|
|
106
|
+
export function GitStatusPanel({ metadata, tabId, onNavigate }: GitStatusPanelProps) {
|
|
98
107
|
const projectName = metadata?.projectName as string | undefined;
|
|
99
108
|
const [status, setStatus] = useState<GitStatus | null>(null);
|
|
100
109
|
const [loading, setLoading] = useState(true);
|
|
@@ -222,7 +231,7 @@ export function GitStatusPanel({ metadata, tabId }: GitStatusPanelProps) {
|
|
|
222
231
|
const openDiff = (file: GitFileChange) => {
|
|
223
232
|
openTab({
|
|
224
233
|
type: "git-diff",
|
|
225
|
-
title: file.path
|
|
234
|
+
title: basename(file.path),
|
|
226
235
|
closable: true,
|
|
227
236
|
metadata: {
|
|
228
237
|
projectName,
|
|
@@ -230,12 +239,13 @@ export function GitStatusPanel({ metadata, tabId }: GitStatusPanelProps) {
|
|
|
230
239
|
},
|
|
231
240
|
projectId: projectName ?? null,
|
|
232
241
|
});
|
|
242
|
+
onNavigate?.();
|
|
233
243
|
};
|
|
234
244
|
|
|
235
245
|
const openFile = (file: GitFileChange) => {
|
|
236
246
|
openTab({
|
|
237
247
|
type: "editor",
|
|
238
|
-
title: file.path
|
|
248
|
+
title: basename(file.path),
|
|
239
249
|
closable: true,
|
|
240
250
|
metadata: {
|
|
241
251
|
projectName,
|
|
@@ -243,6 +253,7 @@ export function GitStatusPanel({ metadata, tabId }: GitStatusPanelProps) {
|
|
|
243
253
|
},
|
|
244
254
|
projectId: projectName ?? null,
|
|
245
255
|
});
|
|
256
|
+
onNavigate?.();
|
|
246
257
|
};
|
|
247
258
|
|
|
248
259
|
const allUnstaged = useMemo(
|
|
@@ -325,14 +336,15 @@ export function GitStatusPanel({ metadata, tabId }: GitStatusPanelProps) {
|
|
|
325
336
|
)}
|
|
326
337
|
|
|
327
338
|
<ScrollArea className="flex-1 overflow-hidden">
|
|
328
|
-
<div className="p-
|
|
339
|
+
<div className="p-1.5 space-y-2 overflow-hidden">
|
|
329
340
|
{/* Staged Changes */}
|
|
330
341
|
<FileSection
|
|
331
342
|
title="Staged Changes"
|
|
332
343
|
count={status?.staged.length ?? 0}
|
|
333
344
|
files={status?.staged ?? []}
|
|
334
345
|
viewMode={viewMode}
|
|
335
|
-
actionIcon={<Minus className="size-3
|
|
346
|
+
actionIcon={<Minus className="size-3" />}
|
|
347
|
+
actionAllIcon={<Minus className="size-3" />}
|
|
336
348
|
actionTitle="Unstage"
|
|
337
349
|
onAction={(f) => unstageFiles([f.path])}
|
|
338
350
|
onActionAll={
|
|
@@ -353,7 +365,8 @@ export function GitStatusPanel({ metadata, tabId }: GitStatusPanelProps) {
|
|
|
353
365
|
count={allUnstaged.length}
|
|
354
366
|
files={allUnstaged}
|
|
355
367
|
viewMode={viewMode}
|
|
356
|
-
actionIcon={<Plus className="size-3
|
|
368
|
+
actionIcon={<Plus className="size-3" />}
|
|
369
|
+
actionAllIcon={<Plus className="size-3" />}
|
|
357
370
|
actionTitle="Stage"
|
|
358
371
|
onAction={(f) => stageFiles([f.path])}
|
|
359
372
|
onActionAll={
|
|
@@ -477,7 +490,7 @@ export function GitStatusPanel({ metadata, tabId }: GitStatusPanelProps) {
|
|
|
477
490
|
/* Action buttons */
|
|
478
491
|
/* ------------------------------------------------------------------ */
|
|
479
492
|
|
|
480
|
-
/**
|
|
493
|
+
/** Overlay action buttons — visible on desktop hover, hidden on mobile */
|
|
481
494
|
function ActionButtons({
|
|
482
495
|
showRevert,
|
|
483
496
|
onRevert,
|
|
@@ -496,42 +509,33 @@ function ActionButtons({
|
|
|
496
509
|
disabled: boolean;
|
|
497
510
|
}) {
|
|
498
511
|
return (
|
|
499
|
-
<div className="flex items-center gap-0.5
|
|
512
|
+
<div className="hidden md:flex absolute right-0 top-0 bottom-0 items-center gap-0.5 pl-6 pr-1 bg-gradient-to-l from-background from-70% to-transparent opacity-0 group-hover:opacity-100 transition-opacity">
|
|
500
513
|
{onOpenFile && (
|
|
501
514
|
<button
|
|
502
515
|
type="button"
|
|
503
|
-
className="flex items-center justify-center size-
|
|
504
|
-
onClick={(e) => {
|
|
505
|
-
e.stopPropagation();
|
|
506
|
-
onOpenFile();
|
|
507
|
-
}}
|
|
516
|
+
className="flex items-center justify-center size-5 rounded text-muted-foreground hover:text-primary active:scale-95 transition-colors"
|
|
517
|
+
onClick={(e) => { e.stopPropagation(); onOpenFile(); }}
|
|
508
518
|
disabled={disabled}
|
|
509
519
|
title="Open file"
|
|
510
520
|
>
|
|
511
|
-
<FileText className="size-3
|
|
521
|
+
<FileText className="size-3" />
|
|
512
522
|
</button>
|
|
513
523
|
)}
|
|
514
524
|
{showRevert && onRevert && (
|
|
515
525
|
<button
|
|
516
526
|
type="button"
|
|
517
|
-
className="flex items-center justify-center size-
|
|
518
|
-
onClick={(e) => {
|
|
519
|
-
e.stopPropagation();
|
|
520
|
-
onRevert();
|
|
521
|
-
}}
|
|
527
|
+
className="flex items-center justify-center size-5 rounded text-muted-foreground hover:text-destructive active:scale-95 transition-colors"
|
|
528
|
+
onClick={(e) => { e.stopPropagation(); onRevert(); }}
|
|
522
529
|
disabled={disabled}
|
|
523
530
|
title="Discard changes"
|
|
524
531
|
>
|
|
525
|
-
<Undo2 className="size-3
|
|
532
|
+
<Undo2 className="size-3" />
|
|
526
533
|
</button>
|
|
527
534
|
)}
|
|
528
535
|
<button
|
|
529
536
|
type="button"
|
|
530
|
-
className="flex items-center justify-center size-
|
|
531
|
-
onClick={(e) => {
|
|
532
|
-
e.stopPropagation();
|
|
533
|
-
onAction();
|
|
534
|
-
}}
|
|
537
|
+
className="flex items-center justify-center size-5 rounded text-muted-foreground hover:text-accent-foreground active:scale-95 transition-colors"
|
|
538
|
+
onClick={(e) => { e.stopPropagation(); onAction(); }}
|
|
535
539
|
disabled={disabled}
|
|
536
540
|
title={actionTitle}
|
|
537
541
|
>
|
|
@@ -551,6 +555,7 @@ function FileSection({
|
|
|
551
555
|
files,
|
|
552
556
|
viewMode,
|
|
553
557
|
actionIcon,
|
|
558
|
+
actionAllIcon,
|
|
554
559
|
actionTitle,
|
|
555
560
|
onAction,
|
|
556
561
|
onActionAll,
|
|
@@ -568,6 +573,7 @@ function FileSection({
|
|
|
568
573
|
files: GitFileChange[];
|
|
569
574
|
viewMode: ViewMode;
|
|
570
575
|
actionIcon: React.ReactNode;
|
|
576
|
+
actionAllIcon?: React.ReactNode;
|
|
571
577
|
actionTitle: string;
|
|
572
578
|
onAction: (f: GitFileChange) => void;
|
|
573
579
|
onActionAll?: () => void;
|
|
@@ -582,26 +588,26 @@ function FileSection({
|
|
|
582
588
|
}) {
|
|
583
589
|
return (
|
|
584
590
|
<div>
|
|
585
|
-
<div className="flex items-center justify-between mb-
|
|
591
|
+
<div className="flex items-center justify-between mb-0.5">
|
|
586
592
|
<span className="text-xs font-medium text-muted-foreground uppercase">
|
|
587
593
|
{title} ({count})
|
|
588
594
|
</span>
|
|
589
595
|
{onActionAll && count > 0 && (
|
|
590
|
-
<
|
|
591
|
-
|
|
592
|
-
|
|
596
|
+
<button
|
|
597
|
+
type="button"
|
|
598
|
+
className="flex items-center justify-center size-5 rounded text-muted-foreground hover:text-accent-foreground active:scale-95 transition-colors"
|
|
593
599
|
onClick={onActionAll}
|
|
594
600
|
disabled={disabled}
|
|
595
601
|
title={actionAllLabel}
|
|
596
602
|
>
|
|
597
|
-
{
|
|
598
|
-
</
|
|
603
|
+
{actionAllIcon}
|
|
604
|
+
</button>
|
|
599
605
|
)}
|
|
600
606
|
</div>
|
|
601
607
|
{files.length === 0 ? (
|
|
602
608
|
<p className="text-xs text-muted-foreground px-1">No changes</p>
|
|
603
609
|
) : viewMode === "flat" ? (
|
|
604
|
-
<div className="
|
|
610
|
+
<div className="w-full overflow-hidden">
|
|
605
611
|
{files.map((f) => (
|
|
606
612
|
<FileRow
|
|
607
613
|
key={f.path}
|
|
@@ -663,21 +669,26 @@ function FileRow({
|
|
|
663
669
|
onRevert?: (f: GitFileChange) => void;
|
|
664
670
|
displayName?: string;
|
|
665
671
|
}) {
|
|
666
|
-
|
|
667
|
-
<div className="flex items-center gap-1 hover:bg-muted/50 rounded
|
|
672
|
+
const row = (
|
|
673
|
+
<div className="group relative flex items-center gap-1 hover:bg-muted/50 rounded pl-1 py-px w-full min-w-0">
|
|
668
674
|
<span
|
|
669
675
|
className={`text-xs font-mono w-4 text-center shrink-0 ${STATUS_COLORS[file.status] ?? ""}`}
|
|
670
676
|
>
|
|
671
677
|
{file.status}
|
|
672
678
|
</span>
|
|
679
|
+
{/* Desktop: click opens diff */}
|
|
673
680
|
<button
|
|
674
681
|
type="button"
|
|
675
|
-
className="flex-1 text-left text-xs font-mono truncate hover:underline min-w-0"
|
|
682
|
+
className="hidden md:block flex-1 text-left text-xs font-mono truncate hover:underline min-w-0"
|
|
676
683
|
onClick={() => onClickFile(file)}
|
|
677
684
|
title={file.path}
|
|
678
685
|
>
|
|
679
686
|
{displayName ?? file.path}
|
|
680
687
|
</button>
|
|
688
|
+
{/* Mobile: plain text (tap handled by DropdownMenu trigger) */}
|
|
689
|
+
<span className="md:hidden flex-1 text-left text-xs font-mono truncate min-w-0">
|
|
690
|
+
{displayName ?? file.path}
|
|
691
|
+
</span>
|
|
681
692
|
<ActionButtons
|
|
682
693
|
showRevert={showRevert}
|
|
683
694
|
onRevert={onRevert ? () => onRevert(file) : undefined}
|
|
@@ -689,6 +700,42 @@ function FileRow({
|
|
|
689
700
|
/>
|
|
690
701
|
</div>
|
|
691
702
|
);
|
|
703
|
+
|
|
704
|
+
// Mobile: wrap in dropdown menu
|
|
705
|
+
return (
|
|
706
|
+
<>
|
|
707
|
+
{/* Desktop — just the row */}
|
|
708
|
+
<div className="hidden md:block">{row}</div>
|
|
709
|
+
{/* Mobile — dropdown trigger */}
|
|
710
|
+
<div className="md:hidden">
|
|
711
|
+
<DropdownMenu>
|
|
712
|
+
<DropdownMenuTrigger asChild>{row}</DropdownMenuTrigger>
|
|
713
|
+
<DropdownMenuContent align="start" className="min-w-40">
|
|
714
|
+
<DropdownMenuItem onClick={() => onClickFile(file)}>
|
|
715
|
+
View Diff
|
|
716
|
+
</DropdownMenuItem>
|
|
717
|
+
{onOpenFile && (
|
|
718
|
+
<DropdownMenuItem onClick={() => onOpenFile(file)}>
|
|
719
|
+
Open File
|
|
720
|
+
</DropdownMenuItem>
|
|
721
|
+
)}
|
|
722
|
+
<DropdownMenuItem onClick={() => onAction(file)} disabled={disabled}>
|
|
723
|
+
{actionTitle}
|
|
724
|
+
</DropdownMenuItem>
|
|
725
|
+
{showRevert && onRevert && (
|
|
726
|
+
<DropdownMenuItem
|
|
727
|
+
className="text-destructive focus:text-destructive"
|
|
728
|
+
onClick={() => onRevert(file)}
|
|
729
|
+
disabled={disabled}
|
|
730
|
+
>
|
|
731
|
+
Discard Changes
|
|
732
|
+
</DropdownMenuItem>
|
|
733
|
+
)}
|
|
734
|
+
</DropdownMenuContent>
|
|
735
|
+
</DropdownMenu>
|
|
736
|
+
</div>
|
|
737
|
+
</>
|
|
738
|
+
);
|
|
692
739
|
}
|
|
693
740
|
|
|
694
741
|
/* ------------------------------------------------------------------ */
|
|
@@ -782,18 +829,22 @@ function TreeNodeView({
|
|
|
782
829
|
const [expanded, setExpanded] = useState(true);
|
|
783
830
|
const isDir = node.children.length > 0 && !node.file;
|
|
784
831
|
|
|
832
|
+
// Connector style constants
|
|
833
|
+
const railX = depth * 12 - 6; // parent's vertical rail x position
|
|
834
|
+
const connectorCls = "absolute border-dashed border-border";
|
|
835
|
+
|
|
785
836
|
if (node.file) {
|
|
786
837
|
return (
|
|
787
|
-
<div
|
|
788
|
-
className="relative overflow-hidden border-b border-border/30"
|
|
789
|
-
style={{ paddingLeft: depth * 16 }}
|
|
790
|
-
>
|
|
791
|
-
{/* Vertical indent line */}
|
|
838
|
+
<div className="relative" style={{ paddingLeft: depth * 12 }}>
|
|
792
839
|
{depth > 0 && (
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
840
|
+
<>
|
|
841
|
+
{/* Vertical segment — stops at row center for last child */}
|
|
842
|
+
<div className={`${connectorCls} border-l`}
|
|
843
|
+
style={{ left: railX, top: 0, bottom: isLast ? "50%" : 0 }} />
|
|
844
|
+
{/* Horizontal branch to content */}
|
|
845
|
+
<div className={`${connectorCls} border-t`}
|
|
846
|
+
style={{ left: railX, top: "50%", width: 6 }} />
|
|
847
|
+
</>
|
|
797
848
|
)}
|
|
798
849
|
<FileRow
|
|
799
850
|
file={node.file}
|
|
@@ -815,55 +866,81 @@ function TreeNodeView({
|
|
|
815
866
|
const folderFiles = collectFiles(node);
|
|
816
867
|
|
|
817
868
|
return (
|
|
818
|
-
<div className="relative
|
|
819
|
-
{/* Vertical indent line for this level */}
|
|
869
|
+
<div className="relative">
|
|
820
870
|
{depth > 0 && (
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
871
|
+
<>
|
|
872
|
+
{/* Vertical segment — full height for non-last, stops at folder row center for last */}
|
|
873
|
+
<div className={`${connectorCls} border-l`}
|
|
874
|
+
style={{ left: railX, top: 0, ...(isLast ? { height: 13 } : { bottom: 0 }) }} />
|
|
875
|
+
{/* Horizontal branch to folder label */}
|
|
876
|
+
<div className={`${connectorCls} border-t`}
|
|
877
|
+
style={{ left: railX, top: 13, width: 8 }} />
|
|
878
|
+
</>
|
|
825
879
|
)}
|
|
826
880
|
{/* Folder row */}
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
style={{ paddingLeft: depth * 16 + 4 }}
|
|
830
|
-
>
|
|
831
|
-
<button
|
|
832
|
-
type="button"
|
|
833
|
-
className="flex items-center gap-1 flex-1 min-w-0 text-xs font-mono text-muted-foreground"
|
|
834
|
-
onClick={() => setExpanded(!expanded)}
|
|
835
|
-
>
|
|
836
|
-
{expanded ? (
|
|
837
|
-
<ChevronDown className="size-3.5 shrink-0" />
|
|
838
|
-
) : (
|
|
839
|
-
<ChevronRight className="size-3.5 shrink-0" />
|
|
840
|
-
)}
|
|
841
|
-
<span className="truncate font-semibold">{node.name}</span>
|
|
842
|
-
<span className="text-[10px] opacity-60 shrink-0">
|
|
843
|
-
({folderFiles.length})
|
|
844
|
-
</span>
|
|
845
|
-
</button>
|
|
846
|
-
<ActionButtons
|
|
847
|
-
showRevert={showRevert}
|
|
848
|
-
onRevert={
|
|
849
|
-
onFolderRevert
|
|
850
|
-
? () => onFolderRevert(folderFiles, node.fullPath)
|
|
851
|
-
: undefined
|
|
852
|
-
}
|
|
853
|
-
onAction={() => onFolderAction?.(folderFiles)}
|
|
854
|
-
actionIcon={actionIcon}
|
|
855
|
-
actionTitle={`${actionTitle} ${node.name}/`}
|
|
856
|
-
disabled={disabled}
|
|
857
|
-
/>
|
|
858
|
-
</div>
|
|
859
|
-
{/* Children with vertical guide line */}
|
|
860
|
-
{expanded && (
|
|
861
|
-
<div className="relative">
|
|
862
|
-
{/* Continuous vertical line for children */}
|
|
881
|
+
{(() => {
|
|
882
|
+
const folderRow = (
|
|
863
883
|
<div
|
|
864
|
-
className="
|
|
865
|
-
style={{
|
|
866
|
-
|
|
884
|
+
className="group relative flex items-center hover:bg-muted/50 rounded py-0.5"
|
|
885
|
+
style={{ paddingLeft: depth * 12 + 2 }}
|
|
886
|
+
>
|
|
887
|
+
<button
|
|
888
|
+
type="button"
|
|
889
|
+
className="flex items-center gap-1 flex-1 min-w-0 text-xs font-mono text-muted-foreground"
|
|
890
|
+
onClick={() => setExpanded(!expanded)}
|
|
891
|
+
>
|
|
892
|
+
{expanded ? (
|
|
893
|
+
<ChevronDown className="size-3.5 shrink-0" />
|
|
894
|
+
) : (
|
|
895
|
+
<ChevronRight className="size-3.5 shrink-0" />
|
|
896
|
+
)}
|
|
897
|
+
<span className="truncate font-semibold">{node.name}</span>
|
|
898
|
+
<span className="text-[10px] opacity-60 shrink-0">
|
|
899
|
+
({folderFiles.length})
|
|
900
|
+
</span>
|
|
901
|
+
</button>
|
|
902
|
+
<ActionButtons
|
|
903
|
+
showRevert={showRevert}
|
|
904
|
+
onRevert={
|
|
905
|
+
onFolderRevert
|
|
906
|
+
? () => onFolderRevert(folderFiles, node.fullPath)
|
|
907
|
+
: undefined
|
|
908
|
+
}
|
|
909
|
+
onAction={() => onFolderAction?.(folderFiles)}
|
|
910
|
+
actionIcon={actionIcon}
|
|
911
|
+
actionTitle={`${actionTitle} ${node.name}/`}
|
|
912
|
+
disabled={disabled}
|
|
913
|
+
/>
|
|
914
|
+
</div>
|
|
915
|
+
);
|
|
916
|
+
return (
|
|
917
|
+
<>
|
|
918
|
+
<div className="hidden md:block">{folderRow}</div>
|
|
919
|
+
<div className="md:hidden">
|
|
920
|
+
<DropdownMenu>
|
|
921
|
+
<DropdownMenuTrigger asChild>{folderRow}</DropdownMenuTrigger>
|
|
922
|
+
<DropdownMenuContent align="start" className="min-w-40">
|
|
923
|
+
<DropdownMenuItem onClick={() => onFolderAction?.(folderFiles)} disabled={disabled}>
|
|
924
|
+
{actionTitle} {node.name}/
|
|
925
|
+
</DropdownMenuItem>
|
|
926
|
+
{onFolderRevert && (
|
|
927
|
+
<DropdownMenuItem
|
|
928
|
+
className="text-destructive focus:text-destructive"
|
|
929
|
+
onClick={() => onFolderRevert(folderFiles, node.fullPath)}
|
|
930
|
+
disabled={disabled}
|
|
931
|
+
>
|
|
932
|
+
Discard Changes
|
|
933
|
+
</DropdownMenuItem>
|
|
934
|
+
)}
|
|
935
|
+
</DropdownMenuContent>
|
|
936
|
+
</DropdownMenu>
|
|
937
|
+
</div>
|
|
938
|
+
</>
|
|
939
|
+
);
|
|
940
|
+
})()}
|
|
941
|
+
{/* Children — each child draws its own connector segment */}
|
|
942
|
+
{expanded && (
|
|
943
|
+
<div>
|
|
867
944
|
{node.children.map((child, i) => (
|
|
868
945
|
<TreeNodeView
|
|
869
946
|
key={child.fullPath}
|
|
@@ -15,6 +15,7 @@ import { useProjectStore } from "@/stores/project-store";
|
|
|
15
15
|
import { useSettingsStore } from "@/stores/settings-store";
|
|
16
16
|
import { useFileStore, type FileNode } from "@/stores/file-store";
|
|
17
17
|
import { api } from "@/lib/api-client";
|
|
18
|
+
import { basename } from "@/lib/utils";
|
|
18
19
|
|
|
19
20
|
interface CommandItem {
|
|
20
21
|
id: string;
|
|
@@ -161,7 +162,7 @@ export function CommandPalette({ open, onClose, initialQuery = "" }: { open: boo
|
|
|
161
162
|
const meta = activeProject ? { projectName: activeProject.name } : undefined;
|
|
162
163
|
|
|
163
164
|
return fsFiles.map((fp) => {
|
|
164
|
-
const name = fp
|
|
165
|
+
const name = basename(fp);
|
|
165
166
|
return {
|
|
166
167
|
id: `fs:${fp}`,
|
|
167
168
|
label: name,
|
|
@@ -54,7 +54,7 @@ export function MobileDrawer({ isOpen, onClose, initialTab }: MobileDrawerProps)
|
|
|
54
54
|
{/* Drawer panel */}
|
|
55
55
|
<div
|
|
56
56
|
className={cn(
|
|
57
|
-
"fixed left-0 top-0 bottom-0 w-[
|
|
57
|
+
"fixed left-0 top-0 bottom-0 w-[90vw] bg-background border-r border-border",
|
|
58
58
|
"z-50 flex flex-col transition-transform duration-300 ease-out",
|
|
59
59
|
isOpen ? "translate-x-0" : "-translate-x-full",
|
|
60
60
|
)}
|
|
@@ -84,7 +84,7 @@ export function MobileDrawer({ isOpen, onClose, initialTab }: MobileDrawerProps)
|
|
|
84
84
|
)
|
|
85
85
|
)}
|
|
86
86
|
{activeTab === "git" && (
|
|
87
|
-
<GitStatusPanel metadata={{ projectName: activeProject?.name }} />
|
|
87
|
+
<GitStatusPanel metadata={{ projectName: activeProject?.name }} onNavigate={onClose} />
|
|
88
88
|
)}
|
|
89
89
|
{activeTab === "settings" && (
|
|
90
90
|
<SettingsTab />
|
|
@@ -58,7 +58,7 @@ export function MobileNav({ onMenuPress, onProjectsPress }: MobileNavProps) {
|
|
|
58
58
|
|
|
59
59
|
// Context menu actions
|
|
60
60
|
const pos = findPanelPosition(grid, focusedPanelId);
|
|
61
|
-
const canSplitDown = pos ?
|
|
61
|
+
const canSplitDown = pos ? grid.length < MAX_ROWS : false;
|
|
62
62
|
const otherPanelIds = Object.keys(usePanelStore.getState().panels).filter((id) => id !== focusedPanelId);
|
|
63
63
|
|
|
64
64
|
function moveTabLeft(tabId: string) {
|
|
@@ -18,42 +18,42 @@ export function PanelLayout({ projectName }: PanelLayoutProps) {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
return (
|
|
21
|
-
<Group orientation="
|
|
22
|
-
{grid.map((
|
|
23
|
-
<
|
|
21
|
+
<Group orientation="vertical" style={{ height: "100%" }}>
|
|
22
|
+
{grid.map((row, rowIdx) => (
|
|
23
|
+
<RowGroup key={`row-${rowIdx}`} row={row} rowIdx={rowIdx} totalRows={grid.length} projectName={projectName} />
|
|
24
24
|
))}
|
|
25
25
|
</Group>
|
|
26
26
|
);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
function
|
|
30
|
-
const defaultSize = `${Math.round(100 /
|
|
29
|
+
function RowGroup({ row, rowIdx, totalRows, projectName }: { row: string[]; rowIdx: number; totalRows: number; projectName: string }) {
|
|
30
|
+
const defaultSize = `${Math.round(100 / totalRows)}%`;
|
|
31
31
|
return (
|
|
32
32
|
<>
|
|
33
33
|
<Panel minSize="15%" defaultSize={defaultSize}>
|
|
34
|
-
{
|
|
35
|
-
<EditorPanel panelId={
|
|
34
|
+
{row.length === 1 ? (
|
|
35
|
+
<EditorPanel panelId={row[0]!} projectName={projectName} />
|
|
36
36
|
) : (
|
|
37
|
-
<Group orientation="
|
|
38
|
-
{
|
|
39
|
-
<
|
|
37
|
+
<Group orientation="horizontal">
|
|
38
|
+
{row.map((panelId, colIdx) => (
|
|
39
|
+
<ColPanel key={panelId} panelId={panelId} colIdx={colIdx} totalCols={row.length} projectName={projectName} />
|
|
40
40
|
))}
|
|
41
41
|
</Group>
|
|
42
42
|
)}
|
|
43
43
|
</Panel>
|
|
44
|
-
{
|
|
44
|
+
{rowIdx < totalRows - 1 && <ResizeHandle orientation="horizontal" />}
|
|
45
45
|
</>
|
|
46
46
|
);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
function
|
|
50
|
-
const defaultSize = `${Math.round(100 /
|
|
49
|
+
function ColPanel({ panelId, colIdx, totalCols, projectName }: { panelId: string; colIdx: number; totalCols: number; projectName: string }) {
|
|
50
|
+
const defaultSize = `${Math.round(100 / totalCols)}%`;
|
|
51
51
|
return (
|
|
52
52
|
<>
|
|
53
53
|
<Panel minSize="15%" defaultSize={defaultSize}>
|
|
54
54
|
<EditorPanel panelId={panelId} projectName={projectName} />
|
|
55
55
|
</Panel>
|
|
56
|
-
{
|
|
56
|
+
{colIdx < totalCols - 1 && <ResizeHandle orientation="vertical" />}
|
|
57
57
|
</>
|
|
58
58
|
);
|
|
59
59
|
}
|
|
@@ -64,8 +64,8 @@ function ResizeHandle({ orientation }: { orientation: "horizontal" | "vertical"
|
|
|
64
64
|
<Separator
|
|
65
65
|
className={`
|
|
66
66
|
group/handle relative flex items-center justify-center
|
|
67
|
-
${isVertical ? "w-
|
|
68
|
-
bg-border/
|
|
67
|
+
${isVertical ? "w-1 cursor-col-resize" : "h-1 cursor-row-resize"}
|
|
68
|
+
bg-border/30 hover:bg-primary/30 active:bg-primary/50
|
|
69
69
|
transition-colors duration-150
|
|
70
70
|
`}
|
|
71
71
|
>
|
|
@@ -17,8 +17,8 @@ export function SplitDropOverlay({ panelId }: SplitDropOverlayProps) {
|
|
|
17
17
|
const isMobile = usePanelStore((s) => s.isMobile());
|
|
18
18
|
const pos = findPanelPosition(grid, panelId);
|
|
19
19
|
|
|
20
|
-
const
|
|
21
|
-
const
|
|
20
|
+
const canSplitV = !isMobile && grid.length < MAX_ROWS;
|
|
21
|
+
const canSplitH = pos ? !isMobile && (grid[pos.row]?.length ?? 0) < maxColumns(false) : false;
|
|
22
22
|
|
|
23
23
|
const getZone = useCallback(
|
|
24
24
|
(e: React.DragEvent): DropZone => {
|
|
@@ -74,7 +74,7 @@ export function SplitDropOverlay({ panelId }: SplitDropOverlayProps) {
|
|
|
74
74
|
}
|
|
75
75
|
} else {
|
|
76
76
|
// Split: create new panel on the TARGET panel's edge
|
|
77
|
-
const direction = zone === "top" ? "up" as const : zone
|
|
77
|
+
const direction = zone === "top" ? "up" as const : zone === "bottom" ? "down" as const : zone as "left" | "right";
|
|
78
78
|
store.splitPanel(direction, payload.tabId, payload.panelId, panelId);
|
|
79
79
|
}
|
|
80
80
|
} catch { /* ignore */ }
|