@hienlh/ppm 0.5.2 → 0.5.4

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.
Files changed (61) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/web/assets/{api-client-ANLU-Irq.js → api-client-BxCvlogn.js} +1 -1
  3. package/dist/web/assets/chat-tab-AwRs7rWS.js +7 -0
  4. package/dist/web/assets/code-editor-BviTme00.js +1 -0
  5. package/dist/web/assets/diff-viewer-CCZM_VBl.js +4 -0
  6. package/dist/web/assets/git-graph-UCZZ6fX6.js +1 -0
  7. package/dist/web/assets/index-BxHR8fUA.css +2 -0
  8. package/dist/web/assets/index-yvVRZ65D.js +21 -0
  9. package/dist/web/assets/{input-D-F4ITU0.js → input-Bzyi1GeB.js} +1 -1
  10. package/dist/web/assets/{jsx-runtime-B4BJKQ1u.js → jsx-runtime-Bzk8w7Zh.js} +1 -1
  11. package/dist/web/assets/markdown-renderer-DzVh1Ft8.js +59 -0
  12. package/dist/web/assets/{rotate-ccw-BesidNnx.js → rotate-ccw-ZqeedZLA.js} +1 -1
  13. package/dist/web/assets/settings-store-DikslxSJ.js +1 -0
  14. package/dist/web/assets/settings-tab-C-AGuxll.js +1 -0
  15. package/dist/web/assets/tab-store-BNgVKR5w.js +1 -0
  16. package/dist/web/assets/terminal-tab-CnbdkUFt.js +36 -0
  17. package/dist/web/assets/{use-monaco-theme-CsNwoeyj.js → use-monaco-theme-BFv4d2_j.js} +2 -2
  18. package/dist/web/assets/{utils-bntUtdc7.js → utils-EM9hC5pN.js} +1 -1
  19. package/dist/web/index.html +8 -9
  20. package/dist/web/sw.js +1 -1
  21. package/package.json +1 -1
  22. package/src/cli/commands/init.ts +2 -2
  23. package/src/cli/commands/status.ts +85 -1
  24. package/src/cli/commands/stop.ts +56 -2
  25. package/src/index.ts +4 -2
  26. package/src/providers/claude-agent-sdk.ts +0 -4
  27. package/src/server/index.ts +81 -20
  28. package/src/services/config.service.ts +11 -1
  29. package/src/web/components/chat/attachment-chips.tsx +1 -1
  30. package/src/web/components/chat/chat-history-bar.tsx +0 -3
  31. package/src/web/components/chat/message-input.tsx +13 -14
  32. package/src/web/components/chat/message-list.tsx +5 -4
  33. package/src/web/components/chat/tool-cards.tsx +3 -6
  34. package/src/web/components/editor/code-editor.tsx +2 -1
  35. package/src/web/components/editor/diff-viewer.tsx +43 -22
  36. package/src/web/components/explorer/file-tree.tsx +3 -3
  37. package/src/web/components/git/git-graph.tsx +2 -1
  38. package/src/web/components/git/git-status-panel.tsx +166 -89
  39. package/src/web/components/layout/command-palette.tsx +2 -1
  40. package/src/web/components/layout/mobile-drawer.tsx +2 -2
  41. package/src/web/components/layout/mobile-nav.tsx +1 -1
  42. package/src/web/components/layout/panel-layout.tsx +16 -16
  43. package/src/web/components/layout/split-drop-overlay.tsx +3 -3
  44. package/src/web/components/shared/markdown-renderer.tsx +16 -10
  45. package/src/web/hooks/use-terminal.ts +66 -23
  46. package/src/web/lib/utils.ts +5 -0
  47. package/src/web/stores/panel-store.ts +15 -14
  48. package/src/web/stores/panel-utils.ts +12 -10
  49. package/src/web/stores/settings-store.ts +1 -1
  50. package/dist/web/assets/chat-tab-CWBzraGA.js +0 -7
  51. package/dist/web/assets/code-editor-C4JSoO8E.js +0 -1
  52. package/dist/web/assets/diff-viewer-BdxT3tDC.js +0 -4
  53. package/dist/web/assets/git-graph-C7Rc_ZjF.js +0 -1
  54. package/dist/web/assets/index-DHOHCLrc.js +0 -21
  55. package/dist/web/assets/index-DhsWierF.css +0 -2
  56. package/dist/web/assets/markdown-renderer-Cv9PPnXe.js +0 -59
  57. package/dist/web/assets/react-WvgCEYPV.js +0 -1
  58. package/dist/web/assets/settings-store-CGtTcr8r.js +0 -1
  59. package/dist/web/assets/settings-tab-DYv7J4Vw.js +0 -1
  60. package/dist/web/assets/tab-store-Dq1kMOkJ.js +0 -1
  61. package/dist/web/assets/terminal-tab-BeYE7Lrg.js +0 -36
