@hienlh/ppm 0.2.20 → 0.2.21
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/CHANGELOG.md +31 -0
- package/CLAUDE.md +18 -1
- package/bun.lock +57 -59
- package/dist/ppm +0 -0
- package/dist/web/assets/chat-tab-C_U7EwM9.js +6 -0
- package/dist/web/assets/code-editor-DuarTBEe.js +1 -0
- package/dist/web/assets/diff-viewer-sBWBgb7U.js +4 -0
- package/dist/web/assets/git-graph-fOKEZiot.js +1 -0
- package/dist/web/assets/index-3zt5mBwZ.css +2 -0
- package/dist/web/assets/index-CaUQy3Zs.js +21 -0
- package/dist/web/assets/input-CTnwfHVN.js +41 -0
- package/dist/web/assets/settings-tab-C5aWMqIA.js +1 -0
- package/dist/web/assets/{terminal-tab-sGlqTp7k.js → terminal-tab-BEFAYT4S.js} +1 -1
- package/dist/web/assets/use-monaco-theme-BxaccPmI.js +11 -0
- package/dist/web/index.html +35 -8
- package/dist/web/sw.js +1 -1
- package/docs/codebase-summary.md +13 -8
- package/docs/project-roadmap.md +22 -4
- package/docs/system-architecture.md +59 -0
- package/package.json +6 -14
- package/src/providers/claude-agent-sdk.ts +2 -2
- package/src/providers/registry.ts +12 -11
- package/src/server/routes/projects.ts +43 -0
- package/src/server/routes/settings.ts +42 -8
- package/src/server/ws/chat.ts +2 -2
- package/src/services/config.service.ts +5 -1
- package/src/services/project.service.ts +1 -0
- package/src/types/config.ts +37 -0
- package/src/types/project.ts +1 -0
- package/src/web/Users/hienlh/Projects/ppm/dist/web/monacoeditorwork/css.worker.bundle.js +54268 -0
- package/src/web/Users/hienlh/Projects/ppm/dist/web/monacoeditorwork/editor.worker.bundle.js +14316 -0
- package/src/web/Users/hienlh/Projects/ppm/dist/web/monacoeditorwork/html.worker.bundle.js +30452 -0
- package/src/web/Users/hienlh/Projects/ppm/dist/web/monacoeditorwork/json.worker.bundle.js +22095 -0
- package/src/web/Users/hienlh/Projects/ppm/dist/web/monacoeditorwork/ts.worker.bundle.js +225957 -0
- package/src/web/app.tsx +43 -5
- package/src/web/components/chat/chat-history-panel.tsx +106 -0
- package/src/web/components/chat/chat-tab.tsx +27 -19
- package/src/web/components/editor/code-editor.tsx +78 -197
- package/src/web/components/editor/diff-viewer.tsx +59 -176
- package/src/web/components/layout/add-project-form.tsx +151 -0
- package/src/web/components/layout/command-palette.tsx +3 -1
- package/src/web/components/layout/editor-panel.tsx +6 -4
- package/src/web/components/layout/mobile-drawer.tsx +48 -180
- package/src/web/components/layout/mobile-nav.tsx +89 -6
- package/src/web/components/layout/panel-layout.tsx +16 -10
- package/src/web/components/layout/project-bar.tsx +329 -0
- package/src/web/components/layout/project-bottom-sheet.tsx +345 -0
- package/src/web/components/layout/sidebar.tsx +56 -142
- package/src/web/components/layout/tab-bar.tsx +1 -6
- package/src/web/components/layout/tab-content.tsx +0 -10
- package/src/web/components/ui/dialog.tsx +1 -1
- package/src/web/lib/project-avatar.ts +45 -0
- package/src/web/lib/project-palette.ts +18 -0
- package/src/web/lib/use-monaco-theme.ts +29 -0
- package/src/web/stores/panel-store.ts +96 -9
- package/src/web/stores/project-store.ts +87 -3
- package/src/web/stores/settings-store.ts +31 -4
- package/src/web/stores/tab-store.ts +0 -2
- package/vite.config.ts +6 -2
- package/dist/web/assets/arrow-up-from-line-DjfWTP75.js +0 -1
- package/dist/web/assets/button-CQ5h5gxS.js +0 -41
- package/dist/web/assets/chat-tab-Cfw__7vJ.js +0 -6
- package/dist/web/assets/code-editor-D8Pz69sx.js +0 -2
- package/dist/web/assets/dialog-BL9i7XEo.js +0 -5
- package/dist/web/assets/diff-viewer-CWS5n7ur.js +0 -4
- package/dist/web/assets/dist-0XHv8Vwc.js +0 -1
- package/dist/web/assets/dist-Ca3N8Xbh.js +0 -46
- package/dist/web/assets/git-graph-DwA62J8-.js +0 -1
- package/dist/web/assets/git-status-panel-DaB-zzSF.js +0 -1
- package/dist/web/assets/index-BYIXPY6U.css +0 -2
- package/dist/web/assets/index-DbTCLiox.js +0 -17
- package/dist/web/assets/project-list-Z4lhtp6P.js +0 -1
- package/dist/web/assets/refresh-cw-S6I91MHO.js +0 -1
- package/dist/web/assets/settings-tab-BW6MGcir.js +0 -1
- package/dist/web/assets/trash-2-CGlFXde_.js +0 -1
- package/dist/web/assets/x-C0Rw5Giw.js +0 -1
- /package/dist/web/assets/{api-client-DzH9zCD7.js → api-client-BCjah751.js} +0 -0
- /package/dist/web/assets/{columns-2-DsiY76NQ.js → columns-2-DFQ3yid7.js} +0 -0
- /package/dist/web/assets/{copy-D_Q54D-v.js → copy-B-kLwqzg.js} +0 -0
- /package/dist/web/assets/{external-link-C6Y-D528.js → external-link-Dim3NH6h.js} +0 -0
- /package/dist/web/assets/{marked.esm-Cv8mjgnt.js → marked.esm-DhBtkBa8.js} +0 -0
- /package/dist/web/assets/{utils-D6me7KDg.js → utils-B-_GCz7E.js} +0 -0
package/docs/project-roadmap.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# PPM Project Roadmap & Status
|
|
2
2
|
|
|
3
|
-
**Last Updated:** March
|
|
3
|
+
**Last Updated:** March 17, 2026
|
|
4
4
|
|
|
5
5
|
## Current Version: v2.0 (In Progress)
|
|
6
6
|
|
|
7
|
-
### Overall Progress:
|
|
7
|
+
### Overall Progress: 90%
|
|
8
8
|
|
|
9
9
|
Multi-project, project-scoped API refactor with improved UX.
|
|
10
10
|
|
|
@@ -49,13 +49,21 @@ Multi-project, project-scoped API refactor with improved UX.
|
|
|
49
49
|
- URL sync for project/tab/file state
|
|
50
50
|
- Tab metadata persistence in localStorage
|
|
51
51
|
|
|
52
|
+
**Latest Work (260317):**
|
|
53
|
+
- Project Switcher Bar: 52px left sidebar with project avatars, drag-to-reorder
|
|
54
|
+
- Project color customization (12-color palette or custom hex)
|
|
55
|
+
- Mobile ProjectBottomSheet for project selection
|
|
56
|
+
- Keep-alive workspace switching (hide/show instead of unmount, preserves xterm DOM)
|
|
57
|
+
- Sidebar tab system: Explorer / Git / History tabs (removed projects/git-status tab types)
|
|
58
|
+
- Smart project initials with collision detection (1-char, 2-char, or index fallback)
|
|
59
|
+
|
|
52
60
|
---
|
|
53
61
|
|
|
54
62
|
### Phase 4: File Explorer & Editor ✅ Complete
|
|
55
63
|
- FileTree component with directory expansion
|
|
56
|
-
-
|
|
64
|
+
- Monaco Editor integration with syntax highlighting
|
|
57
65
|
- File read/write operations
|
|
58
|
-
-
|
|
66
|
+
- Monaco diff viewer for git diffs
|
|
59
67
|
|
|
60
68
|
**Status:** Done
|
|
61
69
|
|
|
@@ -64,6 +72,11 @@ Multi-project, project-scoped API refactor with improved UX.
|
|
|
64
72
|
- File viewer for images/PDFs/markdown
|
|
65
73
|
- Better error messages
|
|
66
74
|
|
|
75
|
+
**Latest Work (260317):**
|
|
76
|
+
- Migrated CodeMirror → Monaco Editor (@monaco-editor/react) for better syntax highlighting and diff viewer
|
|
77
|
+
- Alt+Z keyboard shortcut for word wrap toggle in editor and diff viewer
|
|
78
|
+
- Improved code completion and IntelliSense
|
|
79
|
+
|
|
67
80
|
---
|
|
68
81
|
|
|
69
82
|
### Phase 5: Web Terminal ✅ Complete
|
|
@@ -206,6 +219,11 @@ Multi-project, project-scoped API refactor with improved UX.
|
|
|
206
219
|
- [x] File attachments in chat
|
|
207
220
|
- [x] Mobile responsive improvements
|
|
208
221
|
- [x] URL sync for bookmarking/sharing
|
|
222
|
+
- [x] Project Switcher Bar (52px sidebar, avatars, colors, reordering) (260317)
|
|
223
|
+
- [x] Keep-alive workspace switching (preserve xterm DOM) (260317)
|
|
224
|
+
- [x] Sidebar tab system (Explorer/Git/History) (260317)
|
|
225
|
+
- [x] Monaco Editor migration (CodeMirror → Monaco) (260317)
|
|
226
|
+
- [x] Project color customization (12-color palette + custom hex) (260317)
|
|
209
227
|
- [ ] Complete test coverage (60% complete)
|
|
210
228
|
- [ ] Documentation (in progress)
|
|
211
229
|
- [ ] Security audit (planned)
|
|
@@ -65,12 +65,17 @@
|
|
|
65
65
|
|
|
66
66
|
**Responsibilities:**
|
|
67
67
|
- Render UI for file explorer, editor, terminal, chat
|
|
68
|
+
- Project switching with visual indicators (avatars, colors, keep-alive workspaces)
|
|
68
69
|
- Capture user input (text, file uploads, terminal commands)
|
|
69
70
|
- Display streaming responses, terminal output
|
|
70
71
|
- Handle authentication (token in localStorage)
|
|
71
72
|
|
|
72
73
|
**Key Files:**
|
|
73
74
|
- `src/web/app.tsx` — Root React component
|
|
75
|
+
- `src/web/components/layout/project-bar.tsx` — Narrow left sidebar with project avatars (52px width)
|
|
76
|
+
- `src/web/components/layout/project-bottom-sheet.tsx` — Mobile project switcher (bottom sheet)
|
|
77
|
+
- `src/web/components/layout/sidebar.tsx` — Main sidebar with Explorer/Git/History tabs
|
|
78
|
+
- `src/web/components/chat/chat-history-panel.tsx` — History tab content (chat sessions)
|
|
74
79
|
- `src/web/components/` — UI components
|
|
75
80
|
- `src/cli/commands/` — CLI command handlers
|
|
76
81
|
|
|
@@ -100,6 +105,8 @@ PUT /api/settings/ai → Update AI provider settings
|
|
|
100
105
|
POST /api/projects → Create project
|
|
101
106
|
GET /api/projects → List projects
|
|
102
107
|
DELETE /api/projects/:name → Delete project
|
|
108
|
+
PATCH /api/projects/reorder → Reorder projects by name order
|
|
109
|
+
PATCH /api/projects/:name/color → Set project color (hex string)
|
|
103
110
|
GET /api/project/:name/chat/sessions → List sessions
|
|
104
111
|
POST /api/project/:name/chat/sessions → Create session
|
|
105
112
|
GET /api/project/:name/chat/sessions/:id/messages → Get history
|
|
@@ -255,6 +262,58 @@ HTTP/1.1 200 OK
|
|
|
255
262
|
|
|
256
263
|
---
|
|
257
264
|
|
|
265
|
+
## Project Workspace Management
|
|
266
|
+
|
|
267
|
+
### Keep-Alive Pattern (v2.0+)
|
|
268
|
+
When switching projects, workspaces are preserved instead of destroyed:
|
|
269
|
+
1. **Workspace Mount State**: Each project's UI (tabs, terminal xterm DOM, file selections) remains mounted in the DOM
|
|
270
|
+
2. **Visibility Toggle**: CSS `display: none/block` hides/shows workspaces instead of React unmounting
|
|
271
|
+
3. **Terminal DOM Persistence**: xterm.js terminal instances retain their DOM structure across switches (prevents re-render flicker)
|
|
272
|
+
4. **Cache Efficiency**: Zustand stores persist open tabs, selections, and scroll positions per project
|
|
273
|
+
|
|
274
|
+
**Benefits:**
|
|
275
|
+
- Instant project switching (no DOM reconstruction)
|
|
276
|
+
- Terminal history preserved across switches
|
|
277
|
+
- Smooth UX without flashing/re-rendering
|
|
278
|
+
- Reduced network requests (cached UI state)
|
|
279
|
+
|
|
280
|
+
### Project Color & Ordering (v2.0+)
|
|
281
|
+
**Storage**: Colors stored as optional `color` field in `Project` interface (hex string or undefined)
|
|
282
|
+
|
|
283
|
+
**Endpoints:**
|
|
284
|
+
- `PATCH /api/projects/:name/color` — Update project color
|
|
285
|
+
- `PATCH /api/projects/reorder` — Reorder projects array in config
|
|
286
|
+
|
|
287
|
+
**UI Components:**
|
|
288
|
+
- `ProjectBar` (52px sidebar) — Shows project avatars with color backgrounds, context menu for reorder/rename/delete/color-picker
|
|
289
|
+
- `ProjectBottomSheet` (mobile) — Bottom sheet switcher with scrollable project list
|
|
290
|
+
- `ProjectAvatar` utility — Generates smart initials with collision resolution (prefer 1-char, fallback to 2-char or index)
|
|
291
|
+
- `PROJECT_PALETTE` — 12-color palette for default colors when not customized
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## Code Editor Migration (v2.0+)
|
|
296
|
+
|
|
297
|
+
**Migration**: CodeMirror 6 → Monaco Editor (@monaco-editor/react)
|
|
298
|
+
|
|
299
|
+
**Reasons:**
|
|
300
|
+
- Better syntax highlighting for complex languages
|
|
301
|
+
- Superior IntelliSense and code completion
|
|
302
|
+
- Performance improvements on large files
|
|
303
|
+
- More polished diff viewer experience
|
|
304
|
+
|
|
305
|
+
**Components Updated:**
|
|
306
|
+
- `src/web/components/editor/code-editor.tsx` — Monaco Editor with language detection
|
|
307
|
+
- `src/web/components/editor/diff-viewer.tsx` — Monaco diff viewer for git diffs
|
|
308
|
+
|
|
309
|
+
**Features:**
|
|
310
|
+
- Alt+Z toggle for word wrap
|
|
311
|
+
- Automatic language detection from file extension
|
|
312
|
+
- Theme sync with app dark/light mode
|
|
313
|
+
- Responsive layout with proper scrolling
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
258
317
|
## Authentication Flow
|
|
259
318
|
|
|
260
319
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hienlh/ppm",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.21",
|
|
4
4
|
"description": "Personal Project Manager — mobile-first web IDE with AI assistance",
|
|
5
5
|
"module": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
"ppm": "src/index.ts"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"dev": "bun run --hot src/index.ts start & bun run vite --config vite.config.ts & wait",
|
|
12
|
-
"dev:server": "bun run --hot src/index.ts start -
|
|
11
|
+
"dev": "bun run --hot src/index.ts start -c ~/.ppm/config.dev.yaml & bun run vite --config vite.config.ts & wait",
|
|
12
|
+
"dev:server": "bun run --hot src/index.ts start -c ~/.ppm/config.dev.yaml",
|
|
13
13
|
"dev:web": "bun run vite --config vite.config.ts",
|
|
14
14
|
"build:web": "bun run vite build --config vite.config.ts",
|
|
15
15
|
"build": "bun run build:web && bun build src/index.ts --compile --outfile dist/ppm",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"@types/react-dom": "^19.2.3",
|
|
28
28
|
"@types/web-push": "^3.6.4",
|
|
29
29
|
"@vitejs/plugin-react": "^6.0.1",
|
|
30
|
+
"esbuild": "^0.27.4",
|
|
30
31
|
"tailwindcss": "^4.2.1",
|
|
31
32
|
"vite": "^8.0.0",
|
|
32
33
|
"vite-plugin-pwa": "^1.2.0"
|
|
@@ -36,23 +37,13 @@
|
|
|
36
37
|
},
|
|
37
38
|
"dependencies": {
|
|
38
39
|
"@anthropic-ai/claude-agent-sdk": "^0.2.76",
|
|
39
|
-
"@codemirror/autocomplete": "^6.20.1",
|
|
40
|
-
"@codemirror/lang-css": "^6.3.1",
|
|
41
|
-
"@codemirror/lang-html": "^6.4.11",
|
|
42
|
-
"@codemirror/lang-javascript": "^6.2.5",
|
|
43
|
-
"@codemirror/lang-json": "^6.0.2",
|
|
44
|
-
"@codemirror/lang-markdown": "^6.5.0",
|
|
45
|
-
"@codemirror/lang-python": "^6.2.1",
|
|
46
|
-
"@codemirror/merge": "^6.12.1",
|
|
47
|
-
"@codemirror/theme-one-dark": "^6.1.3",
|
|
48
40
|
"@inquirer/prompts": "^8.3.0",
|
|
49
|
-
"@
|
|
41
|
+
"@monaco-editor/react": "^4.7.0",
|
|
50
42
|
"@xterm/addon-fit": "^0.11.0",
|
|
51
43
|
"@xterm/addon-web-links": "^0.12.0",
|
|
52
44
|
"@xterm/xterm": "^6.0.0",
|
|
53
45
|
"class-variance-authority": "^0.7.1",
|
|
54
46
|
"clsx": "^2.1.1",
|
|
55
|
-
"codemirror": "^6.0.2",
|
|
56
47
|
"commander": "^14.0.3",
|
|
57
48
|
"diff2html": "^3.4.56",
|
|
58
49
|
"hono": "^4.12.8",
|
|
@@ -68,6 +59,7 @@
|
|
|
68
59
|
"simple-git": "^3.33.0",
|
|
69
60
|
"sonner": "^2.0.7",
|
|
70
61
|
"tailwind-merge": "^3.5.0",
|
|
62
|
+
"vite-plugin-monaco-editor": "^1.1.0",
|
|
71
63
|
"web-push": "^3.6.7",
|
|
72
64
|
"zustand": "^5.0.11"
|
|
73
65
|
}
|
|
@@ -56,8 +56,8 @@ interface PendingApproval {
|
|
|
56
56
|
* Uses canUseTool callback for tool approvals and AskUserQuestion.
|
|
57
57
|
*/
|
|
58
58
|
export class ClaudeAgentSdkProvider implements AIProvider {
|
|
59
|
-
id = "claude
|
|
60
|
-
name = "Claude
|
|
59
|
+
id = "claude";
|
|
60
|
+
name = "Claude";
|
|
61
61
|
|
|
62
62
|
private activeSessions = new Map<string, Session>();
|
|
63
63
|
private messageCount = new Map<string, number>();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AIProvider } from "./provider.interface.ts";
|
|
2
2
|
import { MockProvider } from "./mock-provider.ts";
|
|
3
3
|
import { ClaudeAgentSdkProvider } from "./claude-agent-sdk.ts";
|
|
4
|
+
import { configService } from "../services/config.service.ts";
|
|
4
5
|
|
|
5
6
|
export interface ProviderInfo {
|
|
6
7
|
id: string;
|
|
@@ -9,13 +10,9 @@ export interface ProviderInfo {
|
|
|
9
10
|
|
|
10
11
|
class ProviderRegistry {
|
|
11
12
|
private providers = new Map<string, AIProvider>();
|
|
12
|
-
private defaultId: string | null = null;
|
|
13
13
|
|
|
14
14
|
register(provider: AIProvider): void {
|
|
15
15
|
this.providers.set(provider.id, provider);
|
|
16
|
-
if (!this.defaultId) {
|
|
17
|
-
this.defaultId = provider.id;
|
|
18
|
-
}
|
|
19
16
|
}
|
|
20
17
|
|
|
21
18
|
get(id: string): AIProvider | undefined {
|
|
@@ -29,15 +26,19 @@ class ProviderRegistry {
|
|
|
29
26
|
}));
|
|
30
27
|
}
|
|
31
28
|
|
|
29
|
+
/** Get the default provider based on config's default_provider */
|
|
32
30
|
getDefault(): AIProvider {
|
|
33
|
-
|
|
34
|
-
const provider = this.providers.get(
|
|
35
|
-
if (
|
|
36
|
-
|
|
31
|
+
const defaultId = configService.get("ai").default_provider;
|
|
32
|
+
const provider = this.providers.get(defaultId);
|
|
33
|
+
if (provider) return provider;
|
|
34
|
+
// Fallback to "claude" if config value doesn't match any registered provider
|
|
35
|
+
const fallback = this.providers.get("claude");
|
|
36
|
+
if (fallback) return fallback;
|
|
37
|
+
throw new Error(`Default provider "${defaultId}" not found in registry`);
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
/** Singleton registry
|
|
41
|
+
/** Singleton registry */
|
|
41
42
|
export const providerRegistry = new ProviderRegistry();
|
|
42
|
-
providerRegistry.register(new ClaudeAgentSdkProvider());
|
|
43
|
-
providerRegistry.register(new MockProvider());
|
|
43
|
+
providerRegistry.register(new ClaudeAgentSdkProvider());
|
|
44
|
+
providerRegistry.register(new MockProvider()); // testing only
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
2
|
import { projectService } from "../../services/project.service.ts";
|
|
3
|
+
import { configService } from "../../services/config.service.ts";
|
|
3
4
|
import { searchGitDirs } from "../../services/git-dirs.service.ts";
|
|
4
5
|
import { ok, err } from "../../types/api.ts";
|
|
5
6
|
|
|
@@ -45,6 +46,48 @@ projectRoutes.get("/suggest-dirs", (c) => {
|
|
|
45
46
|
}
|
|
46
47
|
});
|
|
47
48
|
|
|
49
|
+
/** PATCH /api/projects/reorder — reorder projects array */
|
|
50
|
+
projectRoutes.patch("/reorder", async (c) => {
|
|
51
|
+
try {
|
|
52
|
+
const body = await c.req.json<{ order: string[] }>();
|
|
53
|
+
if (!Array.isArray(body.order)) {
|
|
54
|
+
return c.json(err("Missing required field: order (string[])"), 400);
|
|
55
|
+
}
|
|
56
|
+
const projects = configService.get("projects");
|
|
57
|
+
const orderMap = new Map(body.order.map((name, i) => [name, i]));
|
|
58
|
+
const reordered = [...projects].sort((a, b) => {
|
|
59
|
+
const ai = orderMap.get(a.name) ?? Infinity;
|
|
60
|
+
const bi = orderMap.get(b.name) ?? Infinity;
|
|
61
|
+
return ai - bi;
|
|
62
|
+
});
|
|
63
|
+
configService.set("projects", reordered);
|
|
64
|
+
configService.save();
|
|
65
|
+
return c.json(ok({ reordered: reordered.length }));
|
|
66
|
+
} catch (e) {
|
|
67
|
+
return c.json(err((e as Error).message), 400);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
/** PATCH /api/projects/:name/color — set project color */
|
|
72
|
+
projectRoutes.patch("/:name/color", async (c) => {
|
|
73
|
+
try {
|
|
74
|
+
const name = c.req.param("name");
|
|
75
|
+
const body = await c.req.json<{ color: string | null }>();
|
|
76
|
+
const projects = configService.get("projects");
|
|
77
|
+
const idx = projects.findIndex((p) => p.name === name);
|
|
78
|
+
if (idx === -1) return c.json(err(`Project not found: ${name}`), 404);
|
|
79
|
+
const updated = { ...projects[idx]! };
|
|
80
|
+
if (body.color) updated.color = body.color;
|
|
81
|
+
else delete updated.color;
|
|
82
|
+
projects[idx] = updated;
|
|
83
|
+
configService.set("projects", projects);
|
|
84
|
+
configService.save();
|
|
85
|
+
return c.json(ok(updated));
|
|
86
|
+
} catch (e) {
|
|
87
|
+
return c.json(err((e as Error).message), 400);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
48
91
|
/** PATCH /api/projects/:name — update a project's name/path */
|
|
49
92
|
projectRoutes.patch("/:name", async (c) => {
|
|
50
93
|
try {
|
|
@@ -3,20 +3,51 @@ import { configService } from "../../services/config.service.ts";
|
|
|
3
3
|
import {
|
|
4
4
|
validateAIProviderConfig,
|
|
5
5
|
validateDefaultProvider,
|
|
6
|
+
VALID_PROVIDERS,
|
|
6
7
|
type AIProviderConfig,
|
|
8
|
+
type ThemeConfig,
|
|
7
9
|
} from "../../types/config.ts";
|
|
8
10
|
import { ok, err } from "../../types/api.ts";
|
|
9
11
|
|
|
10
12
|
export const settingsRoutes = new Hono();
|
|
11
13
|
|
|
14
|
+
/** Strip api_key_env from all providers in an AI config object */
|
|
15
|
+
function stripSensitiveFields(ai: { providers: Record<string, unknown> }) {
|
|
16
|
+
const clone = structuredClone(ai);
|
|
17
|
+
for (const provider of Object.values(clone.providers)) {
|
|
18
|
+
delete (provider as Record<string, unknown>).api_key_env;
|
|
19
|
+
}
|
|
20
|
+
return clone;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ── Theme ─────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
/** GET /settings/theme */
|
|
26
|
+
settingsRoutes.get("/theme", (c) => {
|
|
27
|
+
return c.json(ok({ theme: configService.get("theme") ?? "system" }));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/** PUT /settings/theme */
|
|
31
|
+
settingsRoutes.put("/theme", async (c) => {
|
|
32
|
+
try {
|
|
33
|
+
const { theme } = await c.req.json<{ theme: ThemeConfig }>();
|
|
34
|
+
if (!["light", "dark", "system"].includes(theme)) {
|
|
35
|
+
return c.json(err("theme must be light, dark, or system"), 400);
|
|
36
|
+
}
|
|
37
|
+
configService.set("theme", theme);
|
|
38
|
+
configService.save();
|
|
39
|
+
return c.json(ok({ theme }));
|
|
40
|
+
} catch (e) {
|
|
41
|
+
return c.json(err((e as Error).message), 400);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// ── AI ────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
12
47
|
/** GET /settings/ai — return current AI config (strips api_key_env) */
|
|
13
48
|
settingsRoutes.get("/ai", (c) => {
|
|
14
|
-
const ai =
|
|
15
|
-
|
|
16
|
-
for (const provider of Object.values(ai.providers)) {
|
|
17
|
-
delete (provider as unknown as Record<string, unknown>).api_key_env;
|
|
18
|
-
}
|
|
19
|
-
return c.json(ok(ai));
|
|
49
|
+
const ai = configService.get("ai");
|
|
50
|
+
return c.json(ok(stripSensitiveFields(ai)));
|
|
20
51
|
});
|
|
21
52
|
|
|
22
53
|
/** PUT /settings/ai — update AI provider settings, writes to yaml */
|
|
@@ -54,8 +85,11 @@ settingsRoutes.put("/ai", async (c) => {
|
|
|
54
85
|
}
|
|
55
86
|
}
|
|
56
87
|
|
|
57
|
-
// Validate default_provider references existing provider
|
|
88
|
+
// Validate default_provider is in allowed list and references existing provider
|
|
58
89
|
if (body.default_provider) {
|
|
90
|
+
if (!VALID_PROVIDERS.includes(body.default_provider as any)) {
|
|
91
|
+
return c.json(err(`default_provider must be one of: ${VALID_PROVIDERS.join(", ")}`), 400);
|
|
92
|
+
}
|
|
59
93
|
const dpErr = validateDefaultProvider(updated.default_provider, updated.providers);
|
|
60
94
|
if (dpErr) return c.json(err(dpErr), 400);
|
|
61
95
|
}
|
|
@@ -63,7 +97,7 @@ settingsRoutes.put("/ai", async (c) => {
|
|
|
63
97
|
configService.set("ai", updated);
|
|
64
98
|
configService.save();
|
|
65
99
|
|
|
66
|
-
return c.json(ok(updated));
|
|
100
|
+
return c.json(ok(stripSensitiveFields(updated)));
|
|
67
101
|
} catch (e) {
|
|
68
102
|
return c.json(err((e as Error).message), 400);
|
|
69
103
|
}
|
package/src/server/ws/chat.ts
CHANGED
|
@@ -159,7 +159,7 @@ export const chatWebSocket = {
|
|
|
159
159
|
open(ws: ChatWsSocket) {
|
|
160
160
|
const { sessionId, projectName } = ws.data;
|
|
161
161
|
const session = chatService.getSession(sessionId);
|
|
162
|
-
const providerId = session?.providerId ??
|
|
162
|
+
const providerId = session?.providerId ?? providerRegistry.getDefault().id;
|
|
163
163
|
|
|
164
164
|
let projectPath: string | undefined;
|
|
165
165
|
if (projectName) {
|
|
@@ -238,7 +238,7 @@ export const chatWebSocket = {
|
|
|
238
238
|
}
|
|
239
239
|
|
|
240
240
|
const entry = activeSessions.get(sessionId);
|
|
241
|
-
const providerId = entry?.providerId ??
|
|
241
|
+
const providerId = entry?.providerId ?? providerRegistry.getDefault().id;
|
|
242
242
|
|
|
243
243
|
if (parsed.type === "message") {
|
|
244
244
|
// Resume session in provider first
|
|
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { randomBytes } from "node:crypto";
|
|
5
5
|
import yaml from "js-yaml";
|
|
6
6
|
import type { PpmConfig } from "../types/config.ts";
|
|
7
|
-
import { DEFAULT_CONFIG } from "../types/config.ts";
|
|
7
|
+
import { DEFAULT_CONFIG, sanitizeConfig } from "../types/config.ts";
|
|
8
8
|
|
|
9
9
|
const PPM_DIR = resolve(homedir(), ".ppm");
|
|
10
10
|
const GLOBAL_CONFIG_PATH = resolve(PPM_DIR, "config.yaml");
|
|
@@ -34,6 +34,10 @@ class ConfigService {
|
|
|
34
34
|
this.config.auth.token = randomBytes(16).toString("hex");
|
|
35
35
|
this.save();
|
|
36
36
|
}
|
|
37
|
+
// Fix invalid config values and persist corrections
|
|
38
|
+
if (sanitizeConfig(this.config)) {
|
|
39
|
+
this.save();
|
|
40
|
+
}
|
|
37
41
|
return this.config;
|
|
38
42
|
}
|
|
39
43
|
}
|
package/src/types/config.ts
CHANGED
|
@@ -4,10 +4,13 @@ export interface PushConfig {
|
|
|
4
4
|
vapid_subject: string;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
export type ThemeConfig = "light" | "dark" | "system";
|
|
8
|
+
|
|
7
9
|
export interface PpmConfig {
|
|
8
10
|
device_name: string;
|
|
9
11
|
port: number;
|
|
10
12
|
host: string;
|
|
13
|
+
theme: ThemeConfig;
|
|
11
14
|
auth: AuthConfig;
|
|
12
15
|
projects: ProjectConfig[];
|
|
13
16
|
ai: AIConfig;
|
|
@@ -22,6 +25,7 @@ export interface AuthConfig {
|
|
|
22
25
|
export interface ProjectConfig {
|
|
23
26
|
path: string;
|
|
24
27
|
name: string;
|
|
28
|
+
color?: string;
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
export interface AIConfig {
|
|
@@ -44,6 +48,7 @@ export const DEFAULT_CONFIG: PpmConfig = {
|
|
|
44
48
|
device_name: "",
|
|
45
49
|
port: 8080,
|
|
46
50
|
host: "0.0.0.0",
|
|
51
|
+
theme: "system",
|
|
47
52
|
auth: { enabled: true, token: "" },
|
|
48
53
|
projects: [],
|
|
49
54
|
ai: {
|
|
@@ -63,6 +68,9 @@ export const DEFAULT_CONFIG: PpmConfig = {
|
|
|
63
68
|
const VALID_TYPES = ["agent-sdk", "mock"] as const;
|
|
64
69
|
const VALID_EFFORTS = ["low", "medium", "high", "max"] as const;
|
|
65
70
|
const VALID_MODELS = ["claude-sonnet-4-6", "claude-opus-4-6", "claude-haiku-4-5"] as const;
|
|
71
|
+
/** Only these values are allowed for default_provider in config */
|
|
72
|
+
export const VALID_PROVIDERS = ["claude"] as const;
|
|
73
|
+
const VALID_THEMES: ThemeConfig[] = ["light", "dark", "system"];
|
|
66
74
|
|
|
67
75
|
/** Validate AI provider config fields. Returns array of error messages (empty = valid). */
|
|
68
76
|
export function validateAIProviderConfig(config: Partial<AIProviderConfig>): string[] {
|
|
@@ -95,3 +103,32 @@ export function validateDefaultProvider(defaultProvider: string, providers: Reco
|
|
|
95
103
|
}
|
|
96
104
|
return null;
|
|
97
105
|
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Sanitize a loaded config — fix invalid values to defaults.
|
|
109
|
+
* Returns true if any field was corrected (caller should save).
|
|
110
|
+
*/
|
|
111
|
+
export function sanitizeConfig(config: PpmConfig): boolean {
|
|
112
|
+
let dirty = false;
|
|
113
|
+
|
|
114
|
+
// Fix invalid theme
|
|
115
|
+
if (!VALID_THEMES.includes(config.theme)) {
|
|
116
|
+
config.theme = DEFAULT_CONFIG.theme;
|
|
117
|
+
dirty = true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Fix invalid default_provider — must be in VALID_PROVIDERS
|
|
121
|
+
if (!VALID_PROVIDERS.includes(config.ai.default_provider as any)) {
|
|
122
|
+
config.ai.default_provider = DEFAULT_CONFIG.ai.default_provider;
|
|
123
|
+
dirty = true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Ensure the default provider has a config entry
|
|
127
|
+
if (!config.ai.providers[config.ai.default_provider]) {
|
|
128
|
+
config.ai.providers[config.ai.default_provider] =
|
|
129
|
+
structuredClone(DEFAULT_CONFIG.ai.providers[DEFAULT_CONFIG.ai.default_provider]!);
|
|
130
|
+
dirty = true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return dirty;
|
|
134
|
+
}
|