@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.
Files changed (75) hide show
  1. package/dist/web/assets/api-client-Bnf9LAt4.js +1 -0
  2. package/dist/web/assets/arrow-up-from-line-BXL5dtbG.js +1 -0
  3. package/dist/web/assets/button-BxijdhtM.js +1 -0
  4. package/dist/web/assets/chat-tab-BZopEuub.js +61 -0
  5. package/dist/web/assets/code-editor-hbllHzj7.js +2 -0
  6. package/dist/web/assets/createLucideIcon-Dy1wlrF7.js +1 -0
  7. package/dist/web/assets/dialog-RczsXsmw.js +45 -0
  8. package/dist/web/assets/diff-viewer-D6ixPlNB.js +4 -0
  9. package/dist/web/assets/dist-CSp7ir0r.js +46 -0
  10. package/dist/web/assets/external-link-WSiY-639.js +1 -0
  11. package/dist/web/assets/git-graph-DXMB_DoT.js +1 -0
  12. package/dist/web/assets/git-status-panel-D8ZUQrRF.js +1 -0
  13. package/dist/web/assets/index-DGSLw2GE.js +10 -0
  14. package/dist/web/assets/index-DYd_2slk.css +2 -0
  15. package/dist/web/assets/jsx-runtime-BnxRlLMJ.js +1 -0
  16. package/dist/web/assets/project-list-DWVXEimw.js +1 -0
  17. package/dist/web/assets/react-Uzd0zARU.js +1 -0
  18. package/dist/web/assets/refresh-cw-DtopuYJf.js +1 -0
  19. package/dist/web/assets/settings-tab-DJRzIAuP.js +1 -0
  20. package/dist/web/assets/terminal-tab-BrP-ENHg.css +1 -0
  21. package/dist/web/assets/terminal-tab-CbwaI-oq.js +36 -0
  22. package/dist/web/assets/trash-2-CHLebaNh.js +1 -0
  23. package/dist/web/assets/utils-Cgi2TYRi.js +1 -0
  24. package/dist/web/assets/x-BISR7bpK.js +1 -0
  25. package/dist/web/icon-192.svg +5 -0
  26. package/dist/web/icon-512.svg +5 -0
  27. package/dist/web/index.html +25 -0
  28. package/dist/web/manifest.webmanifest +1 -0
  29. package/dist/web/registerSW.js +1 -0
  30. package/dist/web/sw.js +1 -0
  31. package/dist/web/workbox-3e722498.js +1 -0
  32. package/package.json +2 -1
  33. package/.claude/agent-memory/tester/MEMORY.md +0 -3
  34. package/.claude/agent-memory/tester/project-ppm-test-conventions.md +0 -32
  35. package/.github/workflows/release.yml +0 -46
  36. package/plans/260314-2009-ppm-implementation/phase-01-project-skeleton.md +0 -81
  37. package/plans/260314-2009-ppm-implementation/phase-02-backend-core.md +0 -148
  38. package/plans/260314-2009-ppm-implementation/phase-03-frontend-shell.md +0 -256
  39. package/plans/260314-2009-ppm-implementation/phase-04-file-explorer-editor.md +0 -120
  40. package/plans/260314-2009-ppm-implementation/phase-05-web-terminal.md +0 -174
  41. package/plans/260314-2009-ppm-implementation/phase-06-git-integration.md +0 -244
  42. package/plans/260314-2009-ppm-implementation/phase-07-ai-chat.md +0 -242
  43. package/plans/260314-2009-ppm-implementation/phase-08-cli-commands.md +0 -143
  44. package/plans/260314-2009-ppm-implementation/phase-09-pwa-build-deploy.md +0 -209
  45. package/plans/260314-2009-ppm-implementation/phase-10-testing.md +0 -311
  46. package/plans/260314-2009-ppm-implementation/plan.md +0 -202
  47. package/plans/260315-0356-project-scoped-api-refactor/phase-01-backend-project-router.md +0 -145
  48. package/plans/260315-0356-project-scoped-api-refactor/phase-02-frontend-api-migration.md +0 -107
  49. package/plans/260315-0356-project-scoped-api-refactor/phase-03-per-project-tabs.md +0 -100
  50. package/plans/260315-0356-project-scoped-api-refactor/phase-04-websocket-migration.md +0 -66
  51. package/plans/260315-0356-project-scoped-api-refactor/plan.md +0 -87
  52. package/plans/reports/brainstorm-260314-1938-final-techstack.md +0 -342
  53. package/plans/reports/docs-manager-260315-1314-documentation-creation.md +0 -386
  54. package/plans/reports/fullstack-developer-260314-2252-phase-02-backend-core.md +0 -57
  55. package/plans/reports/fullstack-developer-260314-2253-phase-03-frontend-shell.md +0 -70
  56. package/plans/reports/fullstack-developer-260314-2300-phase-04-05-file-api-terminal-ws.md +0 -49
  57. package/plans/reports/fullstack-developer-260314-2300-phase-04-05-file-explorer-editor-terminal.md +0 -52
  58. package/plans/reports/fullstack-developer-260314-2307-ai-chat-phase7.md +0 -58
  59. package/plans/reports/fullstack-developer-260314-2307-phase-06-git-integration.md +0 -33
  60. package/plans/reports/research-260314-1911-ppm-tech-stack.md +0 -318
  61. package/plans/reports/research-260314-1930-claude-code-integration.md +0 -293
  62. package/plans/reports/researcher-260314-2232-node-pty-bun-crash-analysis.md +0 -305
  63. package/plans/reports/researcher-260314-2232-ui-style.md +0 -942
  64. package/plans/reports/researcher-260315-0300-opcode-claude-interaction.md +0 -745
  65. package/plans/reports/researcher-260315-0303-opcode-deep-analysis.md +0 -742
  66. package/plans/reports/researcher-260315-0305-claude-agent-sdk-github-research.md +0 -423
  67. package/plans/reports/tester-260314-2053-initial-test-suite.md +0 -81
  68. package/repomix-output.xml +0 -23745
  69. package/tests/integration/api/chat-routes.test.ts +0 -95
  70. package/tests/integration/claude-agent-sdk-integration.test.ts +0 -228
  71. package/tests/integration/ws/chat-websocket.test.ts +0 -312
  72. package/tests/test-setup.ts +0 -5
  73. package/tests/unit/providers/claude-agent-sdk.test.ts +0 -339
  74. package/tests/unit/providers/mock-provider.test.ts +0 -143
  75. 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)