@@ -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.split("/").pop() ?? 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.split("/").pop() ?? 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-2 space-y-3 overflow-hidden">
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.5" />}
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.5" />}
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
- /** Inline action buttons for a file / folder row */
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 shrink-0 ml-1">
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-7 rounded border border-border/60 bg-muted/60 text-muted-foreground hover:bg-primary/15 hover:text-primary hover:border-primary/40 active:scale-95 transition-colors"
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.5" />
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-7 rounded border border-border/60 bg-muted/60 text-muted-foreground hover:bg-destructive/15 hover:text-destructive hover:border-destructive/40 active:scale-95 transition-colors"
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.5" />
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-7 rounded border border-border/60 bg-muted/60 text-muted-foreground hover:bg-accent hover:text-accent-foreground active:scale-95 transition-colors"
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-1">
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
- <Button
591
- variant="ghost"
592
- size="xs"
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
- {actionAllLabel}
598
- </Button>
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="divide-y divide-border/40 w-full overflow-hidden">
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
- return (
667
- <div className="flex items-center gap-1 hover:bg-muted/50 rounded px-1 py-1 w-full min-w-0">
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
- <div
794
- className="absolute top-0 bottom-0 border-l border-border/30"
795
- style={{ left: depth * 16 - 8 }}
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 overflow-hidden">
819
- {/* Vertical indent line for this level */}
869
+ <div className="relative">
820
870
  {depth > 0 && (
821
- <div
822
- className="absolute top-0 border-l border-border/30"
823
- style={{ left: depth * 16 - 8, bottom: isLast ? "50%" : 0 }}
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
- <div
828
- className="flex items-center hover:bg-muted/50 rounded py-1 pr-1 border-b border-border/30"
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="absolute top-0 bottom-0 border-l border-border/30"
865
- style={{ left: depth * 16 + 8 }}
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.split("/").pop() ?? 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-[280px] bg-background border-r border-border",
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 ? (grid[pos.col]?.length ?? 0) < MAX_ROWS : false;
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="horizontal" style={{ height: "100%" }}>
22
- {grid.map((column, colIdx) => (
23
- <ColumnPanel key={`col-${colIdx}`} column={column} colIdx={colIdx} totalCols={grid.length} projectName={projectName} />
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 ColumnPanel({ column, colIdx, totalCols, projectName }: { column: string[]; colIdx: number; totalCols: number; projectName: string }) {
30
- const defaultSize = `${Math.round(100 / totalCols)}%`;
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
- {column.length === 1 ? (
35
- <EditorPanel panelId={column[0]!} projectName={projectName} />
34
+ {row.length === 1 ? (
35
+ <EditorPanel panelId={row[0]!} projectName={projectName} />
36
36
  ) : (
37
- <Group orientation="vertical">
38
- {column.map((panelId, rowIdx) => (
39
- <RowPanel key={panelId} panelId={panelId} rowIdx={rowIdx} totalRows={column.length} projectName={projectName} />
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
- {colIdx < totalCols - 1 && <ResizeHandle orientation="vertical" />}
44
+ {rowIdx < totalRows - 1 && <ResizeHandle orientation="horizontal" />}
45
45
  </>
46
46
  );
47
47
  }
48
48
 
49
- function RowPanel({ panelId, rowIdx, totalRows, projectName }: { panelId: string; rowIdx: number; totalRows: number; projectName: string }) {
50
- const defaultSize = `${Math.round(100 / totalRows)}%`;
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
- {rowIdx < totalRows - 1 && <ResizeHandle orientation="horizontal" />}
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-2 cursor-col-resize" : "h-2 cursor-row-resize"}
68
- bg-border/50 hover:bg-primary/30 active:bg-primary/50
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 canSplitH = !isMobile && grid.length < maxColumns(false);
21
- const canSplitV = pos ? (grid[pos.col]?.length ?? 0) < MAX_ROWS : false;
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 as "left" | "right" | "down";
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 */ }
@@ -4,6 +4,7 @@ import { useTabStore } from "@/stores/tab-store";
4
4
  import { useFileStore, type FileNode } from "@/stores/file-store";
5
5
  import { openCommandPalette } from "@/hooks/use-global-keybindings";
6
6
  import { api, projectUrl } from "@/lib/api-client";
7
+ import { basename } from "@/lib/utils";
7
8
 
8
9
  // Configure marked globally
9
10
  marked.use({ gfm: true, breaks: true });
@@ -11,6 +12,8 @@ marked.use({ gfm: true, breaks: true });
11
12
  /** Common text file extensions that PPM can open as editor tabs */
12
13
  const FILE_EXTS = "ts|tsx|js|jsx|mjs|cjs|py|json|md|mdx|yaml|yml|toml|css|scss|less|html|htm|sh|bash|zsh|go|rs|sql|rb|java|kt|swift|c|cpp|h|hpp|cs|vue|svelte|txt|env|cfg|conf|ini|xml|csv|log|dockerfile|makefile|gradle";
13
14
  const FILE_EXT_RE = new RegExp(`\\.(${FILE_EXTS})$`, "i");
15
+ /** Glob/regex chars that indicate a pattern, not a real file */
16
+ const GLOB_CHARS_RE = /[*?{}\[\]]/;
14
17
 
15
18
  interface MarkdownRendererProps {
16
19
  content: string;
@@ -39,11 +42,12 @@ function transformHtml(raw: string): string {
39
42
  '<a href="$1" target="_blank" rel="noopener noreferrer"',
40
43
  );
41
44
 
42
- // <a> with file paths → add data-file-path
45
+ // <a> with file paths → add data-file-path (only files, not folders or glob patterns)
43
46
  html = html.replace(/<a\s+href="([^"]+)"/g, (match, href: string) => {
44
47
  if (/^https?:\/\//.test(href)) return match; // already handled
45
- const isFile = /^(\/|\.\/|\.\.\/)/.test(href) || FILE_EXT_RE.test(href);
46
- return isFile ? `<a href="${href}" data-file-path="${href}"` : match;
48
+ if (GLOB_CHARS_RE.test(href)) return match; // skip glob/regex patterns
49
+ if (!FILE_EXT_RE.test(href)) return match; // must have a file extension
50
+ return `<a href="${href}" data-file-path="${href}"`;
47
51
  });
48
52
 
49
53
  // Inline <code> with file-like names → make clickable
@@ -58,7 +62,8 @@ function transformHtml(raw: string): string {
58
62
  (match, text: string) => {
59
63
  const trimmed = text.trim();
60
64
  if (!trimmed || trimmed.includes(" ")) return match;
61
- if (!FILE_EXT_RE.test(trimmed) && !/^(\/|\.\/|\.\.\/)/.test(trimmed)) return match;
65
+ if (GLOB_CHARS_RE.test(trimmed)) return match; // skip glob/regex patterns
66
+ if (!FILE_EXT_RE.test(trimmed)) return match; // must have a file extension
62
67
  return `<code data-file-clickable="${trimmed}" style="cursor:pointer;text-decoration:underline;text-decoration-style:dotted">${text}</code>`;
63
68
  },
64
69
  );
@@ -119,7 +124,7 @@ export function MarkdownRenderer({ content, projectName, className = "", codeAct
119
124
  if (!filePath) return;
120
125
  const isAbsolute = /^(\/|[A-Za-z]:[/\\])/.test(filePath);
121
126
  const isRelative = /^(\.\/|\.\.\/)/.test(filePath);
122
- const fileName = filePath.split("/").pop() ?? filePath;
127
+ const fileName = basename(filePath);
123
128
 
124
129
  // Absolute path → verify then open
125
130
  if (isAbsolute) {
@@ -138,16 +143,17 @@ export function MarkdownRenderer({ content, projectName, className = "", codeAct
138
143
  .then(() => {
139
144
  openTab({ type: "editor", title: fileName, metadata: meta, projectId: projectName, closable: true });
140
145
  })
141
- .catch(() => searchAndOpen(fileName));
146
+ .catch(() => searchAndOpen(filePath));
142
147
  return;
143
148
  }
144
149
 
145
150
  // Just a filename → search in project tree
146
- searchAndOpen(fileName);
151
+ searchAndOpen(filePath);
147
152
  }
148
153
 
149
- /** Search project file tree; if 1 match → open directly, else → command palette */
150
- function searchAndOpen(fileName: string) {
154
+ /** Search project file tree; if 1 match → open directly, else → command palette with full path */
155
+ function searchAndOpen(filePath: string) {
156
+ const fileName = basename(filePath);
151
157
  const matches = findInTree(fileTree, fileName);
152
158
  if (matches.length === 1) {
153
159
  const match = matches[0]!;
@@ -159,7 +165,7 @@ export function MarkdownRenderer({ content, projectName, className = "", codeAct
159
165
  closable: true,
160
166
  });
161
167
  } else {
162
- openCommandPalette(fileName);
168
+ openCommandPalette(filePath);
163
169
  }
164
170
  }
165
171