@hienlh/ppm 0.1.0

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 (159) hide show
  1. package/.claude/agent-memory/tester/MEMORY.md +3 -0
  2. package/.claude/agent-memory/tester/project-ppm-test-conventions.md +32 -0
  3. package/.env.example +1 -0
  4. package/.github/workflows/release.yml +46 -0
  5. package/README.md +349 -0
  6. package/bun.lock +1217 -0
  7. package/components.json +21 -0
  8. package/docs/code-standards.md +574 -0
  9. package/docs/codebase-summary.md +294 -0
  10. package/docs/deployment-guide.md +631 -0
  11. package/docs/design-guidelines.md +661 -0
  12. package/docs/project-overview-pdr.md +142 -0
  13. package/docs/project-roadmap.md +400 -0
  14. package/docs/system-architecture.md +459 -0
  15. package/package.json +68 -0
  16. package/plans/260314-2009-ppm-implementation/phase-01-project-skeleton.md +81 -0
  17. package/plans/260314-2009-ppm-implementation/phase-02-backend-core.md +148 -0
  18. package/plans/260314-2009-ppm-implementation/phase-03-frontend-shell.md +256 -0
  19. package/plans/260314-2009-ppm-implementation/phase-04-file-explorer-editor.md +120 -0
  20. package/plans/260314-2009-ppm-implementation/phase-05-web-terminal.md +174 -0
  21. package/plans/260314-2009-ppm-implementation/phase-06-git-integration.md +244 -0
  22. package/plans/260314-2009-ppm-implementation/phase-07-ai-chat.md +242 -0
  23. package/plans/260314-2009-ppm-implementation/phase-08-cli-commands.md +143 -0
  24. package/plans/260314-2009-ppm-implementation/phase-09-pwa-build-deploy.md +209 -0
  25. package/plans/260314-2009-ppm-implementation/phase-10-testing.md +311 -0
  26. package/plans/260314-2009-ppm-implementation/plan.md +202 -0
  27. package/plans/260315-0356-project-scoped-api-refactor/phase-01-backend-project-router.md +145 -0
  28. package/plans/260315-0356-project-scoped-api-refactor/phase-02-frontend-api-migration.md +107 -0
  29. package/plans/260315-0356-project-scoped-api-refactor/phase-03-per-project-tabs.md +100 -0
  30. package/plans/260315-0356-project-scoped-api-refactor/phase-04-websocket-migration.md +66 -0
  31. package/plans/260315-0356-project-scoped-api-refactor/plan.md +87 -0
  32. package/plans/reports/brainstorm-260314-1938-final-techstack.md +342 -0
  33. package/plans/reports/docs-manager-260315-1314-documentation-creation.md +386 -0
  34. package/plans/reports/fullstack-developer-260314-2252-phase-02-backend-core.md +57 -0
  35. package/plans/reports/fullstack-developer-260314-2253-phase-03-frontend-shell.md +70 -0
  36. package/plans/reports/fullstack-developer-260314-2300-phase-04-05-file-api-terminal-ws.md +49 -0
  37. package/plans/reports/fullstack-developer-260314-2300-phase-04-05-file-explorer-editor-terminal.md +52 -0
  38. package/plans/reports/fullstack-developer-260314-2307-ai-chat-phase7.md +58 -0
  39. package/plans/reports/fullstack-developer-260314-2307-phase-06-git-integration.md +33 -0
  40. package/plans/reports/research-260314-1911-ppm-tech-stack.md +318 -0
  41. package/plans/reports/research-260314-1930-claude-code-integration.md +293 -0
  42. package/plans/reports/researcher-260314-2232-node-pty-bun-crash-analysis.md +305 -0
  43. package/plans/reports/researcher-260314-2232-ui-style.md +942 -0
  44. package/plans/reports/researcher-260315-0300-opcode-claude-interaction.md +745 -0
  45. package/plans/reports/researcher-260315-0303-opcode-deep-analysis.md +742 -0
  46. package/plans/reports/researcher-260315-0305-claude-agent-sdk-github-research.md +423 -0
  47. package/plans/reports/tester-260314-2053-initial-test-suite.md +81 -0
  48. package/ppm.example.yaml +14 -0
  49. package/repomix-output.xml +23745 -0
  50. package/scripts/build.ts +13 -0
  51. package/src/cli/commands/chat-cmd.ts +259 -0
  52. package/src/cli/commands/config-cmd.ts +121 -0
  53. package/src/cli/commands/git-cmd.ts +315 -0
  54. package/src/cli/commands/init.ts +57 -0
  55. package/src/cli/commands/open.ts +19 -0
  56. package/src/cli/commands/projects.ts +100 -0
  57. package/src/cli/commands/start.ts +3 -0
  58. package/src/cli/commands/stop.ts +33 -0
  59. package/src/cli/utils/project-resolver.ts +27 -0
  60. package/src/index.ts +59 -0
  61. package/src/providers/claude-agent-sdk.ts +499 -0
  62. package/src/providers/claude-binary-finder.ts +256 -0
  63. package/src/providers/claude-code-cli.ts +413 -0
  64. package/src/providers/claude-process-registry.ts +106 -0
  65. package/src/providers/mock-provider.ts +171 -0
  66. package/src/providers/provider.interface.ts +10 -0
  67. package/src/providers/registry.ts +45 -0
  68. package/src/server/helpers/resolve-project.ts +22 -0
  69. package/src/server/index.ts +181 -0
  70. package/src/server/middleware/auth.ts +30 -0
  71. package/src/server/routes/chat.ts +153 -0
  72. package/src/server/routes/files.ts +168 -0
  73. package/src/server/routes/git.ts +261 -0
  74. package/src/server/routes/project-scoped.ts +27 -0
  75. package/src/server/routes/projects.ts +57 -0
  76. package/src/server/routes/static.ts +26 -0
  77. package/src/server/ws/chat.ts +130 -0
  78. package/src/server/ws/terminal.ts +89 -0
  79. package/src/services/chat.service.ts +110 -0
  80. package/src/services/claude-usage.service.ts +113 -0
  81. package/src/services/config.service.ts +90 -0
  82. package/src/services/file.service.ts +261 -0
  83. package/src/services/git-dirs.service.ts +112 -0
  84. package/src/services/git.service.ts +372 -0
  85. package/src/services/project.service.ts +107 -0
  86. package/src/services/slash-items.service.ts +184 -0
  87. package/src/services/terminal.service.ts +212 -0
  88. package/src/types/api.ts +37 -0
  89. package/src/types/chat.ts +92 -0
  90. package/src/types/config.ts +41 -0
  91. package/src/types/git.ts +50 -0
  92. package/src/types/project.ts +18 -0
  93. package/src/types/terminal.ts +20 -0
  94. package/src/web/app.tsx +168 -0
  95. package/src/web/components/auth/login-screen.tsx +88 -0
  96. package/src/web/components/chat/attachment-chips.tsx +55 -0
  97. package/src/web/components/chat/chat-placeholder.tsx +10 -0
  98. package/src/web/components/chat/chat-tab.tsx +301 -0
  99. package/src/web/components/chat/file-picker.tsx +126 -0
  100. package/src/web/components/chat/message-input.tsx +420 -0
  101. package/src/web/components/chat/message-list.tsx +838 -0
  102. package/src/web/components/chat/session-picker.tsx +139 -0
  103. package/src/web/components/chat/slash-command-picker.tsx +135 -0
  104. package/src/web/components/chat/usage-badge.tsx +186 -0
  105. package/src/web/components/editor/code-editor.tsx +329 -0
  106. package/src/web/components/editor/diff-viewer.tsx +276 -0
  107. package/src/web/components/editor/editor-placeholder.tsx +10 -0
  108. package/src/web/components/explorer/file-actions.tsx +191 -0
  109. package/src/web/components/explorer/file-tree.tsx +298 -0
  110. package/src/web/components/git/git-graph.tsx +727 -0
  111. package/src/web/components/git/git-placeholder.tsx +55 -0
  112. package/src/web/components/git/git-status-panel.tsx +850 -0
  113. package/src/web/components/layout/mobile-drawer.tsx +137 -0
  114. package/src/web/components/layout/mobile-nav.tsx +103 -0
  115. package/src/web/components/layout/sidebar.tsx +90 -0
  116. package/src/web/components/layout/tab-bar.tsx +152 -0
  117. package/src/web/components/layout/tab-content.tsx +85 -0
  118. package/src/web/components/projects/dir-suggest.tsx +152 -0
  119. package/src/web/components/projects/project-list.tsx +187 -0
  120. package/src/web/components/settings/settings-tab.tsx +57 -0
  121. package/src/web/components/terminal/terminal-placeholder.tsx +10 -0
  122. package/src/web/components/terminal/terminal-tab.tsx +133 -0
  123. package/src/web/components/ui/button.tsx +64 -0
  124. package/src/web/components/ui/context-menu.tsx +250 -0
  125. package/src/web/components/ui/dialog.tsx +156 -0
  126. package/src/web/components/ui/dropdown-menu.tsx +257 -0
  127. package/src/web/components/ui/input.tsx +21 -0
  128. package/src/web/components/ui/scroll-area.tsx +56 -0
  129. package/src/web/components/ui/separator.tsx +26 -0
  130. package/src/web/components/ui/sonner.tsx +40 -0
  131. package/src/web/components/ui/tabs.tsx +91 -0
  132. package/src/web/components/ui/tooltip.tsx +57 -0
  133. package/src/web/hooks/use-chat.ts +420 -0
  134. package/src/web/hooks/use-terminal.ts +182 -0
  135. package/src/web/hooks/use-url-sync.ts +66 -0
  136. package/src/web/hooks/use-websocket.ts +48 -0
  137. package/src/web/index.html +16 -0
  138. package/src/web/lib/api-client.ts +90 -0
  139. package/src/web/lib/file-support.ts +68 -0
  140. package/src/web/lib/utils.ts +6 -0
  141. package/src/web/lib/ws-client.ts +100 -0
  142. package/src/web/main.tsx +10 -0
  143. package/src/web/public/icon-192.svg +5 -0
  144. package/src/web/public/icon-512.svg +5 -0
  145. package/src/web/stores/file-store.ts +81 -0
  146. package/src/web/stores/project-store.ts +50 -0
  147. package/src/web/stores/settings-store.ts +65 -0
  148. package/src/web/stores/tab-store.ts +187 -0
  149. package/src/web/styles/globals.css +227 -0
  150. package/src/web/vite-env.d.ts +1 -0
  151. package/tests/integration/api/chat-routes.test.ts +95 -0
  152. package/tests/integration/claude-agent-sdk-integration.test.ts +228 -0
  153. package/tests/integration/ws/chat-websocket.test.ts +312 -0
  154. package/tests/test-setup.ts +5 -0
  155. package/tests/unit/providers/claude-agent-sdk.test.ts +339 -0
  156. package/tests/unit/providers/mock-provider.test.ts +143 -0
  157. package/tests/unit/services/chat-service.test.ts +100 -0
  158. package/tsconfig.json +32 -0
  159. package/vite.config.ts +62 -0
