@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.
- package/.claude/agent-memory/tester/MEMORY.md +3 -0
- package/.claude/agent-memory/tester/project-ppm-test-conventions.md +32 -0
- package/.env.example +1 -0
- package/.github/workflows/release.yml +46 -0
- package/README.md +349 -0
- package/bun.lock +1217 -0
- package/components.json +21 -0
- package/docs/code-standards.md +574 -0
- package/docs/codebase-summary.md +294 -0
- package/docs/deployment-guide.md +631 -0
- package/docs/design-guidelines.md +661 -0
- package/docs/project-overview-pdr.md +142 -0
- package/docs/project-roadmap.md +400 -0
- package/docs/system-architecture.md +459 -0
- package/package.json +68 -0
- package/plans/260314-2009-ppm-implementation/phase-01-project-skeleton.md +81 -0
- package/plans/260314-2009-ppm-implementation/phase-02-backend-core.md +148 -0
- package/plans/260314-2009-ppm-implementation/phase-03-frontend-shell.md +256 -0
- package/plans/260314-2009-ppm-implementation/phase-04-file-explorer-editor.md +120 -0
- package/plans/260314-2009-ppm-implementation/phase-05-web-terminal.md +174 -0
- package/plans/260314-2009-ppm-implementation/phase-06-git-integration.md +244 -0
- package/plans/260314-2009-ppm-implementation/phase-07-ai-chat.md +242 -0
- package/plans/260314-2009-ppm-implementation/phase-08-cli-commands.md +143 -0
- package/plans/260314-2009-ppm-implementation/phase-09-pwa-build-deploy.md +209 -0
- package/plans/260314-2009-ppm-implementation/phase-10-testing.md +311 -0
- package/plans/260314-2009-ppm-implementation/plan.md +202 -0
- package/plans/260315-0356-project-scoped-api-refactor/phase-01-backend-project-router.md +145 -0
- package/plans/260315-0356-project-scoped-api-refactor/phase-02-frontend-api-migration.md +107 -0
- package/plans/260315-0356-project-scoped-api-refactor/phase-03-per-project-tabs.md +100 -0
- package/plans/260315-0356-project-scoped-api-refactor/phase-04-websocket-migration.md +66 -0
- package/plans/260315-0356-project-scoped-api-refactor/plan.md +87 -0
- package/plans/reports/brainstorm-260314-1938-final-techstack.md +342 -0
- package/plans/reports/docs-manager-260315-1314-documentation-creation.md +386 -0
- package/plans/reports/fullstack-developer-260314-2252-phase-02-backend-core.md +57 -0
- package/plans/reports/fullstack-developer-260314-2253-phase-03-frontend-shell.md +70 -0
- package/plans/reports/fullstack-developer-260314-2300-phase-04-05-file-api-terminal-ws.md +49 -0
- package/plans/reports/fullstack-developer-260314-2300-phase-04-05-file-explorer-editor-terminal.md +52 -0
- package/plans/reports/fullstack-developer-260314-2307-ai-chat-phase7.md +58 -0
- package/plans/reports/fullstack-developer-260314-2307-phase-06-git-integration.md +33 -0
- package/plans/reports/research-260314-1911-ppm-tech-stack.md +318 -0
- package/plans/reports/research-260314-1930-claude-code-integration.md +293 -0
- package/plans/reports/researcher-260314-2232-node-pty-bun-crash-analysis.md +305 -0
- package/plans/reports/researcher-260314-2232-ui-style.md +942 -0
- package/plans/reports/researcher-260315-0300-opcode-claude-interaction.md +745 -0
- package/plans/reports/researcher-260315-0303-opcode-deep-analysis.md +742 -0
- package/plans/reports/researcher-260315-0305-claude-agent-sdk-github-research.md +423 -0
- package/plans/reports/tester-260314-2053-initial-test-suite.md +81 -0
- package/ppm.example.yaml +14 -0
- package/repomix-output.xml +23745 -0
- package/scripts/build.ts +13 -0
- package/src/cli/commands/chat-cmd.ts +259 -0
- package/src/cli/commands/config-cmd.ts +121 -0
- package/src/cli/commands/git-cmd.ts +315 -0
- package/src/cli/commands/init.ts +57 -0
- package/src/cli/commands/open.ts +19 -0
- package/src/cli/commands/projects.ts +100 -0
- package/src/cli/commands/start.ts +3 -0
- package/src/cli/commands/stop.ts +33 -0
- package/src/cli/utils/project-resolver.ts +27 -0
- package/src/index.ts +59 -0
- package/src/providers/claude-agent-sdk.ts +499 -0
- package/src/providers/claude-binary-finder.ts +256 -0
- package/src/providers/claude-code-cli.ts +413 -0
- package/src/providers/claude-process-registry.ts +106 -0
- package/src/providers/mock-provider.ts +171 -0
- package/src/providers/provider.interface.ts +10 -0
- package/src/providers/registry.ts +45 -0
- package/src/server/helpers/resolve-project.ts +22 -0
- package/src/server/index.ts +181 -0
- package/src/server/middleware/auth.ts +30 -0
- package/src/server/routes/chat.ts +153 -0
- package/src/server/routes/files.ts +168 -0
- package/src/server/routes/git.ts +261 -0
- package/src/server/routes/project-scoped.ts +27 -0
- package/src/server/routes/projects.ts +57 -0
- package/src/server/routes/static.ts +26 -0
- package/src/server/ws/chat.ts +130 -0
- package/src/server/ws/terminal.ts +89 -0
- package/src/services/chat.service.ts +110 -0
- package/src/services/claude-usage.service.ts +113 -0
- package/src/services/config.service.ts +90 -0
- package/src/services/file.service.ts +261 -0
- package/src/services/git-dirs.service.ts +112 -0
- package/src/services/git.service.ts +372 -0
- package/src/services/project.service.ts +107 -0
- package/src/services/slash-items.service.ts +184 -0
- package/src/services/terminal.service.ts +212 -0
- package/src/types/api.ts +37 -0
- package/src/types/chat.ts +92 -0
- package/src/types/config.ts +41 -0
- package/src/types/git.ts +50 -0
- package/src/types/project.ts +18 -0
- package/src/types/terminal.ts +20 -0
- package/src/web/app.tsx +168 -0
- package/src/web/components/auth/login-screen.tsx +88 -0
- package/src/web/components/chat/attachment-chips.tsx +55 -0
- package/src/web/components/chat/chat-placeholder.tsx +10 -0
- package/src/web/components/chat/chat-tab.tsx +301 -0
- package/src/web/components/chat/file-picker.tsx +126 -0
- package/src/web/components/chat/message-input.tsx +420 -0
- package/src/web/components/chat/message-list.tsx +838 -0
- package/src/web/components/chat/session-picker.tsx +139 -0
- package/src/web/components/chat/slash-command-picker.tsx +135 -0
- package/src/web/components/chat/usage-badge.tsx +186 -0
- package/src/web/components/editor/code-editor.tsx +329 -0
- package/src/web/components/editor/diff-viewer.tsx +276 -0
- package/src/web/components/editor/editor-placeholder.tsx +10 -0
- package/src/web/components/explorer/file-actions.tsx +191 -0
- package/src/web/components/explorer/file-tree.tsx +298 -0
- package/src/web/components/git/git-graph.tsx +727 -0
- package/src/web/components/git/git-placeholder.tsx +55 -0
- package/src/web/components/git/git-status-panel.tsx +850 -0
- package/src/web/components/layout/mobile-drawer.tsx +137 -0
- package/src/web/components/layout/mobile-nav.tsx +103 -0
- package/src/web/components/layout/sidebar.tsx +90 -0
- package/src/web/components/layout/tab-bar.tsx +152 -0
- package/src/web/components/layout/tab-content.tsx +85 -0
- package/src/web/components/projects/dir-suggest.tsx +152 -0
- package/src/web/components/projects/project-list.tsx +187 -0
- package/src/web/components/settings/settings-tab.tsx +57 -0
- package/src/web/components/terminal/terminal-placeholder.tsx +10 -0
- package/src/web/components/terminal/terminal-tab.tsx +133 -0
- package/src/web/components/ui/button.tsx +64 -0
- package/src/web/components/ui/context-menu.tsx +250 -0
- package/src/web/components/ui/dialog.tsx +156 -0
- package/src/web/components/ui/dropdown-menu.tsx +257 -0
- package/src/web/components/ui/input.tsx +21 -0
- package/src/web/components/ui/scroll-area.tsx +56 -0
- package/src/web/components/ui/separator.tsx +26 -0
- package/src/web/components/ui/sonner.tsx +40 -0
- package/src/web/components/ui/tabs.tsx +91 -0
- package/src/web/components/ui/tooltip.tsx +57 -0
- package/src/web/hooks/use-chat.ts +420 -0
- package/src/web/hooks/use-terminal.ts +182 -0
- package/src/web/hooks/use-url-sync.ts +66 -0
- package/src/web/hooks/use-websocket.ts +48 -0
- package/src/web/index.html +16 -0
- package/src/web/lib/api-client.ts +90 -0
- package/src/web/lib/file-support.ts +68 -0
- package/src/web/lib/utils.ts +6 -0
- package/src/web/lib/ws-client.ts +100 -0
- package/src/web/main.tsx +10 -0
- package/src/web/public/icon-192.svg +5 -0
- package/src/web/public/icon-512.svg +5 -0
- package/src/web/stores/file-store.ts +81 -0
- package/src/web/stores/project-store.ts +50 -0
- package/src/web/stores/settings-store.ts +65 -0
- package/src/web/stores/tab-store.ts +187 -0
- package/src/web/styles/globals.css +227 -0
- package/src/web/vite-env.d.ts +1 -0
- package/tests/integration/api/chat-routes.test.ts +95 -0
- package/tests/integration/claude-agent-sdk-integration.test.ts +228 -0
- package/tests/integration/ws/chat-websocket.test.ts +312 -0
- package/tests/test-setup.ts +5 -0
- package/tests/unit/providers/claude-agent-sdk.test.ts +339 -0
- package/tests/unit/providers/mock-provider.test.ts +143 -0
- package/tests/unit/services/chat-service.test.ts +100 -0
- package/tsconfig.json +32 -0
- 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
|