@hienlh/ppm 0.1.0 → 0.1.1
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/dist/web/assets/api-client-Bnf9LAt4.js +1 -0
- package/dist/web/assets/arrow-up-from-line-BXL5dtbG.js +1 -0
- package/dist/web/assets/button-BxijdhtM.js +1 -0
- package/dist/web/assets/chat-tab-BZopEuub.js +61 -0
- package/dist/web/assets/code-editor-hbllHzj7.js +2 -0
- package/dist/web/assets/createLucideIcon-Dy1wlrF7.js +1 -0
- package/dist/web/assets/dialog-RczsXsmw.js +45 -0
- package/dist/web/assets/diff-viewer-D6ixPlNB.js +4 -0
- package/dist/web/assets/dist-CSp7ir0r.js +46 -0
- package/dist/web/assets/external-link-WSiY-639.js +1 -0
- package/dist/web/assets/git-graph-DXMB_DoT.js +1 -0
- package/dist/web/assets/git-status-panel-D8ZUQrRF.js +1 -0
- package/dist/web/assets/index-DGSLw2GE.js +10 -0
- package/dist/web/assets/index-DYd_2slk.css +2 -0
- package/dist/web/assets/jsx-runtime-BnxRlLMJ.js +1 -0
- package/dist/web/assets/project-list-DWVXEimw.js +1 -0
- package/dist/web/assets/react-Uzd0zARU.js +1 -0
- package/dist/web/assets/refresh-cw-DtopuYJf.js +1 -0
- package/dist/web/assets/settings-tab-DJRzIAuP.js +1 -0
- package/dist/web/assets/terminal-tab-BrP-ENHg.css +1 -0
- package/dist/web/assets/terminal-tab-CbwaI-oq.js +36 -0
- package/dist/web/assets/trash-2-CHLebaNh.js +1 -0
- package/dist/web/assets/utils-Cgi2TYRi.js +1 -0
- package/dist/web/assets/x-BISR7bpK.js +1 -0
- package/dist/web/icon-192.svg +5 -0
- package/dist/web/icon-512.svg +5 -0
- package/dist/web/index.html +25 -0
- package/dist/web/manifest.webmanifest +1 -0
- package/dist/web/registerSW.js +1 -0
- package/dist/web/sw.js +1 -0
- package/dist/web/workbox-3e722498.js +1 -0
- package/package.json +2 -1
- package/.claude/agent-memory/tester/MEMORY.md +0 -3
- package/.claude/agent-memory/tester/project-ppm-test-conventions.md +0 -32
- package/.github/workflows/release.yml +0 -46
- package/plans/260314-2009-ppm-implementation/phase-01-project-skeleton.md +0 -81
- package/plans/260314-2009-ppm-implementation/phase-02-backend-core.md +0 -148
- package/plans/260314-2009-ppm-implementation/phase-03-frontend-shell.md +0 -256
- package/plans/260314-2009-ppm-implementation/phase-04-file-explorer-editor.md +0 -120
- package/plans/260314-2009-ppm-implementation/phase-05-web-terminal.md +0 -174
- package/plans/260314-2009-ppm-implementation/phase-06-git-integration.md +0 -244
- package/plans/260314-2009-ppm-implementation/phase-07-ai-chat.md +0 -242
- package/plans/260314-2009-ppm-implementation/phase-08-cli-commands.md +0 -143
- package/plans/260314-2009-ppm-implementation/phase-09-pwa-build-deploy.md +0 -209
- package/plans/260314-2009-ppm-implementation/phase-10-testing.md +0 -311
- package/plans/260314-2009-ppm-implementation/plan.md +0 -202
- package/plans/260315-0356-project-scoped-api-refactor/phase-01-backend-project-router.md +0 -145
- package/plans/260315-0356-project-scoped-api-refactor/phase-02-frontend-api-migration.md +0 -107
- package/plans/260315-0356-project-scoped-api-refactor/phase-03-per-project-tabs.md +0 -100
- package/plans/260315-0356-project-scoped-api-refactor/phase-04-websocket-migration.md +0 -66
- package/plans/260315-0356-project-scoped-api-refactor/plan.md +0 -87
- package/plans/reports/brainstorm-260314-1938-final-techstack.md +0 -342
- package/plans/reports/docs-manager-260315-1314-documentation-creation.md +0 -386
- package/plans/reports/fullstack-developer-260314-2252-phase-02-backend-core.md +0 -57
- package/plans/reports/fullstack-developer-260314-2253-phase-03-frontend-shell.md +0 -70
- package/plans/reports/fullstack-developer-260314-2300-phase-04-05-file-api-terminal-ws.md +0 -49
- package/plans/reports/fullstack-developer-260314-2300-phase-04-05-file-explorer-editor-terminal.md +0 -52
- package/plans/reports/fullstack-developer-260314-2307-ai-chat-phase7.md +0 -58
- package/plans/reports/fullstack-developer-260314-2307-phase-06-git-integration.md +0 -33
- package/plans/reports/research-260314-1911-ppm-tech-stack.md +0 -318
- package/plans/reports/research-260314-1930-claude-code-integration.md +0 -293
- package/plans/reports/researcher-260314-2232-node-pty-bun-crash-analysis.md +0 -305
- package/plans/reports/researcher-260314-2232-ui-style.md +0 -942
- package/plans/reports/researcher-260315-0300-opcode-claude-interaction.md +0 -745
- package/plans/reports/researcher-260315-0303-opcode-deep-analysis.md +0 -742
- package/plans/reports/researcher-260315-0305-claude-agent-sdk-github-research.md +0 -423
- package/plans/reports/tester-260314-2053-initial-test-suite.md +0 -81
- package/repomix-output.xml +0 -23745
- package/tests/integration/api/chat-routes.test.ts +0 -95
- package/tests/integration/claude-agent-sdk-integration.test.ts +0 -228
- package/tests/integration/ws/chat-websocket.test.ts +0 -312
- package/tests/test-setup.ts +0 -5
- package/tests/unit/providers/claude-agent-sdk.test.ts +0 -339
- package/tests/unit/providers/mock-provider.test.ts +0 -143
- package/tests/unit/services/chat-service.test.ts +0 -100
|
@@ -1,256 +0,0 @@
|
|
|
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)
|
|
@@ -1,120 +0,0 @@
|
|
|
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
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
# Phase 5: Web Terminal
|
|
2
|
-
|
|
3
|
-
**Owner:** backend-dev (PTY + WS) + frontend-dev (xterm.js) — parallel
|
|
4
|
-
**Priority:** High
|
|
5
|
-
**Depends on:** Phase 2, Phase 3
|
|
6
|
-
**Effort:** Medium
|
|
7
|
-
|
|
8
|
-
## Overview
|
|
9
|
-
|
|
10
|
-
Web-based terminal: xterm.js in browser ↔ WebSocket ↔ Bun.spawn() native Terminal API on server. Multiple terminal sessions.
|
|
11
|
-
|
|
12
|
-
## Backend (backend-dev)
|
|
13
|
-
|
|
14
|
-
### Files
|
|
15
|
-
```
|
|
16
|
-
src/services/terminal.service.ts
|
|
17
|
-
src/server/ws/terminal.ts
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
### Terminal Service
|
|
21
|
-
|
|
22
|
-
**[V2 FIX]** Use `Bun.spawn()` with **native Terminal API** (NOT node-pty).
|
|
23
|
-
|
|
24
|
-
**Why:** node-pty uses NAN (pre-2015 C++ bindings), Bun only supports NAPI. This is a hard incompatibility — segfault crashes entire process, no try-catch possible. See [research report](../reports/researcher-260314-2232-node-pty-bun-crash-analysis.md).
|
|
25
|
-
|
|
26
|
-
**Chosen approach:** `Bun.spawn()` with `terminal` option — built-in, zero dependencies, full PTY support (colors, cursor, resize).
|
|
27
|
-
|
|
28
|
-
```typescript
|
|
29
|
-
class TerminalService {
|
|
30
|
-
private sessions: Map<string, TerminalSession> = new Map();
|
|
31
|
-
private outputBuffers: Map<string, string> = new Map(); // Last 10KB per session
|
|
32
|
-
|
|
33
|
-
create(projectPath: string, shell?: string): string {
|
|
34
|
-
const id = crypto.randomUUID();
|
|
35
|
-
const proc = Bun.spawn([shell || process.env.SHELL || 'bash'], {
|
|
36
|
-
cwd: projectPath,
|
|
37
|
-
terminal: {
|
|
38
|
-
cols: 80,
|
|
39
|
-
rows: 24,
|
|
40
|
-
data: (terminal, chunk) => {
|
|
41
|
-
// Buffer last 10KB for reconnect
|
|
42
|
-
this.appendBuffer(id, chunk.toString());
|
|
43
|
-
// Emit to connected WS clients via event bus
|
|
44
|
-
},
|
|
45
|
-
},
|
|
46
|
-
});
|
|
47
|
-
this.sessions.set(id, { id, proc, projectPath, createdAt: new Date() });
|
|
48
|
-
return id;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
write(id: string, data: string): void {
|
|
52
|
-
this.sessions.get(id)?.proc.terminal?.write(data);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
resize(id: string, cols: number, rows: number): void {
|
|
56
|
-
this.sessions.get(id)?.proc.terminal?.resize(cols, rows);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
kill(id: string): void {
|
|
60
|
-
const session = this.sessions.get(id);
|
|
61
|
-
if (session) {
|
|
62
|
-
session.proc.terminal?.close();
|
|
63
|
-
session.proc.kill();
|
|
64
|
-
this.sessions.delete(id);
|
|
65
|
-
this.outputBuffers.delete(id);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
getBuffer(id: string): string {
|
|
70
|
-
return this.outputBuffers.get(id) ?? '';
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
list(): TerminalSessionInfo[] { /* ... */ }
|
|
74
|
-
get(id: string): TerminalSession | undefined { /* ... */ }
|
|
75
|
-
}
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
**Limitation:** POSIX only (macOS/Linux). Acceptable for dev environment — Windows users use WSL.
|
|
79
|
-
|
|
80
|
-
### WebSocket Handler
|
|
81
|
-
```
|
|
82
|
-
WS /ws/terminal/:id
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
Protocol (binary frames):
|
|
86
|
-
- Client → Server: keystrokes (text)
|
|
87
|
-
- Server → Client: terminal output (text)
|
|
88
|
-
- Client → Server: `\x01RESIZE:cols,rows` (control message)
|
|
89
|
-
- Server detects client disconnect → keep PTY alive for reconnect (30s timeout)
|
|
90
|
-
|
|
91
|
-
### Flow
|
|
92
|
-
```
|
|
93
|
-
Browser (xterm.js) → WS connect /ws/terminal/:id
|
|
94
|
-
If session exists → attach to existing PTY
|
|
95
|
-
If not → create new PTY via TerminalService
|
|
96
|
-
|
|
97
|
-
xterm.js keystroke → WS → pty.write()
|
|
98
|
-
pty.onData() → WS → xterm.js render
|
|
99
|
-
xterm.js resize → WS control msg → pty.resize()
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
## Frontend (frontend-dev)
|
|
103
|
-
|
|
104
|
-
### Files
|
|
105
|
-
```
|
|
106
|
-
src/web/components/terminal/terminal-tab.tsx
|
|
107
|
-
src/web/hooks/use-terminal.ts
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
### Terminal Tab Component
|
|
111
|
-
```typescript
|
|
112
|
-
import { Terminal } from '@xterm/xterm';
|
|
113
|
-
import { FitAddon } from '@xterm/addon-fit';
|
|
114
|
-
import { WebLinksAddon } from '@xterm/addon-web-links';
|
|
115
|
-
|
|
116
|
-
// useTerminal hook manages WS connection + xterm instance
|
|
117
|
-
const useTerminal = (sessionId: string) => {
|
|
118
|
-
// Create Terminal instance
|
|
119
|
-
// Attach FitAddon for auto-resize
|
|
120
|
-
// Connect WebSocket
|
|
121
|
-
// Wire: ws.onmessage → term.write()
|
|
122
|
-
// Wire: term.onData → ws.send()
|
|
123
|
-
// Wire: ResizeObserver → ws.send(resize control msg)
|
|
124
|
-
};
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
### Features
|
|
128
|
-
- Auto-fit to container size
|
|
129
|
-
- Clickable URLs (WebLinksAddon)
|
|
130
|
-
- Copy/paste (mobile: long press select, paste button)
|
|
131
|
-
- Reconnect on WS drop (re-attach to same PTY)
|
|
132
|
-
- "New Terminal" button → opens new tab with new session
|
|
133
|
-
- Terminal font: monospace, configurable size
|
|
134
|
-
|
|
135
|
-
### Mobile Considerations
|
|
136
|
-
- xterm.js works on mobile but keyboard can obscure terminal
|
|
137
|
-
- Use `visualViewport` API to adjust terminal height when keyboard opens
|
|
138
|
-
- Bottom toolbar with common keys: Tab, Ctrl, Esc, arrows
|
|
139
|
-
|
|
140
|
-
## State Persistence & Reconnect
|
|
141
|
-
|
|
142
|
-
### Output Buffer
|
|
143
|
-
- Server keeps a circular buffer (last 10KB) of terminal output per session
|
|
144
|
-
- On WS reconnect → server sends buffered output before live stream
|
|
145
|
-
- Client clears xterm and replays buffer for seamless experience
|
|
146
|
-
|
|
147
|
-
### Session Persistence
|
|
148
|
-
- Terminal sessions survive server restart: save session metadata (id, projectPath, shell, cwd) to `~/.ppm/sessions.json`
|
|
149
|
-
- On server start → attempt to restore sessions from file (re-spawn shell in last known cwd)
|
|
150
|
-
- Sessions that fail to restore → mark as "dead", remove from list
|
|
151
|
-
- Idle session timeout: configurable, default 1 hour — kill PTY + remove from sessions
|
|
152
|
-
|
|
153
|
-
### WS Reconnect Flow
|
|
154
|
-
```
|
|
155
|
-
Client disconnects (network drop, tab switch on mobile)
|
|
156
|
-
→ WS closes
|
|
157
|
-
→ Client: exponential backoff reconnect (1s, 2s, 4s... max 30s)
|
|
158
|
-
→ Server: PTY stays alive, output buffers
|
|
159
|
-
→ Client reconnects: sends { type: 'attach', sessionId: 'xxx' }
|
|
160
|
-
→ Server: sends buffered output, then pipes live
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
## Success Criteria
|
|
164
|
-
|
|
165
|
-
- [ ] Opening terminal tab spawns real shell (bash/zsh) with correct CWD
|
|
166
|
-
- [ ] Keystrokes sent from browser appear in shell; shell output renders in xterm.js
|
|
167
|
-
- [ ] Terminal auto-resizes when browser window/container resizes — sends RESIZE control message
|
|
168
|
-
- [ ] Multiple terminal tabs work simultaneously (each with own PTY session)
|
|
169
|
-
- [ ] WS disconnect → reconnect → terminal shows buffered output + continues working
|
|
170
|
-
- [ ] Works on mobile: keyboard opens without covering terminal, bottom toolbar has Tab/Ctrl/Esc/arrows
|
|
171
|
-
- [ ] `visualViewport` API adjusts terminal height when mobile keyboard opens
|
|
172
|
-
- [ ] Terminal session persists if server stays running (navigate away + come back = same session)
|
|
173
|
-
- [ ] Idle sessions killed after configured timeout (default 1h)
|
|
174
|
-
- [ ] Clickable URLs in terminal output (WebLinksAddon)
|