@@ -0,0 +1,148 @@
1
+ # Phase 2: Backend Core (Server + CLI + Config)
2
+
3
+ **Owner:** backend-dev
4
+ **Priority:** Critical
5
+ **Depends on:** Phase 1
6
+ **Effort:** Medium
7
+
8
+ ## Overview
9
+
10
+ Hono HTTP server, WebSocket upgrade, config loading, auth middleware, static file serving. CLI `init`, `start`, `stop`, `open` commands.
11
+
12
+ ## Key Insights
13
+
14
+ - Hono on Bun has built-in WebSocket support via `Bun.serve`
15
+ - Config loaded from `ppm.yaml` via js-yaml, validated at startup
16
+ - Auth = simple Bearer token header check
17
+ - Static files = serve Vite build output from `dist/web/`
18
+
19
+ ## Files to Create/Modify
20
+
21
+ ```
22
+ src/
23
+ ├── server/
24
+ │ ├── index.ts # Hono app, mount routes, WS upgrade
25
+ │ ├── middleware/auth.ts # Token auth
26
+ │ ├── routes/static.ts # Serve SPA with fallback to index.html
27
+ │ └── routes/projects.ts # GET /api/projects, POST /api/projects
28
+ ├── services/
29
+ │ ├── config.service.ts # Load/save ppm.yaml
30
+ │ └── project.service.ts # Project CRUD (add/remove/list)
31
+ ├── cli/
32
+ │ ├── index.ts # Commander.js program setup
33
+ │ ├── commands/init.ts # Scan .git folders, interactive setup
34
+ │ ├── commands/start.ts # Start Hono server (+ daemon mode)
35
+ │ ├── commands/stop.ts # Kill daemon by PID file
36
+ │ ├── commands/open.ts # Open browser to http://localhost:PORT
37
+ │ └── utils/project-resolver.ts # CWD detect + -p flag
38
+ └── index.ts # Wire CLI → commands
39
+ ```
40
+
41
+ ## Implementation Steps
42
+
43
+ ### 1. Config Service
44
+ ```typescript
45
+ // config.service.ts
46
+ class ConfigService {
47
+ private config: PpmConfig;
48
+ load(path?: string): PpmConfig // Default: ~/.ppm/config.yaml or ./ppm.yaml
49
+ save(): void
50
+ get<K extends keyof PpmConfig>(key: K): PpmConfig[K]
51
+ set<K extends keyof PpmConfig>(key: K, value: PpmConfig[K]): void
52
+ }
53
+ ```
54
+ - Search order: `--config` flag → `./ppm.yaml` → `~/.ppm/config.yaml`
55
+ - Create default config on first run
56
+
57
+ ### 2. Project Service
58
+ ```typescript
59
+ class ProjectService {
60
+ constructor(private config: ConfigService)
61
+ list(): ProjectInfo[]
62
+ add(path: string, name?: string): void
63
+ remove(nameOrPath: string): void
64
+ resolve(nameOrPath?: string): Project // CWD or -p flag
65
+ scanForGitRepos(dir: string): string[] // Find .git folders recursively
66
+ }
67
+ ```
68
+
69
+ ### 3. Hono Server
70
+ ```typescript
71
+ // server/index.ts
72
+ const app = new Hono()
73
+ app.use('/api/*', authMiddleware)
74
+ app.route('/api/projects', projectRoutes)
75
+ // WS upgrade handled at Bun.serve level
76
+ // Static files: serve dist/web/ with SPA fallback
77
+ ```
78
+
79
+ ### 4. Auth Middleware
80
+ - Check `Authorization: Bearer <token>` header
81
+ - Skip auth if `config.auth.enabled === false`
82
+ - Return 401 on failure
83
+
84
+ **Auth flow (minimal):**
85
+ - Token stored in `ppm.yaml` → `auth.token` field
86
+ - Generated on `ppm init` (random 32-char hex)
87
+ - Browser: on first visit, if `auth.enabled === true`, show password input screen
88
+ - User enters token → stored in localStorage → sent as Bearer header on all API calls
89
+ - No user/password DB — single shared token from config
90
+ - Config example:
91
+ ```yaml
92
+ auth:
93
+ enabled: true
94
+ token: "a1b2c3..." # auto-generated, user can change
95
+ ```
96
+
97
+ **Frontend auth screen** (`src/web/components/auth/login-screen.tsx`):
98
+ - Full-screen centered card: "Enter Access Token" + input + submit
99
+ - On submit: store token in localStorage, redirect to app
100
+ - On 401 from any API call: clear localStorage, show login screen
101
+
102
+ ### 5. CLI Commands
103
+ - `ppm init` — interactive: scan parent dir for .git, prompt to add, create config
104
+ - `ppm start` — start Hono server, optionally daemonize (`-d` flag → detach process, write PID to `~/.ppm/ppm.pid`)
105
+ - `ppm stop` — read PID file, kill process
106
+ - `ppm open` — `open http://localhost:${port}` (macOS) / `xdg-open` (Linux)
107
+
108
+ ### 6. Project Resolver
109
+ ```typescript
110
+ function resolveProject(options: { project?: string }): Project {
111
+ if (options.project) return projectService.resolve(options.project);
112
+ const cwd = process.cwd();
113
+ const match = projectService.list().find(p => cwd.startsWith(p.path));
114
+ if (match) return match;
115
+ throw new Error('Not in a registered project. Use -p <name>');
116
+ }
117
+ ```
118
+
119
+ ### 7. Shared Project Resolver (`src/server/helpers/resolve-project.ts`)
120
+
121
+ **[V2 FIX]** All API routes must resolve project by NAME first, fallback to path:
122
+ ```typescript
123
+ export function resolveProjectPath(nameOrPath: string): string {
124
+ const projects = configService.get("projects");
125
+ const byName = projects.find(p => p.name === nameOrPath);
126
+ if (byName) return resolve(byName.path);
127
+ const abs = resolve(nameOrPath);
128
+ const allowed = projects.some(p => abs === p.path || abs.startsWith(p.path + "/"));
129
+ if (!allowed) throw new Error(`Project not found: ${nameOrPath}`);
130
+ return abs;
131
+ }
132
+ ```
133
+ Import this in every route file (files.ts, git.ts, projects.ts).
134
+
135
+ ## Success Criteria
136
+
137
+ - [ ] `ppm init` scans current dir, creates config file with auto-generated auth token
138
+ - [ ] `ppm start` starts HTTP server on configured port
139
+ - [ ] `ppm start -d` runs as daemon, writes PID file; `ppm stop` reads PID and kills process
140
+ - [ ] `GET /api/projects` with valid Bearer token returns project list as `{ ok: true, data: [...] }`
141
+ - [ ] `GET /api/projects` without token returns 401 `{ ok: false, error: "Unauthorized" }`
142
+ - [ ] `GET /api/projects` with `auth.enabled: false` in config returns data without token
143
+ - [ ] `ppm open` opens `http://localhost:<port>` in default browser
144
+ - [ ] `resolveProjectPath("ppm")` returns correct filesystem path from config
145
+ - [ ] `resolveProjectPath("nonexistent")` throws descriptive error
146
+ - [ ] Config service loads from `./ppm.yaml` → `~/.ppm/config.yaml` fallback chain
147
+ - [ ] Config service creates default config if none exists
148
+ - [ ] Static route serves `dist/web/index.html` for all non-API paths (SPA fallback)
@@ -0,0 +1,256 @@
1
+ # Phase 3: Frontend Shell (Tab System + Layout)
2
+
3
+ **Owner:** frontend-dev
4
+ **Priority:** Critical
5
+ **Depends on:** Phase 1
6
+ **Effort:** Medium
7
+
8
+ ## Overview
9
+
10
+ React app shell: tab bar, tab content area, sidebar, mobile navigation. This is the foundation all other UI features plug into.
11
+
12
+ ## Key Insights
13
+
14
+ - Mobile-first: bottom tab bar on mobile, top tab bar on desktop
15
+ - Each tab = lazy-loaded React component
16
+ - zustand store manages tab CRUD + active tab
17
+ - Sidebar: project list + file explorer (collapsible on mobile)
18
+
19
+ ## UI Design System — "Slate Dark"
20
+
21
+ See full spec: [UI Style Guide](../reports/researcher-260314-2232-ui-style.md)
22
+
23
+ ### Color Palette
24
+ | Token | Hex | Usage |
25
+ |-------|-----|-------|
26
+ | Background | `#0f1419` | Main app bg (OLED-friendly) |
27
+ | Surface | `#1a1f2e` | Cards, panels, modals |
28
+ | Surface Elevated | `#252d3d` | Hover states |
29
+ | Border | `#404854` | Dividers, input borders |
30
+ | Text Primary | `#e5e7eb` | Body text (13.5:1 contrast) |
31
+ | Text Secondary | `#9ca3af` | Hints, timestamps |
32
+ | Primary Blue | `#3b82f6` | Buttons, active tabs, focus rings |
33
+ | Success Green | `#10b981` | Commit, save |
34
+ | Error Red | `#ef4444` | Errors, deletions |
35
+ | Warning Orange | `#f59e0b` | Conflicts, unsaved |
36
+
37
+ ### Typography
38
+ - UI font: **Geist Sans** (Vercel's dev tool font)
39
+ - Code font: **Geist Mono** (clear `1lI` distinction, ligatures)
40
+ - Default UI text: 14px / weight 400
41
+ - Editor: 13px mono
42
+ - Terminal: 12px mono
43
+
44
+ ### Component Rules
45
+ - Touch targets: **44px minimum** height
46
+ - Border radius: 8px default, 12px for cards/modals, 6px for inputs
47
+ - **Dual theme:** Dark (default) + Light mode, togglable via settings
48
+ - shadcn/ui with custom CSS variables for both themes
49
+ - Icon library: **Lucide** (lightweight, works on both themes)
50
+ - CodeMirror theme: custom from Slate Dark palette (dark) + custom light variant
51
+ - Fonts: Google Fonts CDN (Geist Sans + Geist Mono)
52
+ - Notifications: bottom-left toast (desktop), bottom full-width toast (mobile) — one-hand friendly
53
+
54
+ ### Light Theme Palette ("Slate Light")
55
+ | Token | Hex | Usage |
56
+ |-------|-----|-------|
57
+ | Background | `#ffffff` | Main app bg |
58
+ | Surface | `#f8fafc` | Cards, panels |
59
+ | Surface Elevated | `#f1f5f9` | Hover states |
60
+ | Border | `#e2e8f0` | Dividers |
61
+ | Text Primary | `#1a1f2e` | Body text |
62
+ | Text Secondary | `#64748b` | Hints |
63
+ | Primary Blue | `#2563eb` | Buttons, active tabs |
64
+ | Success Green | `#059669` | Commit, save |
65
+ | Error Red | `#dc2626` | Errors |
66
+ | Warning Orange | `#d97706` | Conflicts |
67
+
68
+ ### Theme Toggle
69
+ - Store preference in `settings.store.ts` → persisted to localStorage
70
+ - Options: "light" | "dark" | "system" (follows OS `prefers-color-scheme`)
71
+ - Default: "system"
72
+ - Toggle in settings tab + quick toggle icon in top bar / mobile header
73
+ - Tailwind `darkMode: 'class'` → toggle `<html class="dark">`
74
+ - CodeMirror: switch between custom dark/light theme extensions
75
+
76
+ ### Mobile-Specific
77
+ - Bottom nav: 64px height, max 5 tabs, icon + label
78
+ - Active tab indicator: top border (blue)
79
+ - Drawer sidebar: slide-in overlay with backdrop (not toggle)
80
+
81
+ ## Files to Create
82
+
83
+ ```
84
+ src/web/
85
+ ├── index.html
86
+ ├── main.tsx # React entry + PWA register
87
+ ├── app.tsx # Layout: sidebar + tab area
88
+ ├── components/
89
+ │ ├── layout/
90
+ │ │ ├── tab-bar.tsx # Scrollable tab bar
91
+ │ │ ├── tab-content.tsx # Renders active tab component
92
+ │ │ ├── sidebar.tsx # Project list + file tree
93
+ │ │ └── mobile-nav.tsx # Bottom nav for mobile
94
+ │ ├── projects/
95
+ │ │ ├── project-list.tsx # List of registered projects
96
+ │ │ └── project-card.tsx # Single project card
97
+ │ └── ui/ # shadcn/ui (already installed)
98
+ ├── stores/
99
+ │ ├── tab.store.ts # Tab CRUD + active tab
100
+ │ ├── project.store.ts # Current project + project list
101
+ │ └── settings.store.ts # Theme, layout prefs
102
+ ├── hooks/
103
+ │ └── use-websocket.ts # Generic WS hook with reconnect
104
+ ├── lib/
105
+ │ ├── api-client.ts # fetch wrapper for /api/*
106
+ │ └── ws-client.ts # WS client class with reconnect
107
+ └── styles/
108
+ └── globals.css # Tailwind imports + custom vars
109
+ ```
110
+
111
+ ## Implementation Steps
112
+
113
+ ### 1. Tab Store (zustand)
114
+ ```typescript
115
+ interface Tab {
116
+ id: string;
117
+ type: 'projects' | 'terminal' | 'chat' | 'editor' | 'git-graph' | 'git-status' | 'git-diff' | 'settings';
118
+ title: string;
119
+ metadata?: Record<string, any>; // e.g., filePath for editor, sessionId for chat
120
+ closable: boolean;
121
+ }
122
+
123
+ interface TabStore {
124
+ tabs: Tab[];
125
+ activeTabId: string | null;
126
+ openTab(tab: Omit<Tab, 'id'>): string; // returns new tab id
127
+ closeTab(id: string): void;
128
+ setActiveTab(id: string): void;
129
+ updateTab(id: string, updates: Partial<Tab>): void;
130
+ }
131
+ ```
132
+
133
+ ### 2. App Layout
134
+ ```
135
+ Desktop:
136
+ ┌──────────┬────────────────────────────┐
137
+ │ Sidebar │ [Tab1] [Tab2] [Tab3] [+] │
138
+ │ (280px) ├────────────────────────────┤
139
+ │ Projects │ │
140
+ │ Files │ Active Tab Content │
141
+ │ │ │
142
+ └──────────┴────────────────────────────┘
143
+
144
+ Mobile:
145
+ ┌────────────────────────────────────────┐
146
+ │ ☰ PPM - Project Name ⚙️ │
147
+ ├────────────────────────────────────────┤
148
+ │ │
149
+ │ Active Tab Content │
150
+ │ │
151
+ ├────────────────────────────────────────┤
152
+ │ [Projects] [Terminal] [Chat] [Git] [+] │
153
+ └────────────────────────────────────────┘
154
+ ```
155
+
156
+ ### 3. Tab Content (lazy loaded)
157
+ ```typescript
158
+ const TAB_COMPONENTS: Record<Tab['type'], React.LazyExoticComponent<any>> = {
159
+ 'projects': lazy(() => import('./projects/project-list')),
160
+ 'terminal': lazy(() => import('./terminal/terminal-tab')),
161
+ 'chat': lazy(() => import('./chat/chat-tab')),
162
+ 'editor': lazy(() => import('./editor/code-editor')),
163
+ 'git-graph': lazy(() => import('./git/git-graph')),
164
+ 'git-status': lazy(() => import('./git/git-status-panel')),
165
+ 'git-diff': lazy(() => import('./git/git-diff-tab')),
166
+ 'settings': lazy(() => import('./settings/settings-tab')),
167
+ };
168
+ ```
169
+
170
+ ### 4. API Client
171
+ ```typescript
172
+ class ApiClient {
173
+ constructor(private baseUrl: string, private token?: string)
174
+ async get<T>(path: string): Promise<T>
175
+ async post<T>(path: string, body?: any): Promise<T>
176
+ async delete(path: string): Promise<void>
177
+ }
178
+ ```
179
+
180
+ ### 5. WebSocket Client
181
+ ```typescript
182
+ class WsClient {
183
+ constructor(private url: string)
184
+ connect(): void
185
+ disconnect(): void
186
+ send(data: string | ArrayBuffer): void
187
+ onMessage(handler: (data: MessageEvent) => void): void
188
+ // Auto-reconnect with exponential backoff
189
+ // Heartbeat ping/pong
190
+ }
191
+ ```
192
+
193
+ ### 6. Project List Tab
194
+ - Fetch `GET /api/projects` on mount
195
+ - Display project cards with name, path, git status indicator
196
+ - Click project → set as active, open file explorer in sidebar
197
+
198
+ ### 7. Mobile Responsiveness
199
+ - Tailwind breakpoints: `sm:`, `md:`, `lg:`
200
+ - Tab bar: bottom on mobile (`fixed bottom-0`), top on desktop
201
+ - Touch: swipe left/right to switch tabs (nice-to-have)
202
+
203
+ ### 8. Mobile Drawer Sidebar (`src/web/components/layout/mobile-drawer.tsx`)
204
+
205
+ **[V2 FIX]** Do NOT use `hidden md:flex` toggle for mobile sidebar. Instead:
206
+ - Absolute positioned overlay with backdrop (`fixed inset-0 z-50`)
207
+ - Slide-in from left (`translate-x`) with animation
208
+ - Click backdrop to close
209
+ - Separate from desktop sidebar — desktop uses `hidden md:flex`, mobile uses drawer
210
+ - Hamburger button opens drawer, not toggles sidebar visibility
211
+
212
+ ### 9. API Client Auto-Unwrap (`src/web/lib/api-client.ts`)
213
+
214
+ **[V2 FIX]** Api-client must auto-unwrap `{ok, data}` envelope:
215
+ ```typescript
216
+ async get<T>(path: string): Promise<T> {
217
+ const res = await fetch(`${this.baseUrl}${path}`, { headers });
218
+ const json = await res.json();
219
+ if (json.ok === false) throw new Error(json.error || `HTTP ${res.status}`);
220
+ return json.data as T; // Return unwrapped data directly
221
+ }
222
+ ```
223
+
224
+ ### 10. Auth Login Screen (`src/web/components/auth/login-screen.tsx`)
225
+
226
+ - Full-screen centered card with token input field + "Unlock" button
227
+ - On submit → store token in `localStorage('ppm-auth-token')`
228
+ - `api-client` reads token from localStorage for all requests
229
+ - On 401 response from any API call → clear token → redirect to login screen
230
+ - If server has `auth.enabled: false` → skip login, app loads directly
231
+ - Check auth status on app mount: `GET /api/auth/check` → returns `{ ok: true }` or 401
232
+
233
+ ### 11. Tab Metadata for Git Tabs
234
+
235
+ **[V2 FIX]** Both tab-bar "+" dropdown AND mobile-nav MUST pass `{ projectName: activeProject.name }` when opening git-graph, git-status, git-diff tabs. Without this metadata, git components get `undefined` project.
236
+
237
+ ## Success Criteria
238
+
239
+ - [ ] App loads in browser with tab bar + sidebar visible
240
+ - [ ] Login screen shown when `auth.enabled: true` — entering correct token stores in localStorage and loads app
241
+ - [ ] Invalid token shows error message on login screen
242
+ - [ ] Can open new tab, close tab, switch between tabs — active tab content renders
243
+ - [ ] Opening duplicate tab (same type + metadata) focuses existing tab instead of creating new
244
+ - [ ] Project list fetches from API and displays project cards with name and path
245
+ - [ ] Clicking project card sets it as active project (zustand store updates)
246
+ - [ ] Mobile layout: bottom nav visible on small screens, top tab bar on desktop
247
+ - [ ] Mobile drawer: hamburger opens overlay sidebar with backdrop, clicking backdrop closes it
248
+ - [ ] Desktop sidebar: always visible at 280px width, collapsible
249
+ - [ ] API client auto-unwraps `{ok, data}` envelope — `apiClient.get<Project[]>('/api/projects')` returns `Project[]` directly
250
+ - [ ] API client throws error with message when `ok: false` (e.g., 401, 404)
251
+ - [ ] WebSocket client connects, auto-reconnects with exponential backoff (1s, 2s, 4s, max 30s)
252
+ - [ ] Git tabs opened from BOTH tab-bar AND mobile-nav include `{ projectName }` metadata
253
+ - [ ] Theme: dark + light mode both work with proper contrast ratios
254
+ - [ ] Theme toggle: "system" default follows OS preference, manual override persists in localStorage
255
+ - [ ] CodeMirror editor switches theme when app theme changes
256
+ - [ ] Notifications: toast appears bottom-left (desktop), bottom full-width (mobile)
@@ -0,0 +1,120 @@
1
+ # Phase 4: File Explorer + Code Editor
2
+
3
+ **Owner:** backend-dev (API) + frontend-dev (UI) — parallel
4
+ **Priority:** High
5
+ **Depends on:** Phase 2, Phase 3
6
+ **Effort:** Medium
7
+
8
+ ## Overview
9
+
10
+ File tree API, CRUD operations, CodeMirror 6 editor tab, file compare/diff view.
11
+
12
+ ## Backend (backend-dev)
13
+
14
+ ### Files
15
+ ```
16
+ src/services/file.service.ts
17
+ src/server/routes/files.ts
18
+ ```
19
+
20
+ ### File Service
21
+ ```typescript
22
+ class FileService {
23
+ getTree(projectPath: string, depth?: number): FileNode[]
24
+ readFile(filePath: string): { content: string; encoding: string }
25
+ writeFile(filePath: string, content: string): void
26
+ createFile(filePath: string, type: 'file' | 'directory'): void
27
+ deleteFile(filePath: string): void
28
+ renameFile(oldPath: string, newPath: string): void
29
+ moveFile(source: string, destination: string): void
30
+ }
31
+
32
+ interface FileNode {
33
+ name: string;
34
+ path: string;
35
+ type: 'file' | 'directory';
36
+ children?: FileNode[];
37
+ size?: number;
38
+ modified?: string;
39
+ }
40
+ ```
41
+
42
+ ### API Routes
43
+
44
+ **[V2 FIX]** `:project` param is project NAME (e.g. "ppm"), not path. Use `resolveProjectPath()` from `src/server/helpers/resolve-project.ts`.
45
+
46
+ ```
47
+ GET /api/files/tree/:project?depth=3 → FileNode[]
48
+ GET /api/files/read?path=... → { content, encoding }
49
+ PUT /api/files/write → { path, content }
50
+ POST /api/files/create → { path, type }
51
+ DELETE /api/files/delete → { path }
52
+ POST /api/files/rename → { oldPath, newPath }
53
+ POST /api/files/move → { source, destination }
54
+ ```
55
+
56
+ `getTree(projectName)` accepts project name, looks it up via resolveProjectPath internally.
57
+
58
+ ### Security
59
+ - Validate all paths are within registered project directories
60
+ - Block access to `.git/`, `node_modules/`, `.env` files
61
+ - Path traversal prevention (no `../`)
62
+
63
+ ## Frontend (frontend-dev)
64
+
65
+ ### Files
66
+ ```
67
+ src/web/components/explorer/file-tree.tsx
68
+ src/web/components/explorer/file-actions.tsx
69
+ src/web/components/editor/code-editor.tsx
70
+ src/web/components/editor/diff-viewer.tsx
71
+ ```
72
+
73
+ ### File Tree Component
74
+ - Recursive tree rendering with expand/collapse
75
+ - Icons per file type (folder, ts, js, json, md, etc.)
76
+ - Context menu: New File, New Folder, Rename, Delete, Copy Path
77
+ - Click file → open in editor tab
78
+ - Select 2 files → "Compare Selected" option in context menu
79
+ - Search/filter files (nice-to-have)
80
+
81
+ ### Code Editor (CodeMirror 6)
82
+ ```typescript
83
+ // code-editor.tsx
84
+ import CodeMirror from '@uiw/react-codemirror';
85
+ import { oneDark } from '@codemirror/theme-one-dark';
86
+ import { autocompletion } from '@codemirror/autocomplete';
87
+
88
+ // Auto-detect language from file extension
89
+ const getLanguageExtension = (filename: string) => {
90
+ const ext = filename.split('.').pop();
91
+ // Map ext → @codemirror/lang-* import
92
+ };
93
+ ```
94
+
95
+ - Auto-save on change (debounced 1s) via `PUT /api/files/write`
96
+ - Unsaved indicator in tab title (dot or italic)
97
+ - Mobile: CM6 handles touch natively
98
+
99
+ ### Diff Viewer
100
+ - Uses `@codemirror/merge` for side-by-side or unified diff
101
+ - Opened when comparing 2 files from explorer
102
+ - Also used for git diff viewing (Phase 6)
103
+
104
+ ## Success Criteria
105
+
106
+ - [ ] File tree loads project structure as expandable/collapsible tree with correct nesting
107
+ - [ ] File tree excludes `.git/`, `node_modules/`, `.env` by default
108
+ - [ ] Right-click (desktop) / long-press (mobile) opens context menu: New File, New Folder, Rename, Delete, Copy Path
109
+ - [ ] "New File" creates file and opens it in editor tab
110
+ - [ ] "Rename" shows inline input, saves on Enter, cancels on Esc
111
+ - [ ] "Delete" shows confirmation dialog before deleting
112
+ - [ ] Click file → opens CodeMirror editor in new tab with correct syntax highlighting
113
+ - [ ] Click already-open file → focuses existing tab (no duplicate)
114
+ - [ ] Editor: syntax highlighting works for .ts, .js, .py, .html, .css, .json, .md, .yaml
115
+ - [ ] Editor: auto-save on change (debounced 1s) — tab title shows unsaved indicator (dot) until saved
116
+ - [ ] Editor: save confirmed via `PUT /api/files/write` response — dot disappears
117
+ - [ ] Select 2 files in explorer → "Compare Selected" in context menu → opens diff view
118
+ - [ ] Diff viewer shows side-by-side or unified diff using `@codemirror/merge`
119
+ - [ ] Path traversal blocked: accessing `../../etc/passwd` returns 403
120
+ - [ ] Mobile: file tree scrollable with touch, context menu appears on long-press