@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,21 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": false,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "src/web/styles/globals.css",
9
+ "baseColor": "slate",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "aliases": {
14
+ "components": "@/components",
15
+ "utils": "@/lib/utils",
16
+ "ui": "@/components/ui",
17
+ "lib": "@/lib",
18
+ "hooks": "@/hooks"
19
+ },
20
+ "iconLibrary": "lucide"
21
+ }
@@ -0,0 +1,574 @@
1
+ # PPM Code Standards & Conventions
2
+
3
+ ## File Naming
4
+
5
+ | File Type | Convention | Example | Purpose |
6
+ |-----------|-----------|---------|---------|
7
+ | CLI commands | kebab-case | `start-cmd.ts`, `init.ts` | Descriptive command names |
8
+ | Services | kebab-case | `chat.service.ts`, `file.service.ts` | `{feature}.service.ts` pattern |
9
+ | Providers | kebab-case | `claude-agent-sdk.ts`, `mock-provider.ts` | `{name}-provider.ts` or `{name}.ts` |
10
+ | Routes | kebab-case | `chat.ts`, `project-scoped.ts` | Describe HTTP route group |
11
+ | WebSocket | kebab-case | `chat.ts`, `terminal.ts` | Match feature area |
12
+ | Components | PascalCase | `ChatTab.tsx`, `FileTree.tsx` | React convention |
13
+ | Hooks | camelCase with `use` prefix | `useChat.ts`, `useTerminal.ts` | React hook convention |
14
+ | Stores | kebab-case | `chat-store.ts`, `project-store.ts` | Zustand store files |
15
+ | Utilities | kebab-case | `utils.ts`, `file-support.ts` | Grouped by function |
16
+ | Types | kebab-case | `api.ts`, `chat.ts` | Group related types together |
17
+ | Tests | kebab-case with `.test.ts` | `chat.service.test.ts` | Match source file name |
18
+
19
+ ## TypeScript Conventions
20
+
21
+ ### Strict Mode
22
+ All files use TypeScript strict mode (`tsconfig.json` `"strict": true`).
23
+
24
+ ```typescript
25
+ // Required:
26
+ - Explicit return types on functions
27
+ - No `any` types (use `unknown` if necessary)
28
+ - No implicit `any` parameters
29
+ - Exhaustive type checking (switch, conditionals)
30
+ ```
31
+
32
+ ### Path Aliases
33
+ Use `@/*` alias for web layer imports (configured in `tsconfig.json`):
34
+
35
+ ```typescript
36
+ // Good
37
+ import { useChat } from "@/hooks/use-chat";
38
+ import { chatStore } from "@/stores/chat-store";
39
+
40
+ // Avoid
41
+ import { useChat } from "../../hooks/use-chat";
42
+ ```
43
+
44
+ ### Type Definitions
45
+ Place types near usage. Group related types in single files:
46
+
47
+ ```typescript
48
+ // Good: src/types/chat.ts
49
+ export interface Session { id: string; title: string; }
50
+ export interface Message { id: string; content: string; role: "user" | "assistant"; }
51
+ export type ChatEvent = { type: "text"; content: string } | { type: "done" };
52
+
53
+ // Avoid: spread across separate files
54
+ ```
55
+
56
+ ### Enums & Unions
57
+ Prefer discriminated unions over enums for better tree-shaking:
58
+
59
+ ```typescript
60
+ // Good: Discriminated union
61
+ type ChatEvent =
62
+ | { type: "text"; content: string }
63
+ | { type: "tool_use"; tool: string; input: unknown }
64
+ | { type: "done" };
65
+
66
+ // Avoid: Enum
67
+ enum MessageType {
68
+ TEXT,
69
+ TOOL_USE,
70
+ DONE,
71
+ }
72
+ ```
73
+
74
+ ### Async/Await
75
+ Always use `async`/`await` over Promise chains. Use async generators for streaming:
76
+
77
+ ```typescript
78
+ // Good: Async generator for streaming
79
+ async *streamMessages(input: string) {
80
+ for await (const event of provider.sendMessage(input)) {
81
+ yield event;
82
+ }
83
+ }
84
+
85
+ // Avoid: Promise chains
86
+ provider.sendMessage(input).then(...)
87
+ ```
88
+
89
+ ### Error Handling
90
+ Use try-catch for async operations. Throw structured errors:
91
+
92
+ ```typescript
93
+ // Good
94
+ try {
95
+ const file = await FileService.read(path);
96
+ } catch (error) {
97
+ const message = error instanceof Error ? error.message : "Unknown error";
98
+ console.error(`Failed to read ${path}: ${message}`);
99
+ throw new Error(`FileService.read failed: ${message}`);
100
+ }
101
+
102
+ // Avoid: Silent failures
103
+ const file = await FileService.read(path).catch(() => null);
104
+ ```
105
+
106
+ ## Component Patterns
107
+
108
+ ### React Components
109
+ Use functional components with hooks. Keep components focused:
110
+
111
+ ```typescript
112
+ // Good: Single responsibility
113
+ export function ChatTab() {
114
+ const { messages, sendMessage } = useChat();
115
+ return <div>/* Chat UI */</div>;
116
+ }
117
+
118
+ // Avoid: God component
119
+ export function ChatTab() {
120
+ // File management, git status, terminal, chat
121
+ }
122
+ ```
123
+
124
+ ### Zustand Stores
125
+ Define stores as singleton exports. Use selectors to subscribe to specific state:
126
+
127
+ ```typescript
128
+ // Good: src/web/stores/chat-store.ts
129
+ export const chatStore = create<ChatState>((set) => ({
130
+ messages: [],
131
+ addMessage: (msg) => set((state) => ({ messages: [...state.messages, msg] })),
132
+ }));
133
+
134
+ // Usage with selector (avoids full re-render)
135
+ const messages = chatStore((state) => state.messages);
136
+ ```
137
+
138
+ ### Custom Hooks
139
+ Extract logic into hooks for reusability. Return stable references:
140
+
141
+ ```typescript
142
+ // Good: useChat hook
143
+ export function useChat() {
144
+ const [messages, setMessages] = useState<Message[]>([]);
145
+
146
+ const sendMessage = useCallback(async (text: string) => {
147
+ // Send logic
148
+ }, []);
149
+
150
+ return { messages, sendMessage };
151
+ }
152
+ ```
153
+
154
+ ### Lazy-Loaded Tab Content
155
+ Use React.lazy() for code splitting:
156
+
157
+ ```typescript
158
+ // Good
159
+ const ChatTab = lazy(() => import("./chat-tab").then(m => ({ default: m.ChatTab })));
160
+
161
+ // In component
162
+ <Suspense fallback={<Spinner />}>
163
+ <ChatTab />
164
+ </Suspense>
165
+ ```
166
+
167
+ ## Service Patterns
168
+
169
+ ### Singleton Services
170
+ Services are singletons exported as functions or instances:
171
+
172
+ ```typescript
173
+ // Good: services/chat.service.ts
174
+ export async function createSession(projectPath: string): Promise<Session> {
175
+ // Shared logic across all callers
176
+ }
177
+
178
+ // Good: services/config.service.ts
179
+ export const ConfigService = {
180
+ load: () => YAML.parse(configFile),
181
+ save: (config) => YAML.stringify(config),
182
+ };
183
+ ```
184
+
185
+ ### Dependency Injection
186
+ Services should receive dependencies as parameters or imports:
187
+
188
+ ```typescript
189
+ // Good: Pass dependencies explicitly
190
+ export async function streamChat(
191
+ session: Session,
192
+ message: string,
193
+ provider: AIProvider, // Dependency
194
+ ) {
195
+ // Use provider
196
+ }
197
+
198
+ // Avoid: Implicit globals
199
+ import { globalProvider } from "./global"; // Hidden dependency
200
+ ```
201
+
202
+ ### Error Propagation
203
+ Services throw descriptive errors; routes catch and format:
204
+
205
+ ```typescript
206
+ // Good: Service throws
207
+ export function validatePath(path: string) {
208
+ if (path.includes("..")) {
209
+ throw new Error(`Path traversal detected: ${path}`);
210
+ }
211
+ }
212
+
213
+ // Good: Route catches and formats
214
+ try {
215
+ const content = await FileService.read(path);
216
+ res.json(ok(content));
217
+ } catch (error) {
218
+ const msg = error instanceof Error ? error.message : "Unknown";
219
+ res.json(err(msg));
220
+ res.status(400);
221
+ }
222
+ ```
223
+
224
+ ## API Conventions
225
+
226
+ ### Response Envelope
227
+ All REST responses use the `ApiResponse<T>` envelope:
228
+
229
+ ```typescript
230
+ // Good
231
+ { ok: true, data: { /* payload */ } }
232
+ { ok: false, error: "descriptive error message" }
233
+
234
+ // Avoid: Inconsistent shapes
235
+ { success: true, result: { } }
236
+ { error: "error message" } // No shape contract
237
+ ```
238
+
239
+ ### Project-Scoped Routes
240
+ All project-specific endpoints use the pattern `/api/project/:name/*`:
241
+
242
+ ```
243
+ GET /api/projects # List all projects
244
+ POST /api/projects # Create project
245
+ DELETE /api/projects/:name # Delete project
246
+ GET /api/project/:name/chat/... # Chat (project-scoped)
247
+ GET /api/project/:name/git/... # Git (project-scoped)
248
+ GET /api/project/:name/files/... # Files (project-scoped)
249
+ ```
250
+
251
+ ### WebSocket Message Formats
252
+ Structure WebSocket messages as typed JSON objects:
253
+
254
+ ```typescript
255
+ // Client -> Server (chat)
256
+ { type: "message"; content: string }
257
+ { type: "cancel" }
258
+ { type: "approval_response"; requestId: string; approved: boolean }
259
+
260
+ // Server -> Client (chat)
261
+ { type: "text"; content: string }
262
+ { type: "tool_use"; tool: string; input: unknown }
263
+ { type: "approval_request"; requestId: string; tool: string; input: unknown }
264
+ { type: "done"; sessionId: string }
265
+ { type: "error"; message: string }
266
+ ```
267
+
268
+ ### Status Codes
269
+ Use standard HTTP status codes:
270
+
271
+ ```typescript
272
+ // Success
273
+ 200 OK - GET successful, POST/PUT/DELETE with response body
274
+ 201 Created - POST created resource
275
+ 204 No Content - DELETE successful
276
+
277
+ // Client error
278
+ 400 Bad Request - Invalid input, validation failure
279
+ 401 Unauthorized - Missing/invalid auth token
280
+ 403 Forbidden - Authenticated but not authorized (rare in PPM)
281
+ 404 Not Found - Project/file/session not found
282
+
283
+ // Server error
284
+ 500 Internal Error - Unexpected exception
285
+ ```
286
+
287
+ ## Import/Export Conventions
288
+
289
+ ### Named Exports (Preferred)
290
+ Use named exports for better tree-shaking and clarity:
291
+
292
+ ```typescript
293
+ // Good: services/file.service.ts
294
+ export async function read(path: string): Promise<string> { }
295
+ export async function write(path: string, content: string): Promise<void> { }
296
+
297
+ // Usage
298
+ import { read, write } from "./services/file.service";
299
+ ```
300
+
301
+ ### Default Exports (React Components Only)
302
+ Use default exports for React components (enables lazy loading):
303
+
304
+ ```typescript
305
+ // Good: components/chat/chat-tab.tsx
306
+ export default function ChatTab() { }
307
+
308
+ // Usage
309
+ const ChatTab = lazy(() => import("./components/chat/chat-tab"));
310
+ ```
311
+
312
+ ### Wildcard Imports (Avoid)
313
+ Avoid wildcard imports except for types:
314
+
315
+ ```typescript
316
+ // Good: Explicit imports
317
+ import { send, receive } from "./ws-client";
318
+
319
+ // Good: Type wildcard (rare)
320
+ import type * as Types from "./types";
321
+
322
+ // Avoid: Implicit exports
323
+ import * as WsClient from "./ws-client";
324
+ WsClient.send(); // Unclear what's exported
325
+ ```
326
+
327
+ ## Error Handling Patterns
328
+
329
+ ### Service Layer
330
+ Throw descriptive errors with context:
331
+
332
+ ```typescript
333
+ // Good
334
+ throw new Error(`GitService.commit failed: ${error.message}`);
335
+ throw new Error(`FileService: path traversal detected: ${path}`);
336
+
337
+ // Avoid: Generic errors
338
+ throw new Error("Failed");
339
+ throw error; // Re-throw loses context
340
+ ```
341
+
342
+ ### Route Layer
343
+ Catch, format, and return `ApiResponse` with error:
344
+
345
+ ```typescript
346
+ // Good
347
+ try {
348
+ const result = await service.doSomething();
349
+ return res.json(ok(result));
350
+ } catch (error) {
351
+ const message = error instanceof Error ? error.message : "Unknown error";
352
+ res.status(400);
353
+ return res.json(err(message));
354
+ }
355
+ ```
356
+
357
+ ### Component Layer
358
+ Handle errors from API calls with user-friendly messages:
359
+
360
+ ```typescript
361
+ // Good
362
+ try {
363
+ const response = await api.post("/...");
364
+ if (!response.ok) {
365
+ setError(response.error);
366
+ return;
367
+ }
368
+ setState(response.data);
369
+ } catch (error) {
370
+ setError("Network error. Please try again.");
371
+ }
372
+ ```
373
+
374
+ ## Testing Conventions
375
+
376
+ ### Test File Location
377
+ Tests colocate with source files or in `tests/` directory:
378
+
379
+ ```
380
+ src/services/chat.service.ts
381
+ tests/unit/services/chat.service.test.ts ← Match path, add .test suffix
382
+
383
+ src/web/hooks/use-chat.ts
384
+ tests/unit/hooks/use-chat.test.ts
385
+ ```
386
+
387
+ ### Test Structure
388
+ Use AAA pattern (Arrange, Act, Assert):
389
+
390
+ ```typescript
391
+ describe("ChatService", () => {
392
+ it("should create session with unique ID", () => {
393
+ // Arrange
394
+ const projectPath = "/tmp/project";
395
+
396
+ // Act
397
+ const session = ChatService.createSession(projectPath);
398
+ const session2 = ChatService.createSession(projectPath);
399
+
400
+ // Assert
401
+ expect(session.id).not.toBe(session2.id);
402
+ });
403
+ });
404
+ ```
405
+
406
+ ### Mocking
407
+ Mock external dependencies (providers, file system):
408
+
409
+ ```typescript
410
+ // Good: Mock provider
411
+ const mockProvider = {
412
+ createSession: () => ({ id: "test-id" }),
413
+ sendMessage: async function*() { yield { type: "text", content: "response" }; },
414
+ };
415
+
416
+ // Avoid: Mock implementation details
417
+ jest.spyOn(fs, "readFile").mockResolvedValue("content");
418
+ ```
419
+
420
+ ## Security Conventions
421
+
422
+ ### Path Traversal Protection
423
+ Always validate file paths before operations:
424
+
425
+ ```typescript
426
+ // Good: Validate before file access
427
+ export function validatePath(path: string) {
428
+ const normalized = Path.normalize(path);
429
+ if (normalized.startsWith("..")) {
430
+ throw new Error("Path traversal detected");
431
+ }
432
+ if (!normalized.startsWith(projectPath)) {
433
+ throw new Error("Access denied: outside project directory");
434
+ }
435
+ }
436
+ ```
437
+
438
+ ### Token-Based Auth
439
+ Every API route requires token validation via middleware:
440
+
441
+ ```typescript
442
+ // Good: Middleware validates token
443
+ app.use("/api", authMiddleware);
444
+
445
+ // In middleware
446
+ const token = req.header("Authorization");
447
+ if (!token || token !== config.auth.token) {
448
+ res.status(401);
449
+ res.json(err("Unauthorized"));
450
+ }
451
+ ```
452
+
453
+ ### Input Validation
454
+ Validate all user input (file paths, command arguments, message content):
455
+
456
+ ```typescript
457
+ // Good: Validate before processing
458
+ if (!path || typeof path !== "string") {
459
+ throw new Error("Invalid path");
460
+ }
461
+
462
+ if (message.length > 10000) {
463
+ throw new Error("Message too long");
464
+ }
465
+ ```
466
+
467
+ ## Documentation Conventions
468
+
469
+ ### Inline Comments
470
+ Use comments for **why**, not **what**. Let code speak for itself:
471
+
472
+ ```typescript
473
+ // Good: Explain intent
474
+ // Expand node lazily to avoid blocking on large directories
475
+ async function loadChildren(node: TreeNode) {
476
+ // ...
477
+ }
478
+
479
+ // Avoid: Obvious comments
480
+ // Set messages to empty array
481
+ const [messages, setMessages] = useState([]);
482
+ ```
483
+
484
+ ### JSDoc for Public APIs
485
+ Document exported functions with JSDoc:
486
+
487
+ ```typescript
488
+ /**
489
+ * Stream chat messages from AI provider.
490
+ *
491
+ * @param sessionId - Chat session ID
492
+ * @param message - User message text
493
+ * @param provider - AI provider (defaults to registry.default)
494
+ * @yields ChatEvent objects (text, tool_use, approval_request, done)
495
+ * @throws Error if session not found or provider fails
496
+ */
497
+ export async *streamMessages(
498
+ sessionId: string,
499
+ message: string,
500
+ provider?: AIProvider,
501
+ ) {
502
+ // ...
503
+ }
504
+ ```
505
+
506
+ ### Type Comments
507
+ Use type comments for complex types:
508
+
509
+ ```typescript
510
+ // File status with git tracking info
511
+ type FileStatus =
512
+ | { status: "modified" }
513
+ | { status: "untracked" }
514
+ | { status: "staged"; originalPath?: string };
515
+ ```
516
+
517
+ ## Performance Conventions
518
+
519
+ ### Code Splitting
520
+ Use React.lazy() for routes and heavy components:
521
+
522
+ ```typescript
523
+ // Good: Lazy-load terminal component
524
+ const TerminalTab = lazy(() => import("./terminal-tab"));
525
+ ```
526
+
527
+ ### Memoization
528
+ Memoize expensive computations and callbacks:
529
+
530
+ ```typescript
531
+ // Good: Memoize filter result
532
+ const filteredFiles = useMemo(
533
+ () => files.filter(f => f.name.includes(query)),
534
+ [files, query]
535
+ );
536
+
537
+ // Good: Stable callback reference
538
+ const handleClick = useCallback(() => {
539
+ // ...
540
+ }, [dependencies]);
541
+ ```
542
+
543
+ ### Bundle Analysis
544
+ Monitor bundle size growth:
545
+
546
+ ```bash
547
+ # Check bundle stats
548
+ bun run build && ls -lh dist/web/assets/
549
+ ```
550
+
551
+ ## Git Conventions
552
+
553
+ ### Commit Messages
554
+ Use conventional commit format:
555
+
556
+ ```
557
+ feat: add file attachment support to chat
558
+ fix: resolve WebSocket reconnection issue
559
+ refactor: simplify GitService.status method
560
+ docs: update deployment guide
561
+ test: add chat-service unit tests
562
+ chore: upgrade TypeScript to 5.9.3
563
+ ```
564
+
565
+ ### Branch Names
566
+ Use descriptive kebab-case names:
567
+
568
+ ```
569
+ feature/chat-file-attachments
570
+ fix/websocket-reconnect
571
+ refactor/service-layer-cleanup
572
+ docs/deployment-guide
573
+ ```
574
+