@hienlh/ppm 0.9.0-beta.8 → 0.9.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 (159) hide show
  1. package/CHANGELOG.md +238 -0
  2. package/bun.lock +17 -0
  3. package/dist/web/assets/api-settings-BUvk6Saw.js +1 -0
  4. package/dist/web/assets/arrow-up-BYhx9ckd.js +1 -0
  5. package/dist/web/assets/browser-tab-CrkhFCaw.js +1 -0
  6. package/dist/web/assets/chat-tab-C6jpiwh7.js +8 -0
  7. package/dist/web/assets/chevron-right-5HgK6l7K.js +1 -0
  8. package/dist/web/assets/code-editor-CBIPzlP2.js +2 -0
  9. package/dist/web/assets/columns-2-cEVJHYd7.js +1 -0
  10. package/dist/web/assets/createLucideIcon-PuMiQgHl.js +1 -0
  11. package/dist/web/assets/{csv-preview-DLqYtXxt.js → csv-preview-ncSOnJSC.js} +2 -2
  12. package/dist/web/assets/database-viewer-BqOJR_zi.js +1 -0
  13. package/dist/web/assets/diff-viewer-CcLyp4eY.js +4 -0
  14. package/dist/web/assets/{dist-CALwEtco.js → dist-DIV6WgAG.js} +1 -1
  15. package/dist/web/assets/{dist-DGDPTxs1.js → dist-ovWkrgO-.js} +1 -1
  16. package/dist/web/assets/extension-webview-NiZ7Ybvv.js +3 -0
  17. package/dist/web/assets/git-graph-CoTvMrIo.js +1 -0
  18. package/dist/web/assets/index-C8byznLO.js +37 -0
  19. package/dist/web/assets/index-KwC2YrG4.css +2 -0
  20. package/dist/web/assets/jsx-runtime-kMwlnEGE.js +1 -0
  21. package/dist/web/assets/keybindings-store-DPYzBe_M.js +1 -0
  22. package/dist/web/assets/{markdown-renderer-DklUd_Gv.js → markdown-renderer-DPLdR9xc.js} +4 -4
  23. package/dist/web/assets/postgres-viewer-BeiK4lCa.js +1 -0
  24. package/dist/web/assets/settings-tab-D3AvU4lu.js +1 -0
  25. package/dist/web/assets/sqlite-viewer-nA2sD4Yv.js +1 -0
  26. package/dist/web/assets/tab-store-BOgTrqRr.js +1 -0
  27. package/dist/web/assets/table-DFevCOMd.js +1 -0
  28. package/dist/web/assets/tag-CXMT0QB6.js +1 -0
  29. package/dist/web/assets/{terminal-tab-CqRuiIFn.js → terminal-tab-BBi0pEji.js} +2 -2
  30. package/dist/web/assets/{use-monaco-theme-Dcz3aLAE.js → use-monaco-theme-B5pG2d1w.js} +1 -1
  31. package/dist/web/index.html +8 -8
  32. package/dist/web/monacoeditorwork/css.worker.bundle.js +122 -122
  33. package/dist/web/monacoeditorwork/editor.worker.bundle.js +78 -78
  34. package/dist/web/monacoeditorwork/html.worker.bundle.js +110 -110
  35. package/dist/web/monacoeditorwork/json.worker.bundle.js +108 -108
  36. package/dist/web/monacoeditorwork/ts.worker.bundle.js +81 -81
  37. package/dist/web/sw.js +1 -1
  38. package/docs/code-standards.md +128 -1
  39. package/docs/codebase-summary.md +79 -12
  40. package/docs/extension-development-guide.md +532 -0
  41. package/docs/project-changelog.md +51 -1
  42. package/docs/project-roadmap.md +9 -3
  43. package/docs/streaming-input-guide.md +267 -0
  44. package/docs/system-architecture.md +432 -3
  45. package/package.json +6 -3
  46. package/packages/ext-database/package.json +41 -0
  47. package/packages/ext-database/src/connection-tree.ts +142 -0
  48. package/packages/ext-database/src/extension.ts +346 -0
  49. package/packages/ext-database/src/query-panel.ts +120 -0
  50. package/packages/ext-database/src/table-viewer-panel.ts +410 -0
  51. package/packages/ext-database/tsconfig.json +8 -0
  52. package/packages/vscode-compat/package.json +16 -0
  53. package/packages/vscode-compat/src/commands.ts +39 -0
  54. package/packages/vscode-compat/src/context.ts +65 -0
  55. package/packages/vscode-compat/src/disposable.ts +21 -0
  56. package/packages/vscode-compat/src/env.ts +20 -0
  57. package/packages/vscode-compat/src/event-emitter.ts +28 -0
  58. package/packages/vscode-compat/src/index.ts +93 -0
  59. package/packages/vscode-compat/src/not-supported.ts +15 -0
  60. package/packages/vscode-compat/src/types.ts +167 -0
  61. package/packages/vscode-compat/src/uri.ts +65 -0
  62. package/packages/vscode-compat/src/window.ts +229 -0
  63. package/packages/vscode-compat/src/workspace.ts +76 -0
  64. package/packages/vscode-compat/tsconfig.json +10 -0
  65. package/snapshot-state.md +1526 -0
  66. package/src/cli/commands/autostart.ts +1 -1
  67. package/src/cli/commands/ext-cmd.ts +121 -0
  68. package/src/cli/commands/restart.ts +9 -1
  69. package/src/cli/commands/status.ts +19 -0
  70. package/src/index.ts +5 -3
  71. package/src/providers/claude-agent-sdk.ts +221 -17
  72. package/src/providers/cli-provider-base.ts +6 -0
  73. package/src/server/index.ts +55 -155
  74. package/src/server/routes/chat.ts +81 -11
  75. package/src/server/routes/extensions.ts +81 -0
  76. package/src/server/routes/project-scoped.ts +2 -0
  77. package/src/server/routes/settings.ts +27 -0
  78. package/src/server/routes/workspace.ts +35 -0
  79. package/src/server/ws/chat.ts +9 -3
  80. package/src/server/ws/extensions.ts +175 -0
  81. package/src/services/account-selector.service.ts +14 -5
  82. package/src/services/account.service.ts +20 -15
  83. package/src/services/claude-usage.service.ts +29 -24
  84. package/src/services/cloud-ws.service.ts +228 -0
  85. package/src/services/cloud.service.ts +11 -6
  86. package/src/services/contribution-registry.ts +110 -0
  87. package/src/services/db.service.ts +181 -4
  88. package/src/services/extension-host-worker.ts +160 -0
  89. package/src/services/extension-installer.ts +112 -0
  90. package/src/services/extension-manifest.ts +65 -0
  91. package/src/services/extension-rpc-handlers.ts +235 -0
  92. package/src/services/extension-rpc.ts +105 -0
  93. package/src/services/extension.service.ts +228 -0
  94. package/src/services/mcp-config.service.ts +15 -6
  95. package/src/services/supervisor.ts +271 -25
  96. package/src/types/api.ts +1 -0
  97. package/src/types/chat.ts +4 -0
  98. package/src/types/extension-messages.ts +64 -0
  99. package/src/types/extension.ts +131 -0
  100. package/src/web/app.tsx +69 -48
  101. package/src/web/components/chat/account-rotation-settings.tsx +163 -0
  102. package/src/web/components/chat/chat-history-bar.tsx +106 -10
  103. package/src/web/components/chat/chat-tab.tsx +15 -10
  104. package/src/web/components/chat/chat-welcome.tsx +148 -0
  105. package/src/web/components/chat/message-list.tsx +19 -6
  106. package/src/web/components/chat/session-picker.tsx +80 -32
  107. package/src/web/components/chat/usage-badge.tsx +68 -8
  108. package/src/web/components/editor/editor-breadcrumb.tsx +20 -29
  109. package/src/web/components/extensions/extension-inputbox.tsx +92 -0
  110. package/src/web/components/extensions/extension-quickpick.tsx +194 -0
  111. package/src/web/components/extensions/extension-tree-view.tsx +240 -0
  112. package/src/web/components/extensions/extension-webview.tsx +83 -0
  113. package/src/web/components/layout/command-palette.tsx +22 -2
  114. package/src/web/components/layout/editor-panel.tsx +163 -18
  115. package/src/web/components/layout/mobile-nav.tsx +2 -1
  116. package/src/web/components/layout/sidebar.tsx +21 -3
  117. package/src/web/components/layout/status-bar.tsx +64 -0
  118. package/src/web/components/layout/tab-bar.tsx +2 -0
  119. package/src/web/components/layout/tab-content.tsx +5 -0
  120. package/src/web/components/layout/upgrade-banner.tsx +15 -5
  121. package/src/web/components/settings/change-password-section.tsx +128 -0
  122. package/src/web/components/settings/extension-manager-section.tsx +214 -0
  123. package/src/web/components/settings/settings-tab.tsx +9 -2
  124. package/src/web/components/shared/connection-lost-overlay.tsx +89 -0
  125. package/src/web/hooks/use-chat.ts +28 -0
  126. package/src/web/hooks/use-extension-ws.ts +181 -0
  127. package/src/web/hooks/use-global-keybindings.ts +18 -2
  128. package/src/web/hooks/use-server-reload.ts +9 -0
  129. package/src/web/hooks/use-url-sync.ts +173 -21
  130. package/src/web/stores/connection-store.ts +39 -0
  131. package/src/web/stores/extension-store.ts +204 -0
  132. package/src/web/stores/panel-store.ts +63 -9
  133. package/src/web/stores/panel-utils.ts +145 -3
  134. package/src/web/stores/settings-store.ts +7 -2
  135. package/src/web/stores/tab-store.ts +2 -1
  136. package/test-session-ops.mjs +444 -0
  137. package/test-tokens.mjs +212 -0
  138. package/tsconfig.json +3 -1
  139. package/dist/web/assets/api-settings-D21InCnR.js +0 -1
  140. package/dist/web/assets/arrow-up--LjUXLEt.js +0 -1
  141. package/dist/web/assets/browser-tab-BEe89aSD.js +0 -1
  142. package/dist/web/assets/chat-tab-9lqvWozA.js +0 -7
  143. package/dist/web/assets/chevron-right-CHnjJt4E.js +0 -1
  144. package/dist/web/assets/code-editor-COAIZx-B.js +0 -2
  145. package/dist/web/assets/columns-2-DbesTfa7.js +0 -1
  146. package/dist/web/assets/database-viewer-aRR9n_Ui.js +0 -1
  147. package/dist/web/assets/diff-viewer-C4KMvpHr.js +0 -4
  148. package/dist/web/assets/dist-CVTST7Gc.js +0 -1
  149. package/dist/web/assets/git-graph-CfJjl4E3.js +0 -1
  150. package/dist/web/assets/index-Db8uky1a.css +0 -2
  151. package/dist/web/assets/index-DxZuwBDe.js +0 -37
  152. package/dist/web/assets/jsx-runtime-BRW_vwa9.js +0 -1
  153. package/dist/web/assets/keybindings-store-_uWVCZMv.js +0 -1
  154. package/dist/web/assets/postgres-viewer-DEAvAyaX.js +0 -1
  155. package/dist/web/assets/settings-tab-BQedc-No.js +0 -1
  156. package/dist/web/assets/sqlite-viewer-BPA5idzT.js +0 -1
  157. package/dist/web/assets/tab-store-DhK6EpBT.js +0 -1
  158. package/dist/web/assets/table-CQVQM2SB.js +0 -1
  159. package/dist/web/assets/tag-Q2dZiSPX.js +0 -1
@@ -140,10 +140,24 @@ POST /api/db/connections/:id/query → Execute query (readonly ch
140
140
  PATCH /api/db/connections/:id/cell → Update cell value (single)
141
141
  GET /api/upgrade/status → Get current + available versions, install method
142
142
  POST /api/upgrade/apply → Install new version, trigger supervisor self-replace
143
+ GET /api/project/:name/workspace → Get saved workspace layout + metadata
144
+ PUT /api/project/:name/workspace → Save workspace layout (layout JSON)
143
145
  WS /ws/project/:name/chat/:sessionId → Chat streaming
144
146
  WS /ws/project/:name/terminal/:id → Terminal I/O
145
147
  ```
146
148
 
149
+ **URL Format (Deterministic Tabs, v0.8.77+):**
150
+ ```
151
+ /project/{name} → Project root (project switcher)
152
+ /project/{name}/editor/{filePath} → Open editor tab (e.g., src/index.ts)
153
+ /project/{name}/chat/{provider}/{sessionId} → Open chat tab
154
+ /project/{name}/terminal/{index} → Open terminal tab
155
+ /project/{name}/database/{connId}/{table} → Open database browser
156
+ /project/{name}/git-graph → Git history graph (singleton)
157
+ /project/{name}/settings → Settings panel (singleton)
158
+ ```
159
+ Tab IDs are deterministic: `{type}:{identifier}` (e.g., `editor:src/index.ts`, `chat:claude/abc123`). Deep links auto-create missing tabs.
160
+
147
161
  ---
148
162
 
149
163
  ### Service Layer (Business Logic)
@@ -161,7 +175,7 @@ WS /ws/project/:name/terminal/:id → Terminal I/O
161
175
  |---------|---------|-------------|
162
176
  | **ChatService** | Session management, message streaming | createSession, streamMessage, getHistory |
163
177
  | **ConfigService** | Config loading (YAML→SQLite migration) | load, save, getToken |
164
- | **DbService** | SQLite persistence (9 tables, WAL, connections/accounts CRUD) | getDb, openTestDb, getConnections, insertConnection, deleteConnection, getTableCache |
178
+ | **DbService** | SQLite persistence (10 tables, WAL, connections/accounts/workspace CRUD) | getDb, openTestDb, getWorkspace, setWorkspace, getConnections, insertConnection, deleteConnection, getTableCache |
165
179
  | **TableCacheService** | Cache table metadata, search tables | syncTables, searchTables, invalidateCache |
166
180
  | **GitService** | Git command execution | status, diff, commit, stage, branch |
167
181
  | **FileService** | File operations with validation | read, write, tree, delete, mkdir |
@@ -260,11 +274,11 @@ PPM supports multiple AI providers through a generic `AIProvider` interface and
260
274
  - Enforce security (no parent directory access)
261
275
 
262
276
  **Key Patterns:**
263
- - SQLite: WAL mode, foreign keys, lazy init, schema v1 with 6 tables
277
+ - SQLite: WAL mode, foreign keys, lazy init, schema v10 (10 tables: config, connections, accounts, usage_history, session_logs, push_subscriptions, session_map, table_metadata, session_logs, workspace_state)
264
278
  - Path validation: `projectPath/relativePath` only, reject `..`
265
279
  - Caching: Directory trees cached with TTL
266
280
  - Error handling: Descriptive messages (file not found, permission denied)
267
- - Migration: Automatic YAML→SQLite migration on first run with new db.service
281
+ - Migration: Automatic YAML→SQLite migration on first run with new db.service; schema auto-upgrade on version bump
268
282
 
269
283
  ---
270
284
 
@@ -284,6 +298,24 @@ PPM supports multiple AI providers through a generic `AIProvider` interface and
284
298
  const messages = chatStore((s) => s.messages); // Subscribe to messages only
285
299
  ```
286
300
 
301
+ #### Workspace Sync (v0.8.77+)
302
+
303
+ **Deterministic Tab IDs & URL Routing:**
304
+ - Tab IDs derived from type + metadata: `deriveTabId(type, metadata) → {type}:{identifier}`
305
+ - Examples: `editor:src/index.ts`, `chat:claude/abc123`, `terminal:1`, `git-graph`
306
+ - URLs rebuilt from active tab: `/project/{name}/{type}/{identifier}`
307
+ - Deep linking: URL → `parseUrlState()` → auto-create tabs if missing
308
+
309
+ **Workspace Persistence:**
310
+ 1. **Client**: PanelStore layout (grid, panels, tabs) cached in localStorage per project
311
+ 2. **Server**: Workspace JSON persisted in `workspace_state` SQLite table
312
+ 3. **Sync Flow:**
313
+ - User loads project → fetch workspace from server (GET `/api/project/:name/workspace`)
314
+ - Latest-wins: server `updated_at` vs client localStorage timestamp
315
+ - Panel layout changes debounced (1.5s) → POST to server
316
+ - On reconnect: server layout restored, client edits queued
317
+ 4. **Cross-Device:** Any device can load workspace, browser restores exact grid + active tabs
318
+
287
319
  ---
288
320
 
289
321
  ## Communication Protocols
@@ -1066,6 +1098,403 @@ const queryConfig = {
1066
1098
  const query = new Query(messages, queryConfig);
1067
1099
  ```
1068
1100
 
1101
+ ---
1102
+
1103
+ ## Extension System (v0.9.0+)
1104
+
1105
+ ### Overview
1106
+
1107
+ PPM Extension System enables VSCode-compatible, npm-installable extensions that run in isolated Bun Worker threads. Crash-safe, permission-based, with RPC messaging between main process and worker, and WebSocket bridge for real-time UI updates.
1108
+
1109
+ **Architecture (3-tier):**
1110
+ ```
1111
+ Extension Code (Bun Worker) ← @ppm/vscode-compat API
1112
+ │ RPC (postMessage)
1113
+
1114
+ Main Process (Hono/Bun) ← extension-rpc-handlers.ts
1115
+ │ WebSocket (/ws/extensions)
1116
+
1117
+ Browser (React) ← Zustand store + React components
1118
+ ```
1119
+
1120
+ **Key components:**
1121
+ - **Package Format:** npm packages (`@ppm/ext-database`, `@ppm/ext-docker`, etc.)
1122
+ - **Installation:** `~/.ppm/extensions/node_modules/{id}/`
1123
+ - **Lifecycle:** Install → Enable → Activate → Deactivate → Remove
1124
+ - **Worker Isolation:** Each activated extension runs in a Bun Worker (crash-safe, 10s activation timeout)
1125
+ - **Communication:** RPC (Worker↔Main) + WebSocket (Main↔Browser)
1126
+ - **API Shim:** `@ppm/vscode-compat` — VSCode-compatible API (commands, window, workspace)
1127
+ - **State Storage:** globalState + workspaceState in SQLite via Memento
1128
+ - **UI Bridge:** StatusBar, TreeView, WebviewPanel, QuickPick, InputBox, Notifications
1129
+ - **Contributions:** Commands, views, configuration contributed via manifest
1130
+
1131
+ ### Manifest Format
1132
+
1133
+ Extension metadata defined in `package.json` under `ppm` key:
1134
+
1135
+ ```json
1136
+ {
1137
+ "name": "@ppm/ext-database",
1138
+ "version": "1.0.0",
1139
+ "main": "dist/extension.js",
1140
+ "ppm": {
1141
+ "displayName": "Database Browser",
1142
+ "description": "Browse and query databases",
1143
+ "icon": "database.svg",
1144
+ "engines": { "ppm": ">=0.9.0" },
1145
+ "activationEvents": ["onView:databases"],
1146
+ "contributes": {
1147
+ "commands": [
1148
+ {
1149
+ "command": "ppm.database.openConnection",
1150
+ "title": "Open Database Connection",
1151
+ "category": "Database"
1152
+ }
1153
+ ],
1154
+ "views": {
1155
+ "explorer": [
1156
+ {
1157
+ "id": "databases",
1158
+ "name": "Databases",
1159
+ "type": "tree"
1160
+ }
1161
+ ]
1162
+ },
1163
+ "configuration": {
1164
+ "properties": {
1165
+ "ppm.database.maxRows": {
1166
+ "type": "number",
1167
+ "default": 1000,
1168
+ "description": "Max rows to fetch per query"
1169
+ }
1170
+ }
1171
+ }
1172
+ }
1173
+ }
1174
+ }
1175
+ ```
1176
+
1177
+ **Fields:**
1178
+ - `engines.ppm` — PPM version requirement
1179
+ - `activationEvents` — When extension activates (e.g., `onView:databases`, `onCommand:ext.activate`)
1180
+ - `contributes` — UI elements + commands contributed by extension
1181
+
1182
+ ### Installation & Lifecycle
1183
+
1184
+ **Installation** (`ppm ext install @ppm/ext-database`):
1185
+ 1. Fetch package from npm
1186
+ 2. Extract to `~/.ppm/extensions/node_modules/{id}/`
1187
+ 3. Parse manifest from `package.json`
1188
+ 4. Store in SQLite `extensions` table (enabled=1)
1189
+ 5. Discover contributions
1190
+
1191
+ **Activation** (`ppm ext enable @ppm/ext-database` or automatic):
1192
+ 1. Load manifest + entry point from disk
1193
+ 2. Spawn Bun Worker (process isolation)
1194
+ 3. Create scoped `@ppm/vscode-compat` API instance (RPC-backed)
1195
+ 4. Call `activate(context, vscodeApi)` with 10s timeout
1196
+ 5. Register contributions in `contributionRegistry`
1197
+ 6. Broadcast `contributions:update` via WS to all connected browsers
1198
+ 7. Mark as activated
1199
+
1200
+ **Deactivation:**
1201
+ 1. Unregister contributions
1202
+ 2. Terminate worker
1203
+ 3. Clear persisted state if needed
1204
+
1205
+ **Removal** (`ppm ext remove @ppm/ext-database`):
1206
+ 1. Deactivate if active
1207
+ 2. Delete from `~/.ppm/extensions/`
1208
+ 3. Remove from SQLite
1209
+ 4. Unregister contributions
1210
+
1211
+ ### RPC Protocol (Extension ↔ Main Process)
1212
+
1213
+ **Message Types:**
1214
+
1215
+ 1. **Request** (extension → main)
1216
+ ```json
1217
+ {
1218
+ "type": "request",
1219
+ "id": 1,
1220
+ "method": "storage:get",
1221
+ "params": ["extId", "global", "key"]
1222
+ }
1223
+ ```
1224
+
1225
+ 2. **Response** (main → extension)
1226
+ ```json
1227
+ {
1228
+ "type": "response",
1229
+ "id": 1,
1230
+ "result": "value"
1231
+ }
1232
+ ```
1233
+
1234
+ 3. **Event** (both directions)
1235
+ ```json
1236
+ {
1237
+ "type": "event",
1238
+ "event": "file:changed",
1239
+ "data": { "path": "/path/to/file" }
1240
+ }
1241
+ ```
1242
+
1243
+ **Built-in Methods:**
1244
+ - `storage:get(extId, scope, key)` — Get persistent value
1245
+ - `storage:set(extId, scope, key, value)` — Set persistent value
1246
+ - `storage:delete(extId, scope, key)` — Delete key
1247
+ - Extension can define custom RPC methods via `rpc.onRequest(method, handler)`
1248
+
1249
+ ### State Storage
1250
+
1251
+ **Database Schema:**
1252
+
1253
+ ```sql
1254
+ CREATE TABLE extension_storage (
1255
+ ext_id TEXT NOT NULL,
1256
+ scope TEXT NOT NULL, -- 'global' | 'workspace'
1257
+ key TEXT NOT NULL,
1258
+ value TEXT, -- JSON-serialized
1259
+ PRIMARY KEY (ext_id, scope, key)
1260
+ );
1261
+ ```
1262
+
1263
+ **Scopes:**
1264
+ - **globalState** — Persists across all projects (e.g., user settings, cache)
1265
+ - **workspaceState** — Project-specific state (e.g., open panel state)
1266
+
1267
+ **API** (inside extension):
1268
+ ```typescript
1269
+ // In activate(context: ExtensionContext)
1270
+ const globalVal = context.globalState.get("lastConnection", "default");
1271
+ await context.globalState.update("lastConnection", "my-db");
1272
+
1273
+ const wsVal = context.workspaceState.get("selectedTable");
1274
+ await context.workspaceState.update("selectedTable", "users");
1275
+ ```
1276
+
1277
+ ### WebSocket Bridge (Extension ↔ Browser)
1278
+
1279
+ Extensions interact with the browser UI via a dedicated WebSocket at `/ws/extensions`. The main process translates between Worker RPC and WS messages.
1280
+
1281
+ **Server → Client (ExtServerMsg):** `tree:update`, `tree:refresh`, `statusbar:update/remove`, `notification`, `quickpick:show`, `inputbox:show`, `webview:create/html/dispose/postMessage`, `contributions:update`
1282
+
1283
+ **Client → Server (ExtClientMsg):** `ready`, `command:execute`, `tree:expand/click`, `webview:message`, `quickpick:resolve`, `inputbox:resolve`, `notification:action`
1284
+
1285
+ **Message routing:**
1286
+ - Extension calls `vscode.window.showInformationMessage()` → RPC → `extension-rpc-handlers.ts` → `broadcastExtMsg()` → WS → `use-extension-ws` hook → toast notification
1287
+ - Browser user clicks tree item → WS `tree:click` → `extensions.ts` → Worker RPC `ext:command:execute` → CommandService → extension handler
1288
+ - Webview iframe postMessage → parent → CustomEvent → WS `webview:message` → Worker RPC `ext:webview:message` → EventEmitter → extension's `onDidReceiveMessage` handler
1289
+
1290
+ **Request/response pattern:** QuickPick, InputBox, and notification actions use `requestFromBrowser(msg, trackingId, 30s timeout)` — sends WS message and awaits browser response via pending Promise map.
1291
+
1292
+ ### UI Components
1293
+
1294
+ Extension UI state lives in Zustand (`extension-store.ts`) and renders via React:
1295
+ - **StatusBar** — Fixed bottom bar with left/right aligned items
1296
+ - **TreeView** — Recursive tree with expand/collapse, renders in sidebar for `ext:*` tabs
1297
+ - **WebviewPanel** — Sandboxed iframe (`allow-scripts` only), `acquireVsCodeApi()` shim auto-injected
1298
+ - **QuickPick** — Filterable picker with keyboard nav, bottom-sheet on mobile
1299
+ - **InputBox** — Text input dialog with password mode support
1300
+ - **Command Palette** — Extension commands merged with built-in commands
1301
+
1302
+ ### Contribution Registry
1303
+
1304
+ **Purpose:** Central registry of all extension contributions (commands, views, etc.)
1305
+
1306
+ **Storage:** In-memory map during runtime
1307
+
1308
+ **Endpoints:**
1309
+ - `GET /api/extensions/contributions` — List all active contributions
1310
+
1311
+ **Contribution Types:**
1312
+ 1. **Commands** — Callable actions (e.g., `ppm.database.openConnection`)
1313
+ - Registered: `registry.registerCommand(extId, command)`
1314
+ - Invoked: `POST /api/extensions/{extId}/commands/{command}`
1315
+
1316
+ 2. **Views** — Sidebar panels or tree views
1317
+ - Registered: `registry.registerView(extId, view)`
1318
+ - Rendered in UI based on `type` (tree, webview)
1319
+
1320
+ 3. **Configuration** — Settings schema
1321
+ - Registered: `registry.registerConfig(extId, schema)`
1322
+ - Merged with global settings
1323
+
1324
+ ### CLI Commands
1325
+
1326
+ ```bash
1327
+ ppm ext list # List installed extensions
1328
+ ppm ext install @ppm/ext-database # Install from npm
1329
+ ppm ext remove @ppm/ext-database # Uninstall
1330
+ ppm ext enable @ppm/ext-database # Enable extension
1331
+ ppm ext disable @ppm/ext-database # Disable extension
1332
+ ppm ext dev /path/to/ext-src # Symlink local extension for dev
1333
+ ppm ext config <ext-id> <key> <value> # Set config value
1334
+ ```
1335
+
1336
+ **Dev Mode** (`ppm ext dev /path/to/src`):
1337
+ - Symlinks local extension to `~/.ppm/extensions/node_modules/`
1338
+ - Auto-reloads on file change
1339
+ - Extension runs from source (TypeScript not compiled)
1340
+
1341
+ ### REST API
1342
+
1343
+ **Endpoints** (`src/server/routes/extensions.ts`):
1344
+
1345
+ | Method | Endpoint | Description |
1346
+ |--------|----------|-------------|
1347
+ | **GET** | `/api/extensions` | List installed extensions |
1348
+ | **POST** | `/api/extensions` | Install extension (body: {name, version?}) |
1349
+ | **GET** | `/api/extensions/:id` | Get extension info (manifest, status) |
1350
+ | **DELETE** | `/api/extensions/:id` | Remove extension |
1351
+ | **PATCH** | `/api/extensions/:id` | Update extension (body: {enabled}) |
1352
+ | **GET** | `/api/extensions/contributions` | List all contributions (commands, views, config) |
1353
+ | **POST** | `/api/extensions/:id/commands/:cmd` | Invoke extension command |
1354
+
1355
+ **Example: Install Extension**
1356
+ ```bash
1357
+ POST /api/extensions
1358
+ Content-Type: application/json
1359
+
1360
+ { "name": "@ppm/ext-database", "version": "1.0.0" }
1361
+
1362
+ # Response
1363
+ {
1364
+ "ok": true,
1365
+ "data": {
1366
+ "id": "@ppm/ext-database",
1367
+ "version": "1.0.0",
1368
+ "displayName": "Database Browser",
1369
+ "enabled": true,
1370
+ "activated": false
1371
+ }
1372
+ }
1373
+ ```
1374
+
1375
+ ### Service Layer
1376
+
1377
+ **ExtensionService** (`src/services/extension.service.ts`):
1378
+ - `discover()` — Scan `~/.ppm/extensions/` for installed packages
1379
+ - `install(name)` — Fetch from npm, install locally
1380
+ - `remove(id)` — Uninstall extension
1381
+ - `activate(id)` — Load + run extension in worker
1382
+ - `deactivate(id)` — Terminate worker, cleanup
1383
+ - `parseManifest(pkg)` — Extract manifest from package.json
1384
+ - `setExtensionState(extId, scope, key, value)` — Persist state
1385
+
1386
+ **ExtensionInstaller** (`src/services/extension-installer.ts`):
1387
+ - `installExtension(name, dir)` — npm install + verify
1388
+ - `removeExtension(id, dir)` — rm -rf extension directory
1389
+ - `devLinkExtension(localPath)` — Symlink for local dev
1390
+
1391
+ **ExtensionManifest** (`src/services/extension-manifest.ts`):
1392
+ - `parseManifest(pkg)` — Validate + parse ppm section
1393
+ - `discoverManifests(dir)` — Scan all installed extensions
1394
+
1395
+ **RpcChannel** (`src/services/extension-rpc.ts`):
1396
+ - Bidirectional RPC messaging
1397
+ - Request/response matching by ID
1398
+ - Event broadcasting
1399
+ - Timeout handling
1400
+
1401
+ ### Worker Integration
1402
+
1403
+ **ExtensionHostWorker** (`src/services/extension-host-worker.ts`):
1404
+ - Worker-side code that loads + activates extension
1405
+ - Loads extension code into worker context
1406
+ - Exposes ExtensionContext API (globalState, workspaceState, subscriptions)
1407
+ - Handles incoming RPC messages
1408
+ - Communicates back to main process
1409
+
1410
+ **Design:**
1411
+ ```
1412
+ Main Process Worker
1413
+ ↓ ↓
1414
+ ExtensionService ExtensionHostWorker
1415
+ ↓ ↓
1416
+ RpcChannel ←────────────→ RpcChannel
1417
+ ↓ ↓
1418
+ Sends: { Extension Code
1419
+ type: "request", (User's ext.ts)
1420
+ method: "..." ↓
1421
+ } activate(context)
1422
+ ↓ ↓
1423
+ Handlers respond context.storage.get()
1424
+ ↑ ↑
1425
+ └─────────────────┘
1426
+ ```
1427
+
1428
+ ### Dev Workflow
1429
+
1430
+ **Creating an Extension:**
1431
+
1432
+ 1. Create npm package:
1433
+ ```bash
1434
+ npm init -y @ppm/ext-my-feature
1435
+ npm install @ppm/extension-api
1436
+ ```
1437
+
1438
+ 2. Write `src/extension.ts`:
1439
+ ```typescript
1440
+ import type { ExtensionContext } from "@ppm/extension-api";
1441
+
1442
+ export async function activate(context: ExtensionContext) {
1443
+ console.log(`Extension ${context.extensionId} activated!`);
1444
+
1445
+ const val = context.globalState.get("count", 0);
1446
+ await context.globalState.update("count", val + 1);
1447
+ }
1448
+
1449
+ export function deactivate() {
1450
+ console.log("Extension deactivated");
1451
+ }
1452
+ ```
1453
+
1454
+ 3. Add to `package.json`:
1455
+ ```json
1456
+ {
1457
+ "ppm": {
1458
+ "displayName": "My Feature",
1459
+ "main": "dist/extension.js",
1460
+ "contributes": {
1461
+ "commands": [...]
1462
+ }
1463
+ }
1464
+ }
1465
+ ```
1466
+
1467
+ 4. Install locally for dev:
1468
+ ```bash
1469
+ ppm ext dev /path/to/ext-my-feature
1470
+ ```
1471
+
1472
+ 5. Extension auto-activates based on `activationEvents`, state persists
1473
+
1474
+ ### Crash Safety
1475
+
1476
+ **Worker Isolation:**
1477
+ - Each extension in isolated Bun Worker thread
1478
+ - Worker crash doesn't crash main process
1479
+ - Error events logged, extension marked as failed
1480
+ - Main process continues operating
1481
+
1482
+ **Cleanup:**
1483
+ - Worker terminates → cleanup timer expires after 5min
1484
+ - Persisted state preserved in SQLite (not lost on crash)
1485
+ - Next activation reloads from disk, state auto-restored
1486
+
1487
+ ### Future Enhancements (Phase 2+)
1488
+
1489
+ - **UI Webview Support** — Extensions define HTML/React UI panels
1490
+ - **Extension Settings UI** — Auto-generate UI from `contributes.configuration`
1491
+ - **Hot Reload** — Auto-reload extension on file change during dev
1492
+ - **Marketplace** — Browse, rate, publish extensions (v1.0+)
1493
+ - **Permissions** — User prompt for sensitive operations
1494
+ - **Inter-Extension API** — Extensions can call each other via RPC
1495
+
1496
+ ---
1497
+
1069
1498
  **Tool Allow List:**
1070
1499
  - All MCP tools automatically allowed via wildcard `mcp__*`
1071
1500
  - MCP server connection failures don't block chat (logged as warning)
package/package.json CHANGED
@@ -1,17 +1,18 @@
1
1
  {
2
2
  "name": "@hienlh/ppm",
3
- "version": "0.9.0-beta.8",
3
+ "version": "0.9.1",
4
4
  "description": "Personal Project Manager — mobile-first web IDE with AI assistance",
5
5
  "author": "hienlh",
6
6
  "license": "MIT",
7
7
  "module": "src/index.ts",
8
8
  "type": "module",
9
+ "workspaces": ["packages/*"],
9
10
  "bin": {
10
11
  "ppm": "src/index.ts"
11
12
  },
12
13
  "scripts": {
13
14
  "dev": "concurrently \"bun run dev:server\" \"bun run dev:web\"",
14
- "dev:server": "bun run --hot src/index.ts start --profile dev -f",
15
+ "dev:server": "bun run --hot src/server/index.ts __serve__ 8081 0.0.0.0 '' dev",
15
16
  "dev:web": "bun run vite --config vite.config.ts",
16
17
  "build:web": "bun run vite build --config vite.config.ts",
17
18
  "build": "bun run build:web && bun build src/index.ts --compile --outfile dist/ppm",
@@ -32,7 +33,8 @@
32
33
  "esbuild": "^0.27.4",
33
34
  "tailwindcss": "^4.2.1",
34
35
  "vite": "^8.0.0",
35
- "vite-plugin-pwa": "^1.2.0"
36
+ "vite-plugin-pwa": "^1.2.0",
37
+ "workbox-precaching": "^7.4.0"
36
38
  },
37
39
  "peerDependencies": {
38
40
  "typescript": "^5.9.3"
@@ -60,6 +62,7 @@
60
62
  "lucide-react": "^0.577.0",
61
63
  "marked": "^17.0.4",
62
64
  "mermaid": "^11.13.0",
65
+ "monaco-editor": "0.55.1",
63
66
  "next-themes": "^0.4.6",
64
67
  "postgres": "^3.4.8",
65
68
  "qrcode-terminal": "^0.12.0",
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@ppm/ext-database",
3
+ "version": "0.1.0",
4
+ "main": "src/extension.ts",
5
+ "engines": { "ppm": ">=0.9.0" },
6
+ "activationEvents": ["onCommand:ppm-db.openViewer", "onView:ppm-db.connections"],
7
+ "contributes": {
8
+ "commands": [
9
+ { "command": "ppm-db.openViewer", "title": "Database: Open Viewer" },
10
+ { "command": "ppm-db.runQuery", "title": "Database: Run SQL Query" },
11
+ { "command": "ppm-db.refreshConnections", "title": "Database: Refresh Connections", "icon": "refresh" },
12
+ { "command": "ppm-db.addConnection", "title": "Add connection", "icon": "plus" }
13
+ ],
14
+ "views": {
15
+ "sidebar": [
16
+ { "id": "ppm-db.connections", "name": "DB Connections", "type": "tree" }
17
+ ]
18
+ },
19
+ "menus": {
20
+ "commandPalette": [
21
+ { "command": "ppm-db.openViewer" },
22
+ { "command": "ppm-db.runQuery" }
23
+ ],
24
+ "view/title": [
25
+ { "command": "ppm-db.refreshConnections", "when": "view == ppm-db.connections", "group": "navigation" },
26
+ { "command": "ppm-db.addConnection", "when": "view == ppm-db.connections", "group": "navigation" }
27
+ ]
28
+ },
29
+ "configuration": {
30
+ "properties": {
31
+ "ppm-db.maxRows": { "type": "number", "default": 100, "description": "Max rows to display" },
32
+ "ppm-db.autoConnect": { "type": "boolean", "default": true, "description": "Auto-connect on open" }
33
+ }
34
+ }
35
+ },
36
+ "ppm": {
37
+ "displayName": "Database Viewer",
38
+ "icon": "database",
39
+ "webviewDir": "webview"
40
+ }
41
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * TreeDataProvider for database connections → tables → columns.
3
+ * Fetches data from PPM REST API (/api/db/*).
4
+ */
5
+
6
+ interface ConnectionNode {
7
+ id: string;
8
+ name: string;
9
+ type: "connection" | "table" | "column";
10
+ connectionId?: number;
11
+ connectionName?: string;
12
+ connectionType?: string;
13
+ connectionColor?: string | null;
14
+ schemaName?: string;
15
+ dataType?: string;
16
+ }
17
+
18
+ interface ApiConnection {
19
+ id: number;
20
+ name: string;
21
+ type: string;
22
+ color: string | null;
23
+ }
24
+
25
+ interface ApiTable {
26
+ name: string;
27
+ schema: string;
28
+ rowCount: number;
29
+ }
30
+
31
+ interface ApiColumn {
32
+ name: string;
33
+ type: string;
34
+ nullable: boolean;
35
+ pk: boolean;
36
+ defaultValue: string | null;
37
+ }
38
+
39
+ export class ConnectionTreeProvider {
40
+ private _onDidChange: { fire: (el?: ConnectionNode) => void; event: unknown };
41
+ private baseUrl: string;
42
+
43
+ constructor(eventEmitter: { fire: (el?: ConnectionNode) => void; event: unknown }, baseUrl = "") {
44
+ this._onDidChange = eventEmitter;
45
+ this.baseUrl = baseUrl;
46
+ }
47
+
48
+ get onDidChangeTreeData() {
49
+ return this._onDidChange.event;
50
+ }
51
+
52
+ refresh(): void {
53
+ this._onDidChange.fire(undefined);
54
+ }
55
+
56
+ async getChildren(element?: ConnectionNode): Promise<ConnectionNode[]> {
57
+ if (!element) return this.getConnections();
58
+ if (element.type === "connection") return this.getTables(element);
59
+ if (element.type === "table") return this.getColumns(element);
60
+ return [];
61
+ }
62
+
63
+ getTreeItem(element: ConnectionNode): Record<string, unknown> {
64
+ const isConn = element.type === "connection";
65
+ const isTable = element.type === "table";
66
+ const isCol = element.type === "column";
67
+
68
+ return {
69
+ id: element.id,
70
+ label: element.name,
71
+ description: isCol ? element.dataType : undefined,
72
+ collapsibleState: isCol ? "none" : "collapsed",
73
+ contextValue: element.type,
74
+ command: isTable ? "ppm-db.openViewer" : undefined,
75
+ commandArgs: isTable
76
+ ? [element.connectionId, element.connectionName ?? "Database", element.name, element.schemaName ?? "public"]
77
+ : undefined,
78
+ color: isConn ? (element.connectionColor ?? undefined) : undefined,
79
+ badge: isConn ? (element.connectionType === "postgres" ? "PG" : "DB") : undefined,
80
+ actions: isConn ? [
81
+ { icon: "refresh", tooltip: "Refresh tables", command: "ppm-db.refreshConnection", commandArgs: [element.connectionId] },
82
+ ] : undefined,
83
+ };
84
+ }
85
+
86
+ private async getConnections(): Promise<ConnectionNode[]> {
87
+ try {
88
+ const res = await fetch(`${this.baseUrl}/api/db/connections`);
89
+ const json = await res.json() as { ok: boolean; data?: ApiConnection[] };
90
+ if (!json.ok || !json.data) return [];
91
+ return json.data.map((c) => ({
92
+ id: `conn:${c.id}`,
93
+ name: c.name,
94
+ type: "connection" as const,
95
+ connectionId: c.id,
96
+ connectionType: c.type,
97
+ connectionColor: c.color,
98
+ }));
99
+ } catch {
100
+ return [];
101
+ }
102
+ }
103
+
104
+ private async getTables(conn: ConnectionNode): Promise<ConnectionNode[]> {
105
+ try {
106
+ const res = await fetch(`${this.baseUrl}/api/db/connections/${conn.connectionId}/tables`);
107
+ const json = await res.json() as { ok: boolean; data?: ApiTable[] };
108
+ if (!json.ok || !json.data) return [];
109
+ return json.data.map((t) => ({
110
+ id: `table:${conn.connectionId}:${t.schema}.${t.name}`,
111
+ name: t.name,
112
+ type: "table" as const,
113
+ connectionId: conn.connectionId,
114
+ connectionName: conn.name,
115
+ connectionType: conn.connectionType,
116
+ schemaName: t.schema,
117
+ }));
118
+ } catch {
119
+ return [];
120
+ }
121
+ }
122
+
123
+ private async getColumns(table: ConnectionNode): Promise<ConnectionNode[]> {
124
+ try {
125
+ const schema = table.schemaName ?? "public";
126
+ const res = await fetch(
127
+ `${this.baseUrl}/api/db/connections/${table.connectionId}/schema?table=${encodeURIComponent(table.name)}&schema=${schema}`,
128
+ );
129
+ const json = await res.json() as { ok: boolean; data?: ApiColumn[] };
130
+ if (!json.ok || !json.data) return [];
131
+ return json.data.map((c) => ({
132
+ id: `col:${table.connectionId}:${table.name}.${c.name}`,
133
+ name: c.name,
134
+ type: "column" as const,
135
+ connectionId: table.connectionId,
136
+ dataType: c.type + (c.pk ? " PK" : ""),
137
+ }));
138
+ } catch {
139
+ return [];
140
+ }
141
+ }
142
+ }