@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.
Files changed (159) hide show
  1. package/.claude/agent-memory/tester/MEMORY.md +3 -0
  2. package/.claude/agent-memory/tester/project-ppm-test-conventions.md +32 -0
  3. package/.env.example +1 -0
  4. package/.github/workflows/release.yml +46 -0
  5. package/README.md +349 -0
  6. package/bun.lock +1217 -0
  7. package/components.json +21 -0
  8. package/docs/code-standards.md +574 -0
  9. package/docs/codebase-summary.md +294 -0
  10. package/docs/deployment-guide.md +631 -0
  11. package/docs/design-guidelines.md +661 -0
  12. package/docs/project-overview-pdr.md +142 -0
  13. package/docs/project-roadmap.md +400 -0
  14. package/docs/system-architecture.md +459 -0
  15. package/package.json +68 -0
  16. package/plans/260314-2009-ppm-implementation/phase-01-project-skeleton.md +81 -0
  17. package/plans/260314-2009-ppm-implementation/phase-02-backend-core.md +148 -0
  18. package/plans/260314-2009-ppm-implementation/phase-03-frontend-shell.md +256 -0
  19. package/plans/260314-2009-ppm-implementation/phase-04-file-explorer-editor.md +120 -0
  20. package/plans/260314-2009-ppm-implementation/phase-05-web-terminal.md +174 -0
  21. package/plans/260314-2009-ppm-implementation/phase-06-git-integration.md +244 -0
  22. package/plans/260314-2009-ppm-implementation/phase-07-ai-chat.md +242 -0
  23. package/plans/260314-2009-ppm-implementation/phase-08-cli-commands.md +143 -0
  24. package/plans/260314-2009-ppm-implementation/phase-09-pwa-build-deploy.md +209 -0
  25. package/plans/260314-2009-ppm-implementation/phase-10-testing.md +311 -0
  26. package/plans/260314-2009-ppm-implementation/plan.md +202 -0
  27. package/plans/260315-0356-project-scoped-api-refactor/phase-01-backend-project-router.md +145 -0
  28. package/plans/260315-0356-project-scoped-api-refactor/phase-02-frontend-api-migration.md +107 -0
  29. package/plans/260315-0356-project-scoped-api-refactor/phase-03-per-project-tabs.md +100 -0
  30. package/plans/260315-0356-project-scoped-api-refactor/phase-04-websocket-migration.md +66 -0
  31. package/plans/260315-0356-project-scoped-api-refactor/plan.md +87 -0
  32. package/plans/reports/brainstorm-260314-1938-final-techstack.md +342 -0
  33. package/plans/reports/docs-manager-260315-1314-documentation-creation.md +386 -0
  34. package/plans/reports/fullstack-developer-260314-2252-phase-02-backend-core.md +57 -0
  35. package/plans/reports/fullstack-developer-260314-2253-phase-03-frontend-shell.md +70 -0
  36. package/plans/reports/fullstack-developer-260314-2300-phase-04-05-file-api-terminal-ws.md +49 -0
  37. package/plans/reports/fullstack-developer-260314-2300-phase-04-05-file-explorer-editor-terminal.md +52 -0
  38. package/plans/reports/fullstack-developer-260314-2307-ai-chat-phase7.md +58 -0
  39. package/plans/reports/fullstack-developer-260314-2307-phase-06-git-integration.md +33 -0
  40. package/plans/reports/research-260314-1911-ppm-tech-stack.md +318 -0
  41. package/plans/reports/research-260314-1930-claude-code-integration.md +293 -0
  42. package/plans/reports/researcher-260314-2232-node-pty-bun-crash-analysis.md +305 -0
  43. package/plans/reports/researcher-260314-2232-ui-style.md +942 -0
  44. package/plans/reports/researcher-260315-0300-opcode-claude-interaction.md +745 -0
  45. package/plans/reports/researcher-260315-0303-opcode-deep-analysis.md +742 -0
  46. package/plans/reports/researcher-260315-0305-claude-agent-sdk-github-research.md +423 -0
  47. package/plans/reports/tester-260314-2053-initial-test-suite.md +81 -0
  48. package/ppm.example.yaml +14 -0
  49. package/repomix-output.xml +23745 -0
  50. package/scripts/build.ts +13 -0
  51. package/src/cli/commands/chat-cmd.ts +259 -0
  52. package/src/cli/commands/config-cmd.ts +121 -0
  53. package/src/cli/commands/git-cmd.ts +315 -0
  54. package/src/cli/commands/init.ts +57 -0
  55. package/src/cli/commands/open.ts +19 -0
  56. package/src/cli/commands/projects.ts +100 -0
  57. package/src/cli/commands/start.ts +3 -0
  58. package/src/cli/commands/stop.ts +33 -0
  59. package/src/cli/utils/project-resolver.ts +27 -0
  60. package/src/index.ts +59 -0
  61. package/src/providers/claude-agent-sdk.ts +499 -0
  62. package/src/providers/claude-binary-finder.ts +256 -0
  63. package/src/providers/claude-code-cli.ts +413 -0
  64. package/src/providers/claude-process-registry.ts +106 -0
  65. package/src/providers/mock-provider.ts +171 -0
  66. package/src/providers/provider.interface.ts +10 -0
  67. package/src/providers/registry.ts +45 -0
  68. package/src/server/helpers/resolve-project.ts +22 -0
  69. package/src/server/index.ts +181 -0
  70. package/src/server/middleware/auth.ts +30 -0
  71. package/src/server/routes/chat.ts +153 -0
  72. package/src/server/routes/files.ts +168 -0
  73. package/src/server/routes/git.ts +261 -0
  74. package/src/server/routes/project-scoped.ts +27 -0
  75. package/src/server/routes/projects.ts +57 -0
  76. package/src/server/routes/static.ts +26 -0
  77. package/src/server/ws/chat.ts +130 -0
  78. package/src/server/ws/terminal.ts +89 -0
  79. package/src/services/chat.service.ts +110 -0
  80. package/src/services/claude-usage.service.ts +113 -0
  81. package/src/services/config.service.ts +90 -0
  82. package/src/services/file.service.ts +261 -0
  83. package/src/services/git-dirs.service.ts +112 -0
  84. package/src/services/git.service.ts +372 -0
  85. package/src/services/project.service.ts +107 -0
  86. package/src/services/slash-items.service.ts +184 -0
  87. package/src/services/terminal.service.ts +212 -0
  88. package/src/types/api.ts +37 -0
  89. package/src/types/chat.ts +92 -0
  90. package/src/types/config.ts +41 -0
  91. package/src/types/git.ts +50 -0
  92. package/src/types/project.ts +18 -0
  93. package/src/types/terminal.ts +20 -0
  94. package/src/web/app.tsx +168 -0
  95. package/src/web/components/auth/login-screen.tsx +88 -0
  96. package/src/web/components/chat/attachment-chips.tsx +55 -0
  97. package/src/web/components/chat/chat-placeholder.tsx +10 -0
  98. package/src/web/components/chat/chat-tab.tsx +301 -0
  99. package/src/web/components/chat/file-picker.tsx +126 -0
  100. package/src/web/components/chat/message-input.tsx +420 -0
  101. package/src/web/components/chat/message-list.tsx +838 -0
  102. package/src/web/components/chat/session-picker.tsx +139 -0
  103. package/src/web/components/chat/slash-command-picker.tsx +135 -0
  104. package/src/web/components/chat/usage-badge.tsx +186 -0
  105. package/src/web/components/editor/code-editor.tsx +329 -0
  106. package/src/web/components/editor/diff-viewer.tsx +276 -0
  107. package/src/web/components/editor/editor-placeholder.tsx +10 -0
  108. package/src/web/components/explorer/file-actions.tsx +191 -0
  109. package/src/web/components/explorer/file-tree.tsx +298 -0
  110. package/src/web/components/git/git-graph.tsx +727 -0
  111. package/src/web/components/git/git-placeholder.tsx +55 -0
  112. package/src/web/components/git/git-status-panel.tsx +850 -0
  113. package/src/web/components/layout/mobile-drawer.tsx +137 -0
  114. package/src/web/components/layout/mobile-nav.tsx +103 -0
  115. package/src/web/components/layout/sidebar.tsx +90 -0
  116. package/src/web/components/layout/tab-bar.tsx +152 -0
  117. package/src/web/components/layout/tab-content.tsx +85 -0
  118. package/src/web/components/projects/dir-suggest.tsx +152 -0
  119. package/src/web/components/projects/project-list.tsx +187 -0
  120. package/src/web/components/settings/settings-tab.tsx +57 -0
  121. package/src/web/components/terminal/terminal-placeholder.tsx +10 -0
  122. package/src/web/components/terminal/terminal-tab.tsx +133 -0
  123. package/src/web/components/ui/button.tsx +64 -0
  124. package/src/web/components/ui/context-menu.tsx +250 -0
  125. package/src/web/components/ui/dialog.tsx +156 -0
  126. package/src/web/components/ui/dropdown-menu.tsx +257 -0
  127. package/src/web/components/ui/input.tsx +21 -0
  128. package/src/web/components/ui/scroll-area.tsx +56 -0
  129. package/src/web/components/ui/separator.tsx +26 -0
  130. package/src/web/components/ui/sonner.tsx +40 -0
  131. package/src/web/components/ui/tabs.tsx +91 -0
  132. package/src/web/components/ui/tooltip.tsx +57 -0
  133. package/src/web/hooks/use-chat.ts +420 -0
  134. package/src/web/hooks/use-terminal.ts +182 -0
  135. package/src/web/hooks/use-url-sync.ts +66 -0
  136. package/src/web/hooks/use-websocket.ts +48 -0
  137. package/src/web/index.html +16 -0
  138. package/src/web/lib/api-client.ts +90 -0
  139. package/src/web/lib/file-support.ts +68 -0
  140. package/src/web/lib/utils.ts +6 -0
  141. package/src/web/lib/ws-client.ts +100 -0
  142. package/src/web/main.tsx +10 -0
  143. package/src/web/public/icon-192.svg +5 -0
  144. package/src/web/public/icon-512.svg +5 -0
  145. package/src/web/stores/file-store.ts +81 -0
  146. package/src/web/stores/project-store.ts +50 -0
  147. package/src/web/stores/settings-store.ts +65 -0
  148. package/src/web/stores/tab-store.ts +187 -0
  149. package/src/web/styles/globals.css +227 -0
  150. package/src/web/vite-env.d.ts +1 -0
  151. package/tests/integration/api/chat-routes.test.ts +95 -0
  152. package/tests/integration/claude-agent-sdk-integration.test.ts +228 -0
  153. package/tests/integration/ws/chat-websocket.test.ts +312 -0
  154. package/tests/test-setup.ts +5 -0
  155. package/tests/unit/providers/claude-agent-sdk.test.ts +339 -0
  156. package/tests/unit/providers/mock-provider.test.ts +143 -0
  157. package/tests/unit/services/chat-service.test.ts +100 -0
  158. package/tsconfig.json +32 -0
  159. 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)