@hienlh/ppm 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agent-memory/tester/MEMORY.md +3 -0
- package/.claude/agent-memory/tester/project-ppm-test-conventions.md +32 -0
- package/.env.example +1 -0
- package/.github/workflows/release.yml +46 -0
- package/README.md +349 -0
- package/bun.lock +1217 -0
- package/components.json +21 -0
- package/docs/code-standards.md +574 -0
- package/docs/codebase-summary.md +294 -0
- package/docs/deployment-guide.md +631 -0
- package/docs/design-guidelines.md +661 -0
- package/docs/project-overview-pdr.md +142 -0
- package/docs/project-roadmap.md +400 -0
- package/docs/system-architecture.md +459 -0
- package/package.json +68 -0
- package/plans/260314-2009-ppm-implementation/phase-01-project-skeleton.md +81 -0
- package/plans/260314-2009-ppm-implementation/phase-02-backend-core.md +148 -0
- package/plans/260314-2009-ppm-implementation/phase-03-frontend-shell.md +256 -0
- package/plans/260314-2009-ppm-implementation/phase-04-file-explorer-editor.md +120 -0
- package/plans/260314-2009-ppm-implementation/phase-05-web-terminal.md +174 -0
- package/plans/260314-2009-ppm-implementation/phase-06-git-integration.md +244 -0
- package/plans/260314-2009-ppm-implementation/phase-07-ai-chat.md +242 -0
- package/plans/260314-2009-ppm-implementation/phase-08-cli-commands.md +143 -0
- package/plans/260314-2009-ppm-implementation/phase-09-pwa-build-deploy.md +209 -0
- package/plans/260314-2009-ppm-implementation/phase-10-testing.md +311 -0
- package/plans/260314-2009-ppm-implementation/plan.md +202 -0
- package/plans/260315-0356-project-scoped-api-refactor/phase-01-backend-project-router.md +145 -0
- package/plans/260315-0356-project-scoped-api-refactor/phase-02-frontend-api-migration.md +107 -0
- package/plans/260315-0356-project-scoped-api-refactor/phase-03-per-project-tabs.md +100 -0
- package/plans/260315-0356-project-scoped-api-refactor/phase-04-websocket-migration.md +66 -0
- package/plans/260315-0356-project-scoped-api-refactor/plan.md +87 -0
- package/plans/reports/brainstorm-260314-1938-final-techstack.md +342 -0
- package/plans/reports/docs-manager-260315-1314-documentation-creation.md +386 -0
- package/plans/reports/fullstack-developer-260314-2252-phase-02-backend-core.md +57 -0
- package/plans/reports/fullstack-developer-260314-2253-phase-03-frontend-shell.md +70 -0
- package/plans/reports/fullstack-developer-260314-2300-phase-04-05-file-api-terminal-ws.md +49 -0
- package/plans/reports/fullstack-developer-260314-2300-phase-04-05-file-explorer-editor-terminal.md +52 -0
- package/plans/reports/fullstack-developer-260314-2307-ai-chat-phase7.md +58 -0
- package/plans/reports/fullstack-developer-260314-2307-phase-06-git-integration.md +33 -0
- package/plans/reports/research-260314-1911-ppm-tech-stack.md +318 -0
- package/plans/reports/research-260314-1930-claude-code-integration.md +293 -0
- package/plans/reports/researcher-260314-2232-node-pty-bun-crash-analysis.md +305 -0
- package/plans/reports/researcher-260314-2232-ui-style.md +942 -0
- package/plans/reports/researcher-260315-0300-opcode-claude-interaction.md +745 -0
- package/plans/reports/researcher-260315-0303-opcode-deep-analysis.md +742 -0
- package/plans/reports/researcher-260315-0305-claude-agent-sdk-github-research.md +423 -0
- package/plans/reports/tester-260314-2053-initial-test-suite.md +81 -0
- package/ppm.example.yaml +14 -0
- package/repomix-output.xml +23745 -0
- package/scripts/build.ts +13 -0
- package/src/cli/commands/chat-cmd.ts +259 -0
- package/src/cli/commands/config-cmd.ts +121 -0
- package/src/cli/commands/git-cmd.ts +315 -0
- package/src/cli/commands/init.ts +57 -0
- package/src/cli/commands/open.ts +19 -0
- package/src/cli/commands/projects.ts +100 -0
- package/src/cli/commands/start.ts +3 -0
- package/src/cli/commands/stop.ts +33 -0
- package/src/cli/utils/project-resolver.ts +27 -0
- package/src/index.ts +59 -0
- package/src/providers/claude-agent-sdk.ts +499 -0
- package/src/providers/claude-binary-finder.ts +256 -0
- package/src/providers/claude-code-cli.ts +413 -0
- package/src/providers/claude-process-registry.ts +106 -0
- package/src/providers/mock-provider.ts +171 -0
- package/src/providers/provider.interface.ts +10 -0
- package/src/providers/registry.ts +45 -0
- package/src/server/helpers/resolve-project.ts +22 -0
- package/src/server/index.ts +181 -0
- package/src/server/middleware/auth.ts +30 -0
- package/src/server/routes/chat.ts +153 -0
- package/src/server/routes/files.ts +168 -0
- package/src/server/routes/git.ts +261 -0
- package/src/server/routes/project-scoped.ts +27 -0
- package/src/server/routes/projects.ts +57 -0
- package/src/server/routes/static.ts +26 -0
- package/src/server/ws/chat.ts +130 -0
- package/src/server/ws/terminal.ts +89 -0
- package/src/services/chat.service.ts +110 -0
- package/src/services/claude-usage.service.ts +113 -0
- package/src/services/config.service.ts +90 -0
- package/src/services/file.service.ts +261 -0
- package/src/services/git-dirs.service.ts +112 -0
- package/src/services/git.service.ts +372 -0
- package/src/services/project.service.ts +107 -0
- package/src/services/slash-items.service.ts +184 -0
- package/src/services/terminal.service.ts +212 -0
- package/src/types/api.ts +37 -0
- package/src/types/chat.ts +92 -0
- package/src/types/config.ts +41 -0
- package/src/types/git.ts +50 -0
- package/src/types/project.ts +18 -0
- package/src/types/terminal.ts +20 -0
- package/src/web/app.tsx +168 -0
- package/src/web/components/auth/login-screen.tsx +88 -0
- package/src/web/components/chat/attachment-chips.tsx +55 -0
- package/src/web/components/chat/chat-placeholder.tsx +10 -0
- package/src/web/components/chat/chat-tab.tsx +301 -0
- package/src/web/components/chat/file-picker.tsx +126 -0
- package/src/web/components/chat/message-input.tsx +420 -0
- package/src/web/components/chat/message-list.tsx +838 -0
- package/src/web/components/chat/session-picker.tsx +139 -0
- package/src/web/components/chat/slash-command-picker.tsx +135 -0
- package/src/web/components/chat/usage-badge.tsx +186 -0
- package/src/web/components/editor/code-editor.tsx +329 -0
- package/src/web/components/editor/diff-viewer.tsx +276 -0
- package/src/web/components/editor/editor-placeholder.tsx +10 -0
- package/src/web/components/explorer/file-actions.tsx +191 -0
- package/src/web/components/explorer/file-tree.tsx +298 -0
- package/src/web/components/git/git-graph.tsx +727 -0
- package/src/web/components/git/git-placeholder.tsx +55 -0
- package/src/web/components/git/git-status-panel.tsx +850 -0
- package/src/web/components/layout/mobile-drawer.tsx +137 -0
- package/src/web/components/layout/mobile-nav.tsx +103 -0
- package/src/web/components/layout/sidebar.tsx +90 -0
- package/src/web/components/layout/tab-bar.tsx +152 -0
- package/src/web/components/layout/tab-content.tsx +85 -0
- package/src/web/components/projects/dir-suggest.tsx +152 -0
- package/src/web/components/projects/project-list.tsx +187 -0
- package/src/web/components/settings/settings-tab.tsx +57 -0
- package/src/web/components/terminal/terminal-placeholder.tsx +10 -0
- package/src/web/components/terminal/terminal-tab.tsx +133 -0
- package/src/web/components/ui/button.tsx +64 -0
- package/src/web/components/ui/context-menu.tsx +250 -0
- package/src/web/components/ui/dialog.tsx +156 -0
- package/src/web/components/ui/dropdown-menu.tsx +257 -0
- package/src/web/components/ui/input.tsx +21 -0
- package/src/web/components/ui/scroll-area.tsx +56 -0
- package/src/web/components/ui/separator.tsx +26 -0
- package/src/web/components/ui/sonner.tsx +40 -0
- package/src/web/components/ui/tabs.tsx +91 -0
- package/src/web/components/ui/tooltip.tsx +57 -0
- package/src/web/hooks/use-chat.ts +420 -0
- package/src/web/hooks/use-terminal.ts +182 -0
- package/src/web/hooks/use-url-sync.ts +66 -0
- package/src/web/hooks/use-websocket.ts +48 -0
- package/src/web/index.html +16 -0
- package/src/web/lib/api-client.ts +90 -0
- package/src/web/lib/file-support.ts +68 -0
- package/src/web/lib/utils.ts +6 -0
- package/src/web/lib/ws-client.ts +100 -0
- package/src/web/main.tsx +10 -0
- package/src/web/public/icon-192.svg +5 -0
- package/src/web/public/icon-512.svg +5 -0
- package/src/web/stores/file-store.ts +81 -0
- package/src/web/stores/project-store.ts +50 -0
- package/src/web/stores/settings-store.ts +65 -0
- package/src/web/stores/tab-store.ts +187 -0
- package/src/web/styles/globals.css +227 -0
- package/src/web/vite-env.d.ts +1 -0
- package/tests/integration/api/chat-routes.test.ts +95 -0
- package/tests/integration/claude-agent-sdk-integration.test.ts +228 -0
- package/tests/integration/ws/chat-websocket.test.ts +312 -0
- package/tests/test-setup.ts +5 -0
- package/tests/unit/providers/claude-agent-sdk.test.ts +339 -0
- package/tests/unit/providers/mock-provider.test.ts +143 -0
- package/tests/unit/services/chat-service.test.ts +100 -0
- package/tsconfig.json +32 -0
- package/vite.config.ts +62 -0
|
@@ -0,0 +1,174 @@
|
|
|
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)
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# Phase 6: Git Integration
|
|
2
|
+
|
|
3
|
+
**Owner:** backend-dev (API) + frontend-dev (UI) — parallel
|
|
4
|
+
**Priority:** High
|
|
5
|
+
**Depends on:** Phase 4 (file explorer, diff viewer reuse)
|
|
6
|
+
**Effort:** Large (git graph is complex)
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
Git status panel (stage/unstage/commit/push/pull), git diff viewer, git graph visualization (SVG, ported from vscode-git-graph approach).
|
|
11
|
+
|
|
12
|
+
## Backend (backend-dev)
|
|
13
|
+
|
|
14
|
+
### Files
|
|
15
|
+
```
|
|
16
|
+
src/services/git.service.ts
|
|
17
|
+
src/server/routes/git.ts
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Git Service
|
|
21
|
+
```typescript
|
|
22
|
+
import simpleGit from 'simple-git';
|
|
23
|
+
|
|
24
|
+
class GitService {
|
|
25
|
+
// Status
|
|
26
|
+
async status(projectPath: string): Promise<GitStatus>
|
|
27
|
+
async diff(projectPath: string, ref1?: string, ref2?: string): Promise<string> // unified diff
|
|
28
|
+
async fileDiff(projectPath: string, filePath: string): Promise<string>
|
|
29
|
+
|
|
30
|
+
// Staging
|
|
31
|
+
async stage(projectPath: string, files: string[]): Promise<void>
|
|
32
|
+
async unstage(projectPath: string, files: string[]): Promise<void>
|
|
33
|
+
|
|
34
|
+
// Commit + Push/Pull
|
|
35
|
+
async commit(projectPath: string, message: string): Promise<string> // returns hash
|
|
36
|
+
async push(projectPath: string, remote?: string, branch?: string): Promise<void>
|
|
37
|
+
async pull(projectPath: string, remote?: string, branch?: string): Promise<void>
|
|
38
|
+
|
|
39
|
+
// Branch ops
|
|
40
|
+
async branches(projectPath: string): Promise<GitBranch[]>
|
|
41
|
+
async createBranch(projectPath: string, name: string, from?: string): Promise<void>
|
|
42
|
+
async checkout(projectPath: string, ref: string): Promise<void>
|
|
43
|
+
async deleteBranch(projectPath: string, name: string, force?: boolean): Promise<void>
|
|
44
|
+
async merge(projectPath: string, source: string): Promise<void>
|
|
45
|
+
|
|
46
|
+
// Graph data
|
|
47
|
+
async graphData(projectPath: string, maxCount?: number): Promise<GitGraphData>
|
|
48
|
+
|
|
49
|
+
// Advanced (web-only, no CLI needed)
|
|
50
|
+
async cherryPick(projectPath: string, hash: string): Promise<void>
|
|
51
|
+
async revert(projectPath: string, hash: string): Promise<void>
|
|
52
|
+
async createTag(projectPath: string, name: string, hash?: string): Promise<void>
|
|
53
|
+
|
|
54
|
+
// PR URL
|
|
55
|
+
getCreatePrUrl(projectPath: string, branch: string): string | null
|
|
56
|
+
// Parse remote URL → GitHub/GitLab PR creation URL
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Graph Data Extraction
|
|
61
|
+
|
|
62
|
+
**[V2 FIX]** Do NOT parse `git log --format` manually with newline separators. Use simple-git's built-in `.log()` which returns correctly typed `LogResult`:
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
async graphData(projectPath: string, maxCount = 200): Promise<GitGraphData> {
|
|
66
|
+
const git = simpleGit(projectPath);
|
|
67
|
+
|
|
68
|
+
// Use simple-git's built-in log() — handles parsing correctly
|
|
69
|
+
const log = await git.log({
|
|
70
|
+
'--all': null,
|
|
71
|
+
maxCount,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// log.all is already typed: { hash, date, message, author_name, author_email, refs, body, diff? }[]
|
|
75
|
+
const commits: GitCommit[] = log.all.map(c => ({
|
|
76
|
+
hash: c.hash,
|
|
77
|
+
abbreviatedHash: c.hash.slice(0, 7),
|
|
78
|
+
subject: c.message,
|
|
79
|
+
body: c.body,
|
|
80
|
+
authorName: c.author_name,
|
|
81
|
+
authorEmail: c.author_email,
|
|
82
|
+
authorDate: c.date,
|
|
83
|
+
parents: [], // Need separate call or parse refs
|
|
84
|
+
refs: c.refs ? c.refs.split(', ').filter(Boolean) : [],
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
// Get parent hashes via raw format (safe: one field per line)
|
|
88
|
+
const parentLog = await git.raw([
|
|
89
|
+
'log', '--all', `--max-count=${maxCount}`,
|
|
90
|
+
'--format=%H %P' // hash + space-separated parents on ONE line
|
|
91
|
+
]);
|
|
92
|
+
const parentMap = new Map<string, string[]>();
|
|
93
|
+
for (const line of parentLog.trim().split('\n')) {
|
|
94
|
+
const [hash, ...parents] = line.split(' ');
|
|
95
|
+
if (hash) parentMap.set(hash, parents.filter(Boolean));
|
|
96
|
+
}
|
|
97
|
+
for (const c of commits) {
|
|
98
|
+
c.parents = parentMap.get(c.hash) ?? [];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const branchSummary = await git.branch(['-a', '--no-color']);
|
|
102
|
+
const branches: GitBranch[] = Object.entries(branchSummary.branches).map(([name, info]) => ({
|
|
103
|
+
name,
|
|
104
|
+
current: info.current,
|
|
105
|
+
remote: name.startsWith('remotes/'),
|
|
106
|
+
commitHash: info.commit,
|
|
107
|
+
ahead: 0,
|
|
108
|
+
behind: 0,
|
|
109
|
+
}));
|
|
110
|
+
|
|
111
|
+
return { commits, branches };
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Lane Allocation Algorithm
|
|
116
|
+
Port from vscode-git-graph `web/graph.ts`:
|
|
117
|
+
- Each branch gets a lane (column index)
|
|
118
|
+
- Merge/fork lines connect lanes
|
|
119
|
+
- Color = `laneIndex % colorPalette.length`
|
|
120
|
+
- Return: `Map<commitHash, { lane: number, lines: Line[] }>`
|
|
121
|
+
|
|
122
|
+
### API Routes
|
|
123
|
+
```
|
|
124
|
+
GET /api/git/status/:project
|
|
125
|
+
GET /api/git/diff/:project?ref1=&ref2=
|
|
126
|
+
GET /api/git/file-diff/:project?file=&ref=
|
|
127
|
+
GET /api/git/graph/:project?max=500
|
|
128
|
+
GET /api/git/branches/:project
|
|
129
|
+
POST /api/git/stage { project, files }
|
|
130
|
+
POST /api/git/unstage { project, files }
|
|
131
|
+
POST /api/git/commit { project, message }
|
|
132
|
+
POST /api/git/push { project, remote?, branch? }
|
|
133
|
+
POST /api/git/pull { project, remote?, branch? }
|
|
134
|
+
POST /api/git/branch/create { project, name, from? }
|
|
135
|
+
POST /api/git/checkout { project, ref }
|
|
136
|
+
POST /api/git/branch/delete { project, name, force? }
|
|
137
|
+
POST /api/git/merge { project, source }
|
|
138
|
+
POST /api/git/cherry-pick { project, hash }
|
|
139
|
+
POST /api/git/revert { project, hash }
|
|
140
|
+
POST /api/git/tag { project, name, hash? }
|
|
141
|
+
GET /api/git/pr-url/:project?branch= → { url }
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Frontend (frontend-dev)
|
|
145
|
+
|
|
146
|
+
### Files
|
|
147
|
+
```
|
|
148
|
+
src/web/components/git/git-graph.tsx
|
|
149
|
+
src/web/components/git/git-graph-renderer.tsx
|
|
150
|
+
src/web/components/git/git-status-panel.tsx
|
|
151
|
+
src/web/components/git/git-diff-tab.tsx
|
|
152
|
+
src/web/components/git/commit-context-menu.tsx
|
|
153
|
+
src/web/lib/git-graph-layout.ts
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Git Status Panel
|
|
157
|
+
- Split into: Changes (unstaged) + Staged Changes
|
|
158
|
+
- Each file: icon (M/A/D/R), filename, click → open diff
|
|
159
|
+
- Buttons: Stage All, Unstage All
|
|
160
|
+
- Individual file: click +/- to stage/unstage
|
|
161
|
+
- Commit section: textarea input + "Commit" button
|
|
162
|
+
- Push/Pull buttons with branch name display
|
|
163
|
+
- Auto-refresh via WS `/ws/events` or polling
|
|
164
|
+
|
|
165
|
+
### Git Graph (SVG)
|
|
166
|
+
```typescript
|
|
167
|
+
// git-graph-renderer.tsx
|
|
168
|
+
// Receives: commits + lanes from backend API
|
|
169
|
+
|
|
170
|
+
const GitGraphRenderer = ({ data }: { data: GitGraphData }) => {
|
|
171
|
+
// SVG element with:
|
|
172
|
+
// 1. Branch lines: <path> elements, curved or angular
|
|
173
|
+
// 2. Commit nodes: <circle> elements
|
|
174
|
+
// 3. Labels: branch names, tags as badges
|
|
175
|
+
// 4. Click handlers on commits → expand details
|
|
176
|
+
// 5. Context menu on commits and branches
|
|
177
|
+
|
|
178
|
+
// Virtualization: only render visible rows
|
|
179
|
+
// Each commit row height = GRID_Y (e.g., 24px)
|
|
180
|
+
// Scroll container with virtual list
|
|
181
|
+
};
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Git Graph Context Menu (shadcn/ui ContextMenu)
|
|
185
|
+
**On commit node:**
|
|
186
|
+
- Checkout this commit
|
|
187
|
+
- Create branch here...
|
|
188
|
+
- Cherry pick
|
|
189
|
+
- Revert
|
|
190
|
+
- Create tag...
|
|
191
|
+
- Copy commit hash
|
|
192
|
+
- View diff
|
|
193
|
+
|
|
194
|
+
**On branch label:**
|
|
195
|
+
- Checkout
|
|
196
|
+
- Merge into current branch
|
|
197
|
+
- Delete branch
|
|
198
|
+
- Rename branch
|
|
199
|
+
- Push
|
|
200
|
+
- Pull
|
|
201
|
+
- Rebase onto current
|
|
202
|
+
- Create Pull Request → opens browser URL
|
|
203
|
+
|
|
204
|
+
### Git Diff Tab
|
|
205
|
+
- Reuse `@codemirror/merge` diff viewer from Phase 4
|
|
206
|
+
- Opened from: git status (click file), git graph (view diff), file explorer (compare)
|
|
207
|
+
- Header: file path, ref1 vs ref2
|
|
208
|
+
|
|
209
|
+
## Success Criteria
|
|
210
|
+
|
|
211
|
+
**Git Status Panel:**
|
|
212
|
+
- [ ] Shows two sections: "Changes" (unstaged) and "Staged Changes" with file counts
|
|
213
|
+
- [ ] Each file shows status icon (M=modified, A=added, D=deleted, R=renamed) + filename
|
|
214
|
+
- [ ] Click file in either section → opens diff view in new tab
|
|
215
|
+
- [ ] Click "+" on unstaged file → stages it (moves to Staged section)
|
|
216
|
+
- [ ] Click "−" on staged file → unstages it (moves to Changes section)
|
|
217
|
+
- [ ] "Stage All" button stages all changed files
|
|
218
|
+
- [ ] "Unstage All" button unstages all staged files
|
|
219
|
+
- [ ] Commit section: textarea for message + "Commit" button
|
|
220
|
+
- [ ] Commit with empty message → button disabled / shows warning
|
|
221
|
+
- [ ] Commit with nothing staged → shows "Nothing to commit" message
|
|
222
|
+
- [ ] After successful commit → status panel refreshes, staged files clear
|
|
223
|
+
- [ ] Push/Pull buttons show current branch name, disabled when no remote
|
|
224
|
+
|
|
225
|
+
**Git Graph:**
|
|
226
|
+
- [ ] Renders commit history as SVG with colored branch lanes
|
|
227
|
+
- [ ] Each commit: circle node + abbreviated hash + subject + author + relative date
|
|
228
|
+
- [ ] Branch labels rendered as colored badges on corresponding commits
|
|
229
|
+
- [ ] Merge commits show lines connecting from parent lanes
|
|
230
|
+
- [ ] Scroll through 200+ commits without performance issues (virtualized rendering)
|
|
231
|
+
- [ ] Right-click commit → context menu: Checkout, Create branch, Cherry pick, Revert, Create tag, Copy hash, View diff
|
|
232
|
+
- [ ] Right-click branch label → context menu: Checkout, Merge into current, Delete, Push, Create PR
|
|
233
|
+
- [ ] "Create PR" → opens correct GitHub/GitLab URL (parsed from remote) in new browser tab
|
|
234
|
+
|
|
235
|
+
**Git Diff:**
|
|
236
|
+
- [ ] Diff tab shows file path + refs in header
|
|
237
|
+
- [ ] Side-by-side diff using `@codemirror/merge` with syntax highlighting
|
|
238
|
+
- [ ] Added lines highlighted green, removed lines highlighted red
|
|
239
|
+
- [ ] Opened from: git status (click file), git graph (view diff), file explorer (compare)
|
|
240
|
+
|
|
241
|
+
**Mobile:**
|
|
242
|
+
- [ ] Git graph scrollable horizontally and vertically with touch
|
|
243
|
+
- [ ] Context menu appears on long-press (commit node or branch label)
|
|
244
|
+
- [ ] Status panel: swipe file row to stage/unstage (nice-to-have, buttons work as fallback)
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# Phase 7: AI Chat
|
|
2
|
+
|
|
3
|
+
**Owner:** backend-dev (provider + WS) + frontend-dev (chat UI) — parallel
|
|
4
|
+
**Priority:** High
|
|
5
|
+
**Depends on:** Phase 2, Phase 3
|
|
6
|
+
**Effort:** Large
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
AI chat with Claude Agent SDK as first provider. Generic AIProvider interface for multi-provider support. Chat UI with streaming, tool approvals, session management.
|
|
11
|
+
|
|
12
|
+
## Backend (backend-dev)
|
|
13
|
+
|
|
14
|
+
### Files
|
|
15
|
+
```
|
|
16
|
+
src/providers/provider.interface.ts # Already in types, implement here
|
|
17
|
+
src/providers/claude-agent-sdk.ts
|
|
18
|
+
src/providers/cli-subprocess.ts # Stub for future
|
|
19
|
+
src/providers/registry.ts
|
|
20
|
+
src/services/chat.service.ts
|
|
21
|
+
src/server/ws/chat.ts
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Claude Agent SDK Provider
|
|
25
|
+
```typescript
|
|
26
|
+
import { query, listSessions, getSessionMessages } from '@anthropic-ai/claude-agent-sdk';
|
|
27
|
+
|
|
28
|
+
class ClaudeAgentSdkProvider implements AIProvider {
|
|
29
|
+
id = 'claude';
|
|
30
|
+
name = 'Claude Code';
|
|
31
|
+
|
|
32
|
+
async createSession(config: SessionConfig): Promise<Session> {
|
|
33
|
+
// Start a new query() — capture session_id from init message
|
|
34
|
+
// Return session handle
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async resumeSession(sessionId: string): Promise<Session> {
|
|
38
|
+
// Use { resume: sessionId } option
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async *sendMessage(sessionId: string, message: string): AsyncIterable<ChatEvent> {
|
|
42
|
+
const options = sessionId
|
|
43
|
+
? { resume: sessionId, allowedTools: [...] }
|
|
44
|
+
: { allowedTools: [...] };
|
|
45
|
+
|
|
46
|
+
for await (const msg of query({ prompt: message, options })) {
|
|
47
|
+
// Map SDK messages → ChatEvent types
|
|
48
|
+
if (msg.type === 'assistant') {
|
|
49
|
+
for (const block of msg.content) {
|
|
50
|
+
if (block.type === 'text') yield { type: 'text', content: block.text };
|
|
51
|
+
if (block.type === 'tool_use') yield { type: 'tool_use', tool: block.name, input: block.input };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (msg.type === 'result') {
|
|
55
|
+
yield { type: 'done', sessionId: msg.session_id };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async listSessions(): Promise<SessionInfo[]> {
|
|
61
|
+
return await listSessions();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async deleteSession(sessionId: string): Promise<void> {
|
|
65
|
+
// Delete session file from ~/.claude/projects/...
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Tool Approval Flow
|
|
71
|
+
```typescript
|
|
72
|
+
// In claude-agent-sdk.ts
|
|
73
|
+
// canUseTool callback → forward to frontend via WS
|
|
74
|
+
|
|
75
|
+
const options = {
|
|
76
|
+
canUseTool: async (toolName: string, input: any) => {
|
|
77
|
+
// Send approval request to frontend via WS
|
|
78
|
+
const response = await this.requestApproval(sessionId, toolName, input);
|
|
79
|
+
if (response.approved) {
|
|
80
|
+
return { behavior: 'allow', updatedInput: input };
|
|
81
|
+
}
|
|
82
|
+
return { behavior: 'deny', message: response.reason || 'User denied' };
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Provider Registry
|
|
88
|
+
```typescript
|
|
89
|
+
class ProviderRegistry {
|
|
90
|
+
private providers: Map<string, AIProvider> = new Map();
|
|
91
|
+
|
|
92
|
+
register(provider: AIProvider): void
|
|
93
|
+
get(id: string): AIProvider | undefined
|
|
94
|
+
list(): AIProviderInfo[]
|
|
95
|
+
getDefault(): AIProvider
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Chat Service
|
|
100
|
+
```typescript
|
|
101
|
+
class ChatService {
|
|
102
|
+
constructor(private registry: ProviderRegistry)
|
|
103
|
+
|
|
104
|
+
async createSession(providerId: string, config: SessionConfig): Promise<Session>
|
|
105
|
+
async resumeSession(providerId: string, sessionId: string): Promise<Session>
|
|
106
|
+
async listSessions(providerId?: string): Promise<SessionInfo[]>
|
|
107
|
+
async deleteSession(providerId: string, sessionId: string): Promise<void>
|
|
108
|
+
sendMessage(providerId: string, sessionId: string, message: string): AsyncIterable<ChatEvent>
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### WebSocket Handler
|
|
113
|
+
```
|
|
114
|
+
WS /ws/chat/:sessionId
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Protocol (JSON messages):
|
|
118
|
+
```typescript
|
|
119
|
+
// Client → Server
|
|
120
|
+
{ type: 'message', content: string }
|
|
121
|
+
{ type: 'approval_response', requestId: string, approved: boolean, reason?: string }
|
|
122
|
+
|
|
123
|
+
// Server → Client
|
|
124
|
+
{ type: 'text', content: string }
|
|
125
|
+
{ type: 'tool_use', tool: string, input: any }
|
|
126
|
+
{ type: 'tool_result', output: string }
|
|
127
|
+
{ type: 'approval_request', requestId: string, tool: string, input: any }
|
|
128
|
+
{ type: 'done', sessionId: string }
|
|
129
|
+
{ type: 'error', message: string }
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Frontend (frontend-dev)
|
|
133
|
+
|
|
134
|
+
### Files
|
|
135
|
+
```
|
|
136
|
+
src/web/components/chat/chat-tab.tsx
|
|
137
|
+
src/web/components/chat/message-list.tsx
|
|
138
|
+
src/web/components/chat/message-input.tsx
|
|
139
|
+
src/web/components/chat/tool-approval.tsx
|
|
140
|
+
src/web/components/chat/session-picker.tsx
|
|
141
|
+
src/web/hooks/use-chat.ts
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Chat Tab Layout
|
|
145
|
+
```
|
|
146
|
+
┌─────────────────────────────────┐
|
|
147
|
+
│ Claude Code ▼ [Session: abc] │ ← provider picker + session info
|
|
148
|
+
├─────────────────────────────────┤
|
|
149
|
+
│ │
|
|
150
|
+
│ User: Fix the bug in auth.ts │
|
|
151
|
+
│ │
|
|
152
|
+
│ Claude: I'll read the file... │
|
|
153
|
+
│ 📄 Read auth.ts │
|
|
154
|
+
│ ✅ Tool result: (content) │
|
|
155
|
+
│ │
|
|
156
|
+
│ ⚠️ Bash: rm -rf /tmp/test │
|
|
157
|
+
│ [Allow] [Deny] │ ← tool approval dialog
|
|
158
|
+
│ │
|
|
159
|
+
├─────────────────────────────────┤
|
|
160
|
+
│ 📎 Type a message... [Send] │ ← input with file attach
|
|
161
|
+
└─────────────────────────────────┘
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Message Types Rendering
|
|
165
|
+
- **User message:** Right-aligned bubble (or left with avatar)
|
|
166
|
+
- **Assistant text:** Markdown rendered (code blocks with syntax highlight)
|
|
167
|
+
- **Tool use:** Collapsible card showing tool name + input
|
|
168
|
+
- **Tool result:** Collapsible card showing output (truncated)
|
|
169
|
+
- **Approval request:** Highlighted card with Allow/Deny buttons + tool details
|
|
170
|
+
- **Error:** Red alert banner
|
|
171
|
+
|
|
172
|
+
### Session Picker
|
|
173
|
+
- Dropdown or modal listing all sessions
|
|
174
|
+
- Each session: provider icon, title (first message truncated), timestamp
|
|
175
|
+
- Actions: Resume, Delete, Fork (nice-to-have)
|
|
176
|
+
- "New Chat" button → create fresh session
|
|
177
|
+
|
|
178
|
+
### useChat Hook
|
|
179
|
+
```typescript
|
|
180
|
+
const useChat = (sessionId?: string) => {
|
|
181
|
+
const ws = useWebSocket(`/ws/chat/${sessionId}`);
|
|
182
|
+
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
|
183
|
+
const [isStreaming, setIsStreaming] = useState(false);
|
|
184
|
+
const [pendingApproval, setPendingApproval] = useState<ApprovalRequest | null>(null);
|
|
185
|
+
|
|
186
|
+
const sendMessage = (content: string) => {
|
|
187
|
+
ws.send(JSON.stringify({ type: 'message', content }));
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const respondToApproval = (requestId: string, approved: boolean) => {
|
|
191
|
+
ws.send(JSON.stringify({ type: 'approval_response', requestId, approved }));
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
return { messages, isStreaming, pendingApproval, sendMessage, respondToApproval };
|
|
195
|
+
};
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Mobile Considerations
|
|
199
|
+
- Chat input: sticky bottom, auto-resize textarea
|
|
200
|
+
- Long messages: scrollable code blocks
|
|
201
|
+
- Tool approval: full-width card, large touch targets for Allow/Deny
|
|
202
|
+
- Keyboard: input should push content up, not cover it
|
|
203
|
+
|
|
204
|
+
## Chat History & Reconnect
|
|
205
|
+
|
|
206
|
+
### REST API for Message History
|
|
207
|
+
- `GET /api/chat/sessions` → list all sessions (id, provider, title, createdAt)
|
|
208
|
+
- `GET /api/chat/sessions/:id/messages` → full message history for a session
|
|
209
|
+
- On WS reconnect: client loads history via REST, then subscribes to live stream
|
|
210
|
+
- This avoids WS replay complexity — REST is simpler and more reliable
|
|
211
|
+
|
|
212
|
+
### Reconnect Flow
|
|
213
|
+
```
|
|
214
|
+
Client disconnects
|
|
215
|
+
→ WS closes
|
|
216
|
+
→ Client: exponential backoff reconnect
|
|
217
|
+
→ Client reconnects to same sessionId
|
|
218
|
+
→ Client: GET /api/chat/sessions/:id/messages → render history
|
|
219
|
+
→ Server: resume streaming from where it left off (if AI still generating)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Session Persistence
|
|
223
|
+
- Claude Agent SDK sessions persist automatically (stored in ~/.claude/)
|
|
224
|
+
- PPM stores session metadata in `~/.ppm/chat-sessions.json`: id, provider, title, projectName, createdAt
|
|
225
|
+
- Session list fetched from PPM metadata file (fast), not from SDK (slow)
|
|
226
|
+
|
|
227
|
+
## Success Criteria
|
|
228
|
+
|
|
229
|
+
- [ ] Can create new chat session → WS connects, session appears in session list
|
|
230
|
+
- [ ] Sending message streams AI response text in real-time (character by character)
|
|
231
|
+
- [ ] Tool use blocks render with tool name + collapsible input JSON
|
|
232
|
+
- [ ] Tool result blocks render with collapsible output
|
|
233
|
+
- [ ] Tool approval dialog: shows tool name + input, Allow/Deny buttons work, response sent via WS
|
|
234
|
+
- [ ] After Allow → tool executes and result streams back; after Deny → AI acknowledges denial
|
|
235
|
+
- [ ] Can resume existing session: select from picker → loads history via REST → WS reconnects
|
|
236
|
+
- [ ] Session list shows all sessions with provider icon, truncated title, timestamp
|
|
237
|
+
- [ ] "New Chat" button creates fresh session with current project context
|
|
238
|
+
- [ ] Multiple chat tabs work simultaneously (each with own WS connection)
|
|
239
|
+
- [ ] WS disconnect → reconnect → message history loaded from REST API
|
|
240
|
+
- [ ] Works on mobile: sticky bottom input, keyboard pushes content up, large Allow/Deny buttons
|
|
241
|
+
- [ ] Markdown rendering in AI messages: code blocks with syntax highlighting, lists, headers
|
|
242
|
+
- [ ] Error messages from AI/server shown as red alert banner (not silent failure)
|