diff options
| author | Clawd <ai@clawd.bot> | 2026-02-28 18:46:11 -0800 |
|---|---|---|
| committer | Clawd <ai@clawd.bot> | 2026-02-28 18:46:11 -0800 |
| commit | 3ac34530578b9a6f59bcea6b5aeefd97eb03d588 (patch) | |
| tree | 099fe4a4788c5dd5997d1f16f5d2db917eda86d0 | |
| parent | de242df9cbe7dfe483f59f9b25e980727baa4c11 (diff) | |
Move artifacts to ~/.claude-flow/ (outside repo)
- Store session artifacts in ~/.claude-flow/projects/{projectId}/sessions/{sessionId}/
- Artifacts no longer live in project directory - can't be accidentally committed
- Remove .claude-flow/ from .gitignore (not needed anymore)
- Update all IPC handlers and renderer to use projectId instead of projectPath
- Update prompts to remove worktree references
- Update README with new storage location
| -rw-r--r-- | .claude-flow/plan.md | 1090 | ||||
| -rw-r--r-- | .claude-flow/research.md | 214 | ||||
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | README.md | 11 | ||||
| -rw-r--r-- | renderer/src/App.tsx | 14 | ||||
| -rw-r--r-- | src/main/claude/index.ts | 65 | ||||
| -rw-r--r-- | src/main/claude/phases.ts | 55 | ||||
| -rw-r--r-- | src/main/ipc/handlers.ts | 34 | ||||
| -rw-r--r-- | src/main/preload.ts | 37 |
9 files changed, 1376 insertions, 145 deletions
diff --git a/.claude-flow/plan.md b/.claude-flow/plan.md new file mode 100644 index 0000000..bbcae6a --- /dev/null +++ b/.claude-flow/plan.md | |||
| @@ -0,0 +1,1090 @@ | |||
| 1 | # Implementation Plan | ||
| 2 | |||
| 3 | ## Goal | ||
| 4 | Restyle the Claude Flow Electron app with: | ||
| 5 | 1. Fix the window/tab title from "minimal" → "Claude Flow" | ||
| 6 | 2. Add a "CLAUDE FLOW" wordmark to the header | ||
| 7 | 3. Apply a cipherpunk aesthetic (full monospace UI font, sharper geometry, electric-blue accent, uppercase meta labels, focus glow) | ||
| 8 | 4. Add light/dark mode with a text toggle (`[dark]` / `[light]`) persisted to `localStorage` | ||
| 9 | |||
| 10 | ## Approach | ||
| 11 | Five files touched in order of dependency. No new npm packages. The CSS rewrite is the biggest change; the JS/TSX changes are small and surgical. | ||
| 12 | |||
| 13 | **Order of changes:** | ||
| 14 | 1. `renderer/index.html` — title fix (trivial, isolated) | ||
| 15 | 2. `renderer/src/styles/globals.css` — full restyle (all visual work) | ||
| 16 | 3. `renderer/src/App.tsx` — add theme state, wire it down | ||
| 17 | 4. `renderer/src/components/Header.tsx` — wordmark + toggle button | ||
| 18 | 5. `renderer/src/components/DocumentPane.tsx` — dynamic CodeMirror theme | ||
| 19 | |||
| 20 | --- | ||
| 21 | |||
| 22 | ## Changes | ||
| 23 | |||
| 24 | ### 1. Fix Window Title | ||
| 25 | **File:** `renderer/index.html` | ||
| 26 | **What:** Change `<title>minimal</title>` to `<title>Claude Flow</title>` on line 10. | ||
| 27 | |||
| 28 | ```html | ||
| 29 | <!-- BEFORE --> | ||
| 30 | <title>minimal</title> | ||
| 31 | |||
| 32 | <!-- AFTER --> | ||
| 33 | <title>Claude Flow</title> | ||
| 34 | ``` | ||
| 35 | |||
| 36 | --- | ||
| 37 | |||
| 38 | ### 2. Restyle the CSS | ||
| 39 | **File:** `renderer/src/styles/globals.css` | ||
| 40 | **What:** Complete replacement of the file. Key changes from the original: | ||
| 41 | - `:root` dark-mode accent updated to `#60a5fa` / `#93c5fd` | ||
| 42 | - `body` font-family changed to full monospace stack | ||
| 43 | - New `html[data-theme="light"]` block added after `:root` | ||
| 44 | - `border-radius` reduced to 2px on all interactive controls (4px on chat bubbles) | ||
| 45 | - `text-transform: uppercase; letter-spacing: 0.07em` added to all meta/chrome labels | ||
| 46 | - Focus glow (`box-shadow`) added on inputs and focused buttons | ||
| 47 | - New `.app-wordmark` class added | ||
| 48 | - New `.theme-toggle` class added | ||
| 49 | - All existing selectors preserved; only values changed | ||
| 50 | |||
| 51 | ```css | ||
| 52 | * { | ||
| 53 | box-sizing: border-box; | ||
| 54 | margin: 0; | ||
| 55 | padding: 0; | ||
| 56 | } | ||
| 57 | |||
| 58 | /* ── Dark theme (default) ────────────────────────────────────── */ | ||
| 59 | :root { | ||
| 60 | --bg-primary: #1a1a1a; | ||
| 61 | --bg-secondary: #252525; | ||
| 62 | --bg-tertiary: #333; | ||
| 63 | --border: #444; | ||
| 64 | --text-primary: #e0e0e0; | ||
| 65 | --text-secondary: #888; | ||
| 66 | --accent: #60a5fa; /* electric blue — brighter than original #3b82f6 */ | ||
| 67 | --accent-hover: #93c5fd; | ||
| 68 | --success: #10b981; | ||
| 69 | --warning: #f59e0b; | ||
| 70 | --danger: #ef4444; | ||
| 71 | } | ||
| 72 | |||
| 73 | /* ── Light theme overrides ───────────────────────────────────── */ | ||
| 74 | html[data-theme="light"] { | ||
| 75 | --bg-primary: #f4f4f2; | ||
| 76 | --bg-secondary: #e8e8e5; | ||
| 77 | --bg-tertiary: #d8d8d4; | ||
| 78 | --border: #b4b4b0; | ||
| 79 | --text-primary: #1a1a18; | ||
| 80 | --text-secondary: #5a5a56; | ||
| 81 | --accent: #2563eb; | ||
| 82 | --accent-hover: #1d4ed8; | ||
| 83 | --success: #059669; | ||
| 84 | --warning: #d97706; | ||
| 85 | --danger: #dc2626; | ||
| 86 | } | ||
| 87 | |||
| 88 | /* ── Base ────────────────────────────────────────────────────── */ | ||
| 89 | body { | ||
| 90 | font-family: "SF Mono", "Cascadia Code", "JetBrains Mono", "Fira Code", | ||
| 91 | Monaco, "Courier New", monospace; | ||
| 92 | background: var(--bg-primary); | ||
| 93 | color: var(--text-primary); | ||
| 94 | overflow: hidden; | ||
| 95 | font-size: 13px; | ||
| 96 | } | ||
| 97 | |||
| 98 | .app { | ||
| 99 | display: flex; | ||
| 100 | flex-direction: column; | ||
| 101 | height: 100vh; | ||
| 102 | } | ||
| 103 | |||
| 104 | /* ── Header ──────────────────────────────────────────────────── */ | ||
| 105 | .header { | ||
| 106 | display: flex; | ||
| 107 | justify-content: space-between; | ||
| 108 | align-items: center; | ||
| 109 | padding: 10px 16px; | ||
| 110 | background: var(--bg-secondary); | ||
| 111 | border-bottom: 1px solid var(--border); | ||
| 112 | -webkit-app-region: drag; | ||
| 113 | } | ||
| 114 | |||
| 115 | .header-left, | ||
| 116 | .header-right { | ||
| 117 | display: flex; | ||
| 118 | align-items: center; | ||
| 119 | gap: 8px; | ||
| 120 | -webkit-app-region: no-drag; | ||
| 121 | } | ||
| 122 | |||
| 123 | /* App wordmark */ | ||
| 124 | .app-wordmark { | ||
| 125 | font-size: 12px; | ||
| 126 | font-weight: 700; | ||
| 127 | letter-spacing: 0.15em; | ||
| 128 | text-transform: uppercase; | ||
| 129 | color: var(--text-primary); | ||
| 130 | padding-right: 12px; | ||
| 131 | border-right: 1px solid var(--border); | ||
| 132 | margin-right: 4px; | ||
| 133 | user-select: none; | ||
| 134 | white-space: nowrap; | ||
| 135 | } | ||
| 136 | |||
| 137 | .header select, | ||
| 138 | .header button { | ||
| 139 | padding: 5px 10px; | ||
| 140 | background: var(--bg-tertiary); | ||
| 141 | border: 1px solid var(--border); | ||
| 142 | border-radius: 2px; | ||
| 143 | color: var(--text-primary); | ||
| 144 | cursor: pointer; | ||
| 145 | font-size: 12px; | ||
| 146 | font-family: inherit; | ||
| 147 | } | ||
| 148 | |||
| 149 | .header button:hover { | ||
| 150 | background: var(--border); | ||
| 151 | } | ||
| 152 | |||
| 153 | .header button.btn-delete { | ||
| 154 | background: transparent; | ||
| 155 | border: 1px solid var(--border); | ||
| 156 | padding: 5px 8px; | ||
| 157 | font-size: 13px; | ||
| 158 | } | ||
| 159 | |||
| 160 | .header button.btn-delete:hover { | ||
| 161 | background: var(--danger); | ||
| 162 | border-color: var(--danger); | ||
| 163 | } | ||
| 164 | |||
| 165 | /* Theme toggle */ | ||
| 166 | .theme-toggle { | ||
| 167 | font-size: 11px; | ||
| 168 | letter-spacing: 0.08em; | ||
| 169 | text-transform: lowercase; | ||
| 170 | opacity: 0.7; | ||
| 171 | transition: opacity 0.15s; | ||
| 172 | } | ||
| 173 | |||
| 174 | .theme-toggle:hover { | ||
| 175 | opacity: 1; | ||
| 176 | background: var(--bg-tertiary) !important; | ||
| 177 | } | ||
| 178 | |||
| 179 | /* Phase indicator */ | ||
| 180 | .phase-indicator { | ||
| 181 | display: flex; | ||
| 182 | gap: 4px; | ||
| 183 | } | ||
| 184 | |||
| 185 | .phase-step { | ||
| 186 | padding: 3px 10px; | ||
| 187 | font-size: 11px; | ||
| 188 | letter-spacing: 0.07em; | ||
| 189 | text-transform: uppercase; | ||
| 190 | border-radius: 2px; | ||
| 191 | background: var(--bg-tertiary); | ||
| 192 | color: var(--text-secondary); | ||
| 193 | } | ||
| 194 | |||
| 195 | .phase-step.active { | ||
| 196 | background: var(--accent); | ||
| 197 | color: white; | ||
| 198 | } | ||
| 199 | |||
| 200 | .phase-step.complete { | ||
| 201 | background: var(--success); | ||
| 202 | color: white; | ||
| 203 | } | ||
| 204 | |||
| 205 | /* ── Main Content ─────────────────────────────────────────────── */ | ||
| 206 | .main-content { | ||
| 207 | flex: 1; | ||
| 208 | display: flex; | ||
| 209 | overflow: hidden; | ||
| 210 | } | ||
| 211 | |||
| 212 | /* ── Document Pane ───────────────────────────────────────────── */ | ||
| 213 | .document-pane { | ||
| 214 | flex: 1; | ||
| 215 | display: flex; | ||
| 216 | flex-direction: column; | ||
| 217 | border-right: 1px solid var(--border); | ||
| 218 | min-width: 0; | ||
| 219 | overflow: hidden; | ||
| 220 | } | ||
| 221 | |||
| 222 | .document-header { | ||
| 223 | display: flex; | ||
| 224 | justify-content: space-between; | ||
| 225 | align-items: center; | ||
| 226 | padding: 7px 16px; | ||
| 227 | background: var(--bg-secondary); | ||
| 228 | border-bottom: 1px solid var(--border); | ||
| 229 | font-size: 11px; | ||
| 230 | letter-spacing: 0.07em; | ||
| 231 | text-transform: uppercase; | ||
| 232 | color: var(--text-secondary); | ||
| 233 | } | ||
| 234 | |||
| 235 | .document-header button { | ||
| 236 | padding: 3px 8px; | ||
| 237 | background: var(--bg-tertiary); | ||
| 238 | border: 1px solid var(--border); | ||
| 239 | border-radius: 2px; | ||
| 240 | color: var(--text-primary); | ||
| 241 | cursor: pointer; | ||
| 242 | font-size: 11px; | ||
| 243 | font-family: inherit; | ||
| 244 | letter-spacing: 0.05em; | ||
| 245 | } | ||
| 246 | |||
| 247 | .document-header button:hover { | ||
| 248 | background: var(--border); | ||
| 249 | } | ||
| 250 | |||
| 251 | .document-content { | ||
| 252 | flex: 1; | ||
| 253 | overflow-y: auto; | ||
| 254 | padding: 24px; | ||
| 255 | } | ||
| 256 | |||
| 257 | .document-content.editing { | ||
| 258 | font-family: inherit; | ||
| 259 | font-size: 13px; | ||
| 260 | line-height: 1.6; | ||
| 261 | background: var(--bg-primary); | ||
| 262 | border: none; | ||
| 263 | resize: none; | ||
| 264 | color: var(--text-primary); | ||
| 265 | } | ||
| 266 | |||
| 267 | .codemirror-editor { | ||
| 268 | flex: 1; | ||
| 269 | overflow: hidden; | ||
| 270 | min-height: 0; | ||
| 271 | } | ||
| 272 | |||
| 273 | .codemirror-editor .cm-editor { | ||
| 274 | height: 100%; | ||
| 275 | max-width: 100%; | ||
| 276 | } | ||
| 277 | |||
| 278 | .codemirror-editor .cm-scroller { | ||
| 279 | overflow: auto !important; | ||
| 280 | } | ||
| 281 | |||
| 282 | .codemirror-editor .cm-gutters { | ||
| 283 | background: var(--bg-secondary); | ||
| 284 | border-right: 1px solid var(--border); | ||
| 285 | } | ||
| 286 | |||
| 287 | .document-content.rendered { | ||
| 288 | line-height: 1.7; | ||
| 289 | } | ||
| 290 | |||
| 291 | .document-content.rendered h1 { | ||
| 292 | font-size: 22px; | ||
| 293 | margin: 24px 0 16px; | ||
| 294 | letter-spacing: -0.01em; | ||
| 295 | } | ||
| 296 | .document-content.rendered h2 { | ||
| 297 | font-size: 17px; | ||
| 298 | margin: 20px 0 12px; | ||
| 299 | color: var(--text-secondary); | ||
| 300 | text-transform: uppercase; | ||
| 301 | letter-spacing: 0.05em; | ||
| 302 | } | ||
| 303 | .document-content.rendered h3 { | ||
| 304 | font-size: 14px; | ||
| 305 | margin: 16px 0 8px; | ||
| 306 | } | ||
| 307 | .document-content.rendered p { | ||
| 308 | margin: 8px 0; | ||
| 309 | line-height: 1.6; | ||
| 310 | } | ||
| 311 | .document-content.rendered code { | ||
| 312 | background: var(--bg-tertiary); | ||
| 313 | padding: 2px 6px; | ||
| 314 | border-radius: 2px; | ||
| 315 | font-size: 12px; | ||
| 316 | font-family: inherit; | ||
| 317 | } | ||
| 318 | .document-content.rendered pre { | ||
| 319 | background: var(--bg-tertiary); | ||
| 320 | padding: 16px; | ||
| 321 | border-radius: 2px; | ||
| 322 | overflow-x: auto; | ||
| 323 | margin: 16px 0; | ||
| 324 | } | ||
| 325 | .document-content.rendered pre code { | ||
| 326 | background: none; | ||
| 327 | padding: 0; | ||
| 328 | } | ||
| 329 | .document-content.rendered ul, | ||
| 330 | .document-content.rendered ol { | ||
| 331 | margin: 12px 0; | ||
| 332 | padding-left: 24px; | ||
| 333 | } | ||
| 334 | .document-content.rendered li { | ||
| 335 | margin-bottom: 6px; | ||
| 336 | line-height: 1.5; | ||
| 337 | } | ||
| 338 | .document-content.rendered ul.contains-task-list { | ||
| 339 | list-style: none; | ||
| 340 | padding-left: 0; | ||
| 341 | } | ||
| 342 | .document-content.rendered li.task-list-item { | ||
| 343 | display: flex; | ||
| 344 | align-items: flex-start; | ||
| 345 | gap: 8px; | ||
| 346 | } | ||
| 347 | .document-content.rendered li.task-list-item input[type="checkbox"] { | ||
| 348 | margin-top: 4px; | ||
| 349 | } | ||
| 350 | .document-content.rendered table { | ||
| 351 | width: 100%; | ||
| 352 | border-collapse: collapse; | ||
| 353 | margin: 16px 0; | ||
| 354 | font-size: 12px; | ||
| 355 | } | ||
| 356 | .document-content.rendered th, | ||
| 357 | .document-content.rendered td { | ||
| 358 | padding: 8px 12px; | ||
| 359 | text-align: left; | ||
| 360 | border: 1px solid var(--border); | ||
| 361 | } | ||
| 362 | .document-content.rendered th { | ||
| 363 | background: var(--bg-tertiary); | ||
| 364 | font-weight: 600; | ||
| 365 | text-transform: uppercase; | ||
| 366 | letter-spacing: 0.06em; | ||
| 367 | font-size: 11px; | ||
| 368 | } | ||
| 369 | .document-content.rendered tr:nth-child(even) td { | ||
| 370 | background: var(--bg-secondary); | ||
| 371 | } | ||
| 372 | .document-content.rendered blockquote { | ||
| 373 | border-left: 3px solid var(--accent); | ||
| 374 | margin: 16px 0; | ||
| 375 | padding-left: 16px; | ||
| 376 | color: var(--text-secondary); | ||
| 377 | } | ||
| 378 | .document-content.rendered hr { | ||
| 379 | border: none; | ||
| 380 | border-top: 1px solid var(--border); | ||
| 381 | margin: 24px 0; | ||
| 382 | } | ||
| 383 | .document-content.rendered a { | ||
| 384 | color: var(--accent); | ||
| 385 | text-decoration: none; | ||
| 386 | } | ||
| 387 | .document-content.rendered a:hover { | ||
| 388 | text-decoration: underline; | ||
| 389 | } | ||
| 390 | .document-content.rendered .empty { | ||
| 391 | color: var(--text-secondary); | ||
| 392 | font-style: italic; | ||
| 393 | } | ||
| 394 | |||
| 395 | .badge { | ||
| 396 | background: var(--accent); | ||
| 397 | color: white; | ||
| 398 | padding: 2px 8px; | ||
| 399 | border-radius: 2px; | ||
| 400 | font-size: 10px; | ||
| 401 | letter-spacing: 0.08em; | ||
| 402 | text-transform: uppercase; | ||
| 403 | } | ||
| 404 | |||
| 405 | /* ── Chat Pane ───────────────────────────────────────────────── */ | ||
| 406 | .chat-pane { | ||
| 407 | width: 380px; | ||
| 408 | display: flex; | ||
| 409 | flex-direction: column; | ||
| 410 | background: var(--bg-secondary); | ||
| 411 | } | ||
| 412 | |||
| 413 | .chat-messages { | ||
| 414 | flex: 1; | ||
| 415 | overflow-y: auto; | ||
| 416 | padding: 16px; | ||
| 417 | } | ||
| 418 | |||
| 419 | .message { | ||
| 420 | margin-bottom: 10px; | ||
| 421 | padding: 9px 13px; | ||
| 422 | border-radius: 4px; | ||
| 423 | max-width: 90%; | ||
| 424 | font-size: 13px; | ||
| 425 | line-height: 1.5; | ||
| 426 | white-space: pre-wrap; | ||
| 427 | } | ||
| 428 | |||
| 429 | .message.user { | ||
| 430 | background: var(--accent); | ||
| 431 | margin-left: auto; | ||
| 432 | color: white; | ||
| 433 | } | ||
| 434 | |||
| 435 | .message.assistant { | ||
| 436 | background: var(--bg-tertiary); | ||
| 437 | } | ||
| 438 | |||
| 439 | .message.loading { | ||
| 440 | color: var(--text-secondary); | ||
| 441 | font-style: italic; | ||
| 442 | } | ||
| 443 | |||
| 444 | .chat-input { | ||
| 445 | display: flex; | ||
| 446 | gap: 8px; | ||
| 447 | padding: 12px; | ||
| 448 | border-top: 1px solid var(--border); | ||
| 449 | } | ||
| 450 | |||
| 451 | .chat-input input { | ||
| 452 | flex: 1; | ||
| 453 | padding: 9px 13px; | ||
| 454 | background: var(--bg-tertiary); | ||
| 455 | border: 1px solid var(--border); | ||
| 456 | border-radius: 2px; | ||
| 457 | color: var(--text-primary); | ||
| 458 | font-size: 13px; | ||
| 459 | font-family: inherit; | ||
| 460 | transition: border-color 0.15s, box-shadow 0.15s; | ||
| 461 | } | ||
| 462 | |||
| 463 | .chat-input input:focus { | ||
| 464 | outline: none; | ||
| 465 | border-color: var(--accent); | ||
| 466 | box-shadow: 0 0 0 2px rgba(96, 165, 250, 0.2); | ||
| 467 | } | ||
| 468 | |||
| 469 | html[data-theme="light"] .chat-input input:focus { | ||
| 470 | box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2); | ||
| 471 | } | ||
| 472 | |||
| 473 | .chat-input button { | ||
| 474 | padding: 9px 15px; | ||
| 475 | background: var(--accent); | ||
| 476 | border: none; | ||
| 477 | border-radius: 2px; | ||
| 478 | color: white; | ||
| 479 | cursor: pointer; | ||
| 480 | font-size: 12px; | ||
| 481 | font-family: inherit; | ||
| 482 | letter-spacing: 0.05em; | ||
| 483 | text-transform: uppercase; | ||
| 484 | transition: background 0.15s; | ||
| 485 | } | ||
| 486 | |||
| 487 | .chat-input button:hover:not(:disabled) { | ||
| 488 | background: var(--accent-hover); | ||
| 489 | } | ||
| 490 | |||
| 491 | .chat-input button:disabled { | ||
| 492 | opacity: 0.4; | ||
| 493 | cursor: not-allowed; | ||
| 494 | } | ||
| 495 | |||
| 496 | /* ── Action Bar ──────────────────────────────────────────────── */ | ||
| 497 | .action-bar { | ||
| 498 | display: flex; | ||
| 499 | justify-content: space-between; | ||
| 500 | align-items: center; | ||
| 501 | padding: 10px 16px; | ||
| 502 | background: var(--bg-secondary); | ||
| 503 | border-top: 1px solid var(--border); | ||
| 504 | } | ||
| 505 | |||
| 506 | .action-bar-left, | ||
| 507 | .action-bar-right { | ||
| 508 | display: flex; | ||
| 509 | align-items: center; | ||
| 510 | gap: 16px; | ||
| 511 | } | ||
| 512 | |||
| 513 | .token-indicator { | ||
| 514 | display: flex; | ||
| 515 | align-items: center; | ||
| 516 | gap: 8px; | ||
| 517 | } | ||
| 518 | |||
| 519 | .token-bar { | ||
| 520 | width: 100px; | ||
| 521 | height: 4px; | ||
| 522 | background: var(--bg-tertiary); | ||
| 523 | border-radius: 1px; | ||
| 524 | overflow: hidden; | ||
| 525 | } | ||
| 526 | |||
| 527 | .token-fill { | ||
| 528 | height: 100%; | ||
| 529 | transition: width 0.3s ease; | ||
| 530 | } | ||
| 531 | |||
| 532 | .token-label { | ||
| 533 | font-size: 10px; | ||
| 534 | letter-spacing: 0.08em; | ||
| 535 | text-transform: uppercase; | ||
| 536 | color: var(--text-secondary); | ||
| 537 | } | ||
| 538 | |||
| 539 | .permission-toggle { | ||
| 540 | display: flex; | ||
| 541 | align-items: center; | ||
| 542 | gap: 6px; | ||
| 543 | font-size: 11px; | ||
| 544 | letter-spacing: 0.05em; | ||
| 545 | text-transform: uppercase; | ||
| 546 | color: var(--text-secondary); | ||
| 547 | cursor: pointer; | ||
| 548 | } | ||
| 549 | |||
| 550 | .permission-toggle input { | ||
| 551 | cursor: pointer; | ||
| 552 | } | ||
| 553 | |||
| 554 | .btn-secondary { | ||
| 555 | padding: 6px 14px; | ||
| 556 | background: var(--bg-tertiary); | ||
| 557 | border: 1px solid var(--border); | ||
| 558 | border-radius: 2px; | ||
| 559 | color: var(--text-primary); | ||
| 560 | cursor: pointer; | ||
| 561 | font-size: 12px; | ||
| 562 | font-family: inherit; | ||
| 563 | letter-spacing: 0.05em; | ||
| 564 | transition: background 0.15s; | ||
| 565 | } | ||
| 566 | |||
| 567 | .btn-secondary:hover:not(:disabled) { | ||
| 568 | background: var(--border); | ||
| 569 | } | ||
| 570 | |||
| 571 | .btn-secondary:disabled { | ||
| 572 | opacity: 0.4; | ||
| 573 | cursor: not-allowed; | ||
| 574 | } | ||
| 575 | |||
| 576 | .btn-primary { | ||
| 577 | padding: 6px 18px; | ||
| 578 | background: var(--accent); | ||
| 579 | border: none; | ||
| 580 | border-radius: 2px; | ||
| 581 | color: white; | ||
| 582 | cursor: pointer; | ||
| 583 | font-weight: 600; | ||
| 584 | font-size: 12px; | ||
| 585 | font-family: inherit; | ||
| 586 | letter-spacing: 0.07em; | ||
| 587 | text-transform: uppercase; | ||
| 588 | transition: background 0.15s; | ||
| 589 | } | ||
| 590 | |||
| 591 | .btn-primary:hover:not(:disabled) { | ||
| 592 | background: var(--accent-hover); | ||
| 593 | } | ||
| 594 | |||
| 595 | .btn-primary:disabled { | ||
| 596 | opacity: 0.4; | ||
| 597 | cursor: not-allowed; | ||
| 598 | } | ||
| 599 | |||
| 600 | .implementing-status { | ||
| 601 | color: var(--success); | ||
| 602 | font-size: 11px; | ||
| 603 | letter-spacing: 0.1em; | ||
| 604 | text-transform: uppercase; | ||
| 605 | } | ||
| 606 | |||
| 607 | /* ── Error Bar ───────────────────────────────────────────────── */ | ||
| 608 | .error-bar { | ||
| 609 | display: flex; | ||
| 610 | justify-content: space-between; | ||
| 611 | align-items: center; | ||
| 612 | padding: 8px 16px; | ||
| 613 | background: var(--danger); | ||
| 614 | color: white; | ||
| 615 | font-size: 12px; | ||
| 616 | letter-spacing: 0.03em; | ||
| 617 | } | ||
| 618 | |||
| 619 | .error-bar button { | ||
| 620 | background: none; | ||
| 621 | border: none; | ||
| 622 | color: white; | ||
| 623 | font-size: 16px; | ||
| 624 | cursor: pointer; | ||
| 625 | padding: 0 4px; | ||
| 626 | } | ||
| 627 | |||
| 628 | .error-bar button:hover { | ||
| 629 | opacity: 0.8; | ||
| 630 | } | ||
| 631 | |||
| 632 | /* ── Onboarding ──────────────────────────────────────────────── */ | ||
| 633 | .onboarding h1 { | ||
| 634 | font-size: 24px; | ||
| 635 | margin-bottom: 8px; | ||
| 636 | letter-spacing: 0.05em; | ||
| 637 | text-transform: uppercase; | ||
| 638 | } | ||
| 639 | |||
| 640 | .onboarding h2 { | ||
| 641 | font-size: 13px; | ||
| 642 | margin-top: 28px; | ||
| 643 | margin-bottom: 12px; | ||
| 644 | color: var(--accent); | ||
| 645 | text-transform: uppercase; | ||
| 646 | letter-spacing: 0.1em; | ||
| 647 | } | ||
| 648 | |||
| 649 | .onboarding p { | ||
| 650 | margin: 12px 0; | ||
| 651 | line-height: 1.6; | ||
| 652 | } | ||
| 653 | |||
| 654 | .onboarding ol { | ||
| 655 | margin: 12px 0 12px 24px; | ||
| 656 | } | ||
| 657 | |||
| 658 | .onboarding li { | ||
| 659 | margin: 8px 0; | ||
| 660 | line-height: 1.5; | ||
| 661 | } | ||
| 662 | |||
| 663 | .onboarding pre { | ||
| 664 | background: var(--bg-tertiary); | ||
| 665 | padding: 12px 16px; | ||
| 666 | border-radius: 2px; | ||
| 667 | margin: 12px 0; | ||
| 668 | } | ||
| 669 | |||
| 670 | .onboarding code { | ||
| 671 | font-family: inherit; | ||
| 672 | font-size: 12px; | ||
| 673 | } | ||
| 674 | |||
| 675 | .onboarding a { | ||
| 676 | color: var(--accent); | ||
| 677 | text-decoration: none; | ||
| 678 | } | ||
| 679 | |||
| 680 | .onboarding a:hover { | ||
| 681 | text-decoration: underline; | ||
| 682 | } | ||
| 683 | |||
| 684 | .onboarding-tip { | ||
| 685 | margin-top: 28px; | ||
| 686 | padding: 16px; | ||
| 687 | background: var(--bg-tertiary); | ||
| 688 | border-left: 3px solid var(--accent); | ||
| 689 | border-radius: 0 2px 2px 0; | ||
| 690 | } | ||
| 691 | ``` | ||
| 692 | |||
| 693 | --- | ||
| 694 | |||
| 695 | ### 3. Add Theme State to App | ||
| 696 | **File:** `renderer/src/App.tsx` | ||
| 697 | **What:** Three additions to the existing file — no existing logic is touched. | ||
| 698 | |||
| 699 | **3a. Add `Theme` type and `theme` state** — insert after the existing imports, before the `App` function: | ||
| 700 | |||
| 701 | ```typescript | ||
| 702 | // Add this type alias near the top, alongside other local types if any, | ||
| 703 | // or just before the App function: | ||
| 704 | type Theme = "dark" | "light"; | ||
| 705 | ``` | ||
| 706 | |||
| 707 | **3b. Add `theme` state and side-effects** — insert inside `App()`, after the existing `useState` declarations (after `const [error, setError] = useState<string | null>(null)`): | ||
| 708 | |||
| 709 | ```typescript | ||
| 710 | const [theme, setTheme] = useState<Theme>( | ||
| 711 | () => (localStorage.getItem("cf-theme") as Theme) ?? "dark" | ||
| 712 | ); | ||
| 713 | |||
| 714 | // Keep document.documentElement in sync, and persist to localStorage | ||
| 715 | useEffect(() => { | ||
| 716 | document.documentElement.setAttribute("data-theme", theme); | ||
| 717 | localStorage.setItem("cf-theme", theme); | ||
| 718 | }, [theme]); | ||
| 719 | |||
| 720 | const handleToggleTheme = () => | ||
| 721 | setTheme((t) => (t === "dark" ? "light" : "dark")); | ||
| 722 | ``` | ||
| 723 | |||
| 724 | **3c. Pass new props to `Header` and `DocumentPane`** — update the JSX inside the `return`: | ||
| 725 | |||
| 726 | ```tsx | ||
| 727 | // Header — add two new props: | ||
| 728 | <Header | ||
| 729 | projects={projects} | ||
| 730 | sessions={sessions} | ||
| 731 | selectedProject={selectedProject} | ||
| 732 | selectedSession={selectedSession} | ||
| 733 | onSelectProject={setSelectedProject} | ||
| 734 | onSelectSession={setSelectedSession} | ||
| 735 | onCreateProject={handleCreateProject} | ||
| 736 | onCreateSession={handleCreateSession} | ||
| 737 | onDeleteProject={handleDeleteProject} | ||
| 738 | onDeleteSession={handleDeleteSession} | ||
| 739 | theme={theme} | ||
| 740 | onToggleTheme={handleToggleTheme} | ||
| 741 | /> | ||
| 742 | |||
| 743 | // DocumentPane — add one new prop: | ||
| 744 | <DocumentPane | ||
| 745 | content={documentContent} | ||
| 746 | onChange={setDocumentContent} | ||
| 747 | phase={selectedSession?.phase || "research"} | ||
| 748 | disabled={!selectedSession || selectedSession.phase === "implement"} | ||
| 749 | showOnboarding={!selectedProject} | ||
| 750 | theme={theme} | ||
| 751 | /> | ||
| 752 | ``` | ||
| 753 | |||
| 754 | --- | ||
| 755 | |||
| 756 | ### 4. Update Header Component | ||
| 757 | **File:** `renderer/src/components/Header.tsx` | ||
| 758 | **What:** Add `theme` and `onToggleTheme` to the props interface, insert wordmark element, add toggle button. | ||
| 759 | |||
| 760 | **Complete updated file:** | ||
| 761 | |||
| 762 | ```tsx | ||
| 763 | import React from "react"; | ||
| 764 | import type { Project, Session, Phase } from "../types"; | ||
| 765 | |||
| 766 | type Theme = "dark" | "light"; | ||
| 767 | |||
| 768 | interface HeaderProps { | ||
| 769 | projects: Project[]; | ||
| 770 | sessions: Session[]; | ||
| 771 | selectedProject: Project | null; | ||
| 772 | selectedSession: Session | null; | ||
| 773 | onSelectProject: (project: Project | null) => void; | ||
| 774 | onSelectSession: (session: Session | null) => void; | ||
| 775 | onCreateProject: () => void; | ||
| 776 | onCreateSession: () => void; | ||
| 777 | onDeleteProject?: (id: string) => void; | ||
| 778 | onDeleteSession?: (id: string) => void; | ||
| 779 | theme: Theme; | ||
| 780 | onToggleTheme: () => void; | ||
| 781 | } | ||
| 782 | |||
| 783 | const phaseLabels: Record<Phase, string> = { | ||
| 784 | research: "Research", | ||
| 785 | plan: "Plan", | ||
| 786 | implement: "Implement", | ||
| 787 | }; | ||
| 788 | |||
| 789 | const phases: Phase[] = ["research", "plan", "implement"]; | ||
| 790 | |||
| 791 | export function Header({ | ||
| 792 | projects, | ||
| 793 | sessions, | ||
| 794 | selectedProject, | ||
| 795 | selectedSession, | ||
| 796 | onSelectProject, | ||
| 797 | onSelectSession, | ||
| 798 | onCreateProject, | ||
| 799 | onCreateSession, | ||
| 800 | onDeleteProject, | ||
| 801 | onDeleteSession, | ||
| 802 | theme, | ||
| 803 | onToggleTheme, | ||
| 804 | }: HeaderProps) { | ||
| 805 | const handleDeleteProject = () => { | ||
| 806 | if (!selectedProject || !onDeleteProject) return; | ||
| 807 | if (confirm(`Delete project "${selectedProject.name}"? This cannot be undone.`)) { | ||
| 808 | onDeleteProject(selectedProject.id); | ||
| 809 | } | ||
| 810 | }; | ||
| 811 | |||
| 812 | const handleDeleteSession = () => { | ||
| 813 | if (!selectedSession || !onDeleteSession) return; | ||
| 814 | if (confirm(`Delete session "${selectedSession.name}"? This cannot be undone.`)) { | ||
| 815 | onDeleteSession(selectedSession.id); | ||
| 816 | } | ||
| 817 | }; | ||
| 818 | |||
| 819 | return ( | ||
| 820 | <header className="header"> | ||
| 821 | <div className="header-left"> | ||
| 822 | {/* ── Wordmark ── */} | ||
| 823 | <span className="app-wordmark">Claude Flow</span> | ||
| 824 | |||
| 825 | <select | ||
| 826 | value={selectedProject?.id || ""} | ||
| 827 | onChange={(e) => { | ||
| 828 | const project = projects.find((p) => p.id === e.target.value); | ||
| 829 | onSelectProject(project || null); | ||
| 830 | onSelectSession(null); | ||
| 831 | }} | ||
| 832 | > | ||
| 833 | <option value="">Select Project...</option> | ||
| 834 | {projects.map((p) => ( | ||
| 835 | <option key={p.id} value={p.id}> | ||
| 836 | {p.name} | ||
| 837 | </option> | ||
| 838 | ))} | ||
| 839 | </select> | ||
| 840 | <button onClick={onCreateProject}>+ Project</button> | ||
| 841 | {selectedProject && onDeleteProject && ( | ||
| 842 | <button | ||
| 843 | onClick={handleDeleteProject} | ||
| 844 | className="btn-delete" | ||
| 845 | title="Delete project" | ||
| 846 | > | ||
| 847 | 🗑️ | ||
| 848 | </button> | ||
| 849 | )} | ||
| 850 | |||
| 851 | {selectedProject && ( | ||
| 852 | <> | ||
| 853 | <select | ||
| 854 | value={selectedSession?.id || ""} | ||
| 855 | onChange={(e) => { | ||
| 856 | const session = sessions.find((s) => s.id === e.target.value); | ||
| 857 | onSelectSession(session || null); | ||
| 858 | }} | ||
| 859 | > | ||
| 860 | <option value="">Select Session...</option> | ||
| 861 | {sessions.map((s) => ( | ||
| 862 | <option key={s.id} value={s.id}> | ||
| 863 | {s.name} | ||
| 864 | </option> | ||
| 865 | ))} | ||
| 866 | </select> | ||
| 867 | <button onClick={onCreateSession}>+ Session</button> | ||
| 868 | {selectedSession && onDeleteSession && ( | ||
| 869 | <button | ||
| 870 | onClick={handleDeleteSession} | ||
| 871 | className="btn-delete" | ||
| 872 | title="Delete session" | ||
| 873 | > | ||
| 874 | 🗑️ | ||
| 875 | </button> | ||
| 876 | )} | ||
| 877 | </> | ||
| 878 | )} | ||
| 879 | </div> | ||
| 880 | |||
| 881 | <div className="header-right"> | ||
| 882 | {selectedSession && ( | ||
| 883 | <div className="phase-indicator"> | ||
| 884 | {phases.map((phase) => { | ||
| 885 | const phaseIndex = phases.indexOf(phase); | ||
| 886 | const currentIndex = phases.indexOf(selectedSession.phase); | ||
| 887 | const isComplete = phaseIndex < currentIndex; | ||
| 888 | const isActive = phase === selectedSession.phase; | ||
| 889 | |||
| 890 | return ( | ||
| 891 | <span | ||
| 892 | key={phase} | ||
| 893 | className={`phase-step ${isActive ? "active" : ""} ${ | ||
| 894 | isComplete ? "complete" : "" | ||
| 895 | }`} | ||
| 896 | > | ||
| 897 | {phaseLabels[phase]} | ||
| 898 | </span> | ||
| 899 | ); | ||
| 900 | })} | ||
| 901 | </div> | ||
| 902 | )} | ||
| 903 | |||
| 904 | {/* ── Theme toggle ── */} | ||
| 905 | <button className="theme-toggle" onClick={onToggleTheme}> | ||
| 906 | {theme === "dark" ? "[light]" : "[dark]"} | ||
| 907 | </button> | ||
| 908 | </div> | ||
| 909 | </header> | ||
| 910 | ); | ||
| 911 | } | ||
| 912 | ``` | ||
| 913 | |||
| 914 | --- | ||
| 915 | |||
| 916 | ### 5. Dynamic CodeMirror Theme in DocumentPane | ||
| 917 | **File:** `renderer/src/components/DocumentPane.tsx` | ||
| 918 | **What:** Three surgical changes to `MarkdownEditor` — add `theme` prop, update imports, update `useEffect`. | ||
| 919 | |||
| 920 | **5a. Update imports** — add `syntaxHighlighting` and `defaultHighlightStyle` to the `@codemirror/language` import: | ||
| 921 | |||
| 922 | ```typescript | ||
| 923 | // BEFORE: | ||
| 924 | import { markdown } from "@codemirror/lang-markdown"; | ||
| 925 | import { languages } from "@codemirror/language-data"; | ||
| 926 | |||
| 927 | // AFTER: | ||
| 928 | import { markdown } from "@codemirror/lang-markdown"; | ||
| 929 | import { languages } from "@codemirror/language-data"; | ||
| 930 | import { syntaxHighlighting, defaultHighlightStyle } from "@codemirror/language"; | ||
| 931 | ``` | ||
| 932 | |||
| 933 | **5b. Update `MarkdownEditor` props interface** — add `theme`: | ||
| 934 | |||
| 935 | ```typescript | ||
| 936 | // BEFORE: | ||
| 937 | function MarkdownEditor({ | ||
| 938 | content, | ||
| 939 | onChange, | ||
| 940 | disabled, | ||
| 941 | }: { | ||
| 942 | content: string; | ||
| 943 | onChange: (content: string) => void; | ||
| 944 | disabled: boolean; | ||
| 945 | }) | ||
| 946 | |||
| 947 | // AFTER: | ||
| 948 | function MarkdownEditor({ | ||
| 949 | content, | ||
| 950 | onChange, | ||
| 951 | disabled, | ||
| 952 | theme, | ||
| 953 | }: { | ||
| 954 | content: string; | ||
| 955 | onChange: (content: string) => void; | ||
| 956 | disabled: boolean; | ||
| 957 | theme: "dark" | "light"; | ||
| 958 | }) | ||
| 959 | ``` | ||
| 960 | |||
| 961 | **5c. Update the `useEffect` inside `MarkdownEditor`** — swap the theme extension and add `theme` to the dependency array: | ||
| 962 | |||
| 963 | ```typescript | ||
| 964 | // BEFORE (inside useEffect): | ||
| 965 | extensions: [ | ||
| 966 | lineNumbers(), | ||
| 967 | highlightActiveLine(), | ||
| 968 | drawSelection(), | ||
| 969 | history(), | ||
| 970 | keymap.of([...defaultKeymap, ...historyKeymap]), | ||
| 971 | markdown({ codeLanguages: languages }), | ||
| 972 | oneDark, // ← hardcoded | ||
| 973 | updateListener, | ||
| 974 | EditorView.editable.of(!disabled), | ||
| 975 | EditorView.lineWrapping, | ||
| 976 | EditorView.theme({ ... }), | ||
| 977 | ], | ||
| 978 | // dependency array: | ||
| 979 | }, [disabled]); | ||
| 980 | |||
| 981 | // AFTER: | ||
| 982 | extensions: [ | ||
| 983 | lineNumbers(), | ||
| 984 | highlightActiveLine(), | ||
| 985 | drawSelection(), | ||
| 986 | history(), | ||
| 987 | keymap.of([...defaultKeymap, ...historyKeymap]), | ||
| 988 | markdown({ codeLanguages: languages }), | ||
| 989 | theme === "dark" ? oneDark : syntaxHighlighting(defaultHighlightStyle), // ← dynamic | ||
| 990 | updateListener, | ||
| 991 | EditorView.editable.of(!disabled), | ||
| 992 | EditorView.lineWrapping, | ||
| 993 | EditorView.theme({ ... }), | ||
| 994 | ], | ||
| 995 | // dependency array: | ||
| 996 | }, [disabled, theme]); // ← theme added | ||
| 997 | ``` | ||
| 998 | |||
| 999 | **5d. Update `DocumentPaneProps` interface and pass `theme` through** — add to the interface and forward to `MarkdownEditor`: | ||
| 1000 | |||
| 1001 | ```typescript | ||
| 1002 | // BEFORE: | ||
| 1003 | interface DocumentPaneProps { | ||
| 1004 | content: string; | ||
| 1005 | onChange: (content: string) => void; | ||
| 1006 | phase: Phase; | ||
| 1007 | disabled: boolean; | ||
| 1008 | showOnboarding?: boolean; | ||
| 1009 | } | ||
| 1010 | |||
| 1011 | // AFTER: | ||
| 1012 | interface DocumentPaneProps { | ||
| 1013 | content: string; | ||
| 1014 | onChange: (content: string) => void; | ||
| 1015 | phase: Phase; | ||
| 1016 | disabled: boolean; | ||
| 1017 | showOnboarding?: boolean; | ||
| 1018 | theme: "dark" | "light"; | ||
| 1019 | } | ||
| 1020 | ``` | ||
| 1021 | |||
| 1022 | And in the `DocumentPane` function body, destructure `theme` and pass it to `MarkdownEditor`: | ||
| 1023 | |||
| 1024 | ```tsx | ||
| 1025 | // BEFORE: | ||
| 1026 | export function DocumentPane({ | ||
| 1027 | content, | ||
| 1028 | onChange, | ||
| 1029 | phase, | ||
| 1030 | disabled, | ||
| 1031 | showOnboarding, | ||
| 1032 | }: DocumentPaneProps) | ||
| 1033 | |||
| 1034 | // AFTER: | ||
| 1035 | export function DocumentPane({ | ||
| 1036 | content, | ||
| 1037 | onChange, | ||
| 1038 | phase, | ||
| 1039 | disabled, | ||
| 1040 | showOnboarding, | ||
| 1041 | theme, | ||
| 1042 | }: DocumentPaneProps) | ||
| 1043 | ``` | ||
| 1044 | |||
| 1045 | ```tsx | ||
| 1046 | // BEFORE (in JSX): | ||
| 1047 | <MarkdownEditor | ||
| 1048 | content={content} | ||
| 1049 | onChange={onChange} | ||
| 1050 | disabled={disabled} | ||
| 1051 | /> | ||
| 1052 | |||
| 1053 | // AFTER: | ||
| 1054 | <MarkdownEditor | ||
| 1055 | content={content} | ||
| 1056 | onChange={onChange} | ||
| 1057 | disabled={disabled} | ||
| 1058 | theme={theme} | ||
| 1059 | /> | ||
| 1060 | ``` | ||
| 1061 | |||
| 1062 | --- | ||
| 1063 | |||
| 1064 | ## TODO | ||
| 1065 | - [x] **1.** `renderer/index.html` — change `<title>minimal</title>` to `<title>Claude Flow</title>` | ||
| 1066 | - [x] **2.** `renderer/src/styles/globals.css` — full replacement with new CSS (monospace body, electric accent, 2px radii, uppercase labels, light theme block, focus glow, `.app-wordmark`, `.theme-toggle`) | ||
| 1067 | - [x] **3a.** `renderer/src/App.tsx` — add `type Theme` alias | ||
| 1068 | - [x] **3b.** `renderer/src/App.tsx` — add `theme` state + `useEffect` + `handleToggleTheme` | ||
| 1069 | - [x] **3c.** `renderer/src/App.tsx` — pass `theme`/`onToggleTheme` to `Header`, pass `theme` to `DocumentPane` | ||
| 1070 | - [x] **4.** `renderer/src/components/Header.tsx` — full replacement (new props, wordmark, toggle button) | ||
| 1071 | - [x] **5a.** `renderer/src/components/DocumentPane.tsx` — add `syntaxHighlighting, defaultHighlightStyle` import | ||
| 1072 | - [x] **5b.** `renderer/src/components/DocumentPane.tsx` — add `theme` to `MarkdownEditor` props | ||
| 1073 | - [x] **5c.** `renderer/src/components/DocumentPane.tsx` — dynamic theme extension + `theme` in dep array | ||
| 1074 | - [x] **5d.** `renderer/src/components/DocumentPane.tsx` — add `theme` to `DocumentPaneProps`, destructure, forward to `MarkdownEditor` | ||
| 1075 | |||
| 1076 | --- | ||
| 1077 | |||
| 1078 | ## Risks / Considerations | ||
| 1079 | |||
| 1080 | **CodeMirror reinitialization on theme switch**: Adding `theme` to the `useEffect` dependency array means the entire editor is torn down and recreated when the theme changes. This is intentional and correct — CodeMirror extensions are baked into the `EditorState` at creation time and can't be hot-swapped. The existing `content` sync `useEffect` will immediately restore the document contents after reinit, so no text loss occurs. There is a brief visual flash on theme toggle; this is acceptable. | ||
| 1081 | |||
| 1082 | **`data-theme` initial state and flash of wrong theme**: The `useState` initializer reads `localStorage` synchronously, and the `useEffect` sets `data-theme` on first render. Because this happens before paint (Electron loads the renderer in a hidden window and only shows it after load), there should be no flash of the wrong theme in production. In dev the Vite HMR setup may briefly show unstyled content; this is not a concern for shipping. | ||
| 1083 | |||
| 1084 | **Monospace font rendering differences per OS**: "SF Mono" ships with macOS/Xcode; "Cascadia Code" ships with Windows Terminal; "JetBrains Mono" and "Fira Code" are user-installed. The fallback chain is safe — `Monaco` is widely available on macOS, and `"Courier New", monospace` are universal final fallbacks. No font files are bundled; this is system-font-only. | ||
| 1085 | |||
| 1086 | **`Theme` type duplication**: The `type Theme = "dark" | "light"` alias is defined in both `App.tsx` and `Header.tsx`. This is a minor smell. If it grows to more places, move it into `renderer/src/types.ts`. For this task (2 files) the duplication is acceptable. | ||
| 1087 | |||
| 1088 | **Light mode CodeMirror gutter background**: The `.codemirror-editor .cm-gutters` rule uses `var(--bg-secondary)` which will automatically pick up the light-mode value — no extra change needed there. | ||
| 1089 | |||
| 1090 | **`select` element styling in light mode**: Native `<select>` elements on macOS render with a system style that may look slightly inconsistent in light mode. This is an existing limitation of the app's native select usage and is out of scope for this task. | ||
diff --git a/.claude-flow/research.md b/.claude-flow/research.md new file mode 100644 index 0000000..30c4832 --- /dev/null +++ b/.claude-flow/research.md | |||
| @@ -0,0 +1,214 @@ | |||
| 1 | # Research Findings | ||
| 2 | |||
| 3 | ## Overview | ||
| 4 | |||
| 5 | This is an Electron + React + TypeScript app called "Claude Flow" that provides a structured AI-assisted coding workflow (Research → Plan → Implement). The styling is managed through a single monolithic CSS file using CSS custom properties. The current window title is "minimal" (a leftover from the starter template). There is no light/dark mode — only dark. The goal is to: rename the title, add a header wordmark, modernize the UI with a cipherpunk aesthetic (full monospace font, sharper borders, electric-blue accent), and add a text-based light/dark mode toggle. | ||
| 6 | |||
| 7 | --- | ||
| 8 | |||
| 9 | ## Architecture | ||
| 10 | |||
| 11 | ### Tech Stack | ||
| 12 | - **Electron 38** — main process, window chrome, IPC | ||
| 13 | - **React 19 + Vite** — renderer process | ||
| 14 | - **TypeScript** throughout | ||
| 15 | - **CodeMirror 6** — markdown editor in DocumentPane | ||
| 16 | - **better-sqlite3** — local DB for projects/sessions/messages | ||
| 17 | |||
| 18 | ### Renderer Structure | ||
| 19 | ``` | ||
| 20 | renderer/ | ||
| 21 | index.html ← <title>minimal</title> — MUST CHANGE to "Claude Flow" | ||
| 22 | src/ | ||
| 23 | main.tsx ← React root mount | ||
| 24 | App.tsx ← Top-level state, layout | ||
| 25 | types.ts ← Shared TS types | ||
| 26 | styles/globals.css ← ALL styling lives here (CSS custom properties) | ||
| 27 | components/ | ||
| 28 | Header.tsx ← Top bar: project/session selectors, phase indicator | ||
| 29 | DocumentPane.tsx ← Left pane: CodeMirror editor + ReactMarkdown preview | ||
| 30 | ChatPane.tsx ← Right pane: message list + text input | ||
| 31 | ActionBar.tsx ← Bottom bar: token usage bar, Review/Submit buttons | ||
| 32 | ``` | ||
| 33 | |||
| 34 | ### Main Process Structure | ||
| 35 | ``` | ||
| 36 | src/main/ | ||
| 37 | index.ts ← BrowserWindow creation (titleBarStyle: "hiddenInset"), IPC setup | ||
| 38 | preload.ts ← Exposes window.api bridge | ||
| 39 | ipc/handlers.ts | ||
| 40 | db/ ← SQLite schema/queries | ||
| 41 | claude/ ← Claude SDK integration | ||
| 42 | ``` | ||
| 43 | |||
| 44 | --- | ||
| 45 | |||
| 46 | ## Relevant Files | ||
| 47 | |||
| 48 | | File | Relevance to Task | | ||
| 49 | |------|-------------------| | ||
| 50 | | `renderer/index.html` | **Line 10**: `<title>minimal</title>` → change to `Claude Flow` | | ||
| 51 | | `renderer/src/styles/globals.css` | **All CSS** — CSS custom properties `:root {}` define the entire theme. Single file, ~543 lines | | ||
| 52 | | `renderer/src/components/Header.tsx` | Add app wordmark + text theme toggle button (header-right div) | | ||
| 53 | | `renderer/src/App.tsx` | Add theme state, persist to localStorage, set `data-theme` on `document.documentElement`, pass props down | | ||
| 54 | | `renderer/src/components/DocumentPane.tsx` | Make CodeMirror theme dynamic — swap `oneDark` ↔ `syntaxHighlighting(defaultHighlightStyle)` | | ||
| 55 | | `src/main/index.ts` | `titleBarStyle: "hiddenInset"` — macOS frameless, HTML title used in Dock/app switcher | | ||
| 56 | |||
| 57 | --- | ||
| 58 | |||
| 59 | ## Key Insights | ||
| 60 | |||
| 61 | ### 1. Title Change — Two Locations | ||
| 62 | The title "minimal" appears in: | ||
| 63 | - `renderer/index.html` line 10: `<title>minimal</title>` — the browser/OS-level window title | ||
| 64 | - `package.json` line 1: `"name": "minimal-electron-vite-react-better-sqlite"` — package name (lower-priority) | ||
| 65 | - `package.json` line 19: `"appId": "com.example.minimalbsqlite"` — build identifier (lower-priority) | ||
| 66 | |||
| 67 | **Primary fix**: change `<title>minimal</title>` in `renderer/index.html`. | ||
| 68 | |||
| 69 | ### 2. Current Color Palette (Dark Only) | ||
| 70 | ```css | ||
| 71 | :root { | ||
| 72 | --bg-primary: #1a1a1a /* near-black background */ | ||
| 73 | --bg-secondary: #252525 /* panels, header */ | ||
| 74 | --bg-tertiary: #333 /* inputs, code blocks */ | ||
| 75 | --border: #444 /* dividers */ | ||
| 76 | --text-primary: #e0e0e0 /* main text */ | ||
| 77 | --text-secondary:#888 /* muted text, labels */ | ||
| 78 | --accent: #3b82f6 /* blue — buttons, links */ | ||
| 79 | --accent-hover: #2563eb | ||
| 80 | --success: #10b981 /* green */ | ||
| 81 | --warning: #f59e0b /* amber */ | ||
| 82 | --danger: #ef4444 /* red */ | ||
| 83 | } | ||
| 84 | ``` | ||
| 85 | No light mode variables exist. The mechanism for light/dark is straightforward: add `html[data-theme="light"]` overrides. | ||
| 86 | |||
| 87 | ### 3. Light/Dark Mode — Implementation Path | ||
| 88 | **Theme storage**: `localStorage` — persist across sessions | ||
| 89 | **Toggle mechanism**: Set `data-theme="light"` or `data-theme="dark"` on `document.documentElement` | ||
| 90 | **CSS**: Override variables under `html[data-theme="light"]` selector | ||
| 91 | **State**: `useState` + `useEffect` in `App.tsx` — no Context needed, just pass `theme` + `onToggleTheme` as props | ||
| 92 | **Toggle style**: Text-only, bracket notation: `[dark]` / `[light]` — fits the cipherpunk chrome feel | ||
| 93 | |||
| 94 | The `Header` will receive `theme: "dark" | "light"` and `onToggleTheme: () => void` as new props. | ||
| 95 | |||
| 96 | **CodeMirror — RESOLVED (no new packages needed)**: | ||
| 97 | `@codemirror/language` is already installed as a transitive dependency and exports both `defaultHighlightStyle` and `syntaxHighlighting`. The `defaultHighlightStyle` provides a full light-mode syntax highlighting scheme (keywords in purple, strings in red, literals in green, etc.) — identical coverage to `oneDark`, just different colors. | ||
| 98 | |||
| 99 | Strategy: | ||
| 100 | - **Dark** → use `[oneDark]` as today | ||
| 101 | - **Light** → use `[syntaxHighlighting(defaultHighlightStyle)]` from `@codemirror/language` | ||
| 102 | |||
| 103 | `DocumentPane` will receive a `theme` prop. The CodeMirror `useEffect` dependency array will include `theme` (alongside the existing `disabled`) so the editor reinitializes with the correct extension set when the theme changes. | ||
| 104 | |||
| 105 | ### 4. Accent Color — RESOLVED | ||
| 106 | |||
| 107 | **Keep blue, go more electric. No green/cyan.** | ||
| 108 | |||
| 109 | The cipherpunk feel will come from sharpness and monospace typography — not a color gimmick. | ||
| 110 | |||
| 111 | Proposed accent colors: | ||
| 112 | ``` | ||
| 113 | Dark mode: #60a5fa (bright electric blue — more vivid than current #3b82f6) | ||
| 114 | hover: #93c5fd | ||
| 115 | Light mode: #2563eb (deeper blue — maintains contrast on light backgrounds) | ||
| 116 | hover: #1d4ed8 | ||
| 117 | ``` | ||
| 118 | |||
| 119 | ### 5. Font — RESOLVED: Full Monospace | ||
| 120 | |||
| 121 | Apply a monospace font stack to `body` — the entire UI goes mono. The codebase already uses this stack for code blocks, so extending it to the whole UI creates a unified terminal aesthetic without importing any font files. | ||
| 122 | |||
| 123 | ```css | ||
| 124 | body { | ||
| 125 | font-family: "SF Mono", "Cascadia Code", "JetBrains Mono", "Fira Code", Monaco, "Courier New", monospace; | ||
| 126 | } | ||
| 127 | ``` | ||
| 128 | |||
| 129 | ### 6. Header Wordmark — RESOLVED: Yes, Add It | ||
| 130 | |||
| 131 | Add a styled `<span className="app-wordmark">CLAUDE FLOW</span>` as the **first element** inside `.header-left`, before the project dropdown. Separated from the controls by a subtle right border. | ||
| 132 | |||
| 133 | ```css | ||
| 134 | .app-wordmark { | ||
| 135 | font-size: 13px; | ||
| 136 | font-weight: 700; | ||
| 137 | letter-spacing: 0.15em; | ||
| 138 | text-transform: uppercase; | ||
| 139 | color: var(--text-primary); | ||
| 140 | padding-right: 12px; | ||
| 141 | border-right: 1px solid var(--border); | ||
| 142 | margin-right: 4px; | ||
| 143 | user-select: none; | ||
| 144 | white-space: nowrap; | ||
| 145 | } | ||
| 146 | ``` | ||
| 147 | |||
| 148 | ### 7. Other Cipherpunk Touches — All CSS-Only | ||
| 149 | |||
| 150 | - **Sharper borders**: reduce `border-radius` from 4–8px → **2px** on all controls (buttons, inputs, messages, badges). Phase steps → 2px. Chat messages → 4px. | ||
| 151 | - **Uppercase meta labels**: `text-transform: uppercase; letter-spacing: 0.07em` on `.token-label`, `.phase-step`, `.document-header span`, `.badge`, `.implementing-status` | ||
| 152 | - **Subtle focus glow**: `box-shadow: 0 0 0 2px rgba(96, 165, 250, 0.3)` on focused inputs and buttons | ||
| 153 | - **Phase brackets**: Phase step text to be updated in `Header.tsx` JSX: `[Research]` → `[RESEARCH]` etc., with CSS uppercase doing the work so the component just renders the labels naturally | ||
| 154 | |||
| 155 | ### 8. Light Mode Color Palette (Proposed) | ||
| 156 | |||
| 157 | ```css | ||
| 158 | html[data-theme="light"] { | ||
| 159 | --bg-primary: #f4f4f2; /* warm off-white — not stark, feels terminal-adjacent */ | ||
| 160 | --bg-secondary: #e8e8e5; /* panels, header */ | ||
| 161 | --bg-tertiary: #d8d8d4; /* inputs, code blocks */ | ||
| 162 | --border: #b4b4b0; /* dividers */ | ||
| 163 | --text-primary: #1a1a18; /* near-black */ | ||
| 164 | --text-secondary:#5a5a56; /* muted */ | ||
| 165 | --accent: #2563eb; /* deep blue */ | ||
| 166 | --accent-hover: #1d4ed8; | ||
| 167 | --success: #059669; | ||
| 168 | --warning: #d97706; | ||
| 169 | --danger: #dc2626; | ||
| 170 | } | ||
| 171 | ``` | ||
| 172 | |||
| 173 | Warm stone tones rather than pure white — keeps the app from feeling like a generic SaaS product. | ||
| 174 | |||
| 175 | ### 9. No Existing Theme Infrastructure | ||
| 176 | There is currently **no ThemeContext, no localStorage usage, no `data-theme` attribute**. Fully greenfield. Simple prop-passing from `App.tsx` is sufficient. | ||
| 177 | |||
| 178 | ### 10. Window Styling Note | ||
| 179 | - **macOS** (`titleBarStyle: "hiddenInset"`): traffic lights appear, no visible title text in window frame. HTML `<title>` shows in Dock tooltip and app switcher. | ||
| 180 | - **Linux/Windows**: default Electron title bar shows `<title>` in window chrome. Critical to fix. | ||
| 181 | |||
| 182 | --- | ||
| 183 | |||
| 184 | ## Resolved Decisions | ||
| 185 | |||
| 186 | | Question | Decision | | ||
| 187 | |----------|----------| | ||
| 188 | | Accent color | Electric blue: `#60a5fa` (dark) / `#2563eb` (light). No green/cyan. | | ||
| 189 | | CodeMirror light theme | `syntaxHighlighting(defaultHighlightStyle)` from already-installed `@codemirror/language`. Zero new packages. | | ||
| 190 | | Font scope | **Full mono** — `body` font-family. Entire UI uses monospace stack. | | ||
| 191 | | Header wordmark | **Yes** — `CLAUDE FLOW` in `.header-left`, uppercase + letter-spacing + right-border separator | | ||
| 192 | | Theme toggle style | **Text toggle**: `[dark]` / `[light]` bracket notation | | ||
| 193 | |||
| 194 | --- | ||
| 195 | |||
| 196 | ## Proposed Change Scope | ||
| 197 | |||
| 198 | | Change | File(s) | Effort | | ||
| 199 | |--------|---------|--------| | ||
| 200 | | Fix `<title>` | `renderer/index.html` | Trivial | | ||
| 201 | | Apply monospace body font | `globals.css` | Trivial | | ||
| 202 | | Sharpen border-radius across all controls | `globals.css` | Small | | ||
| 203 | | Uppercase + letter-spacing on meta labels | `globals.css` | Small | | ||
| 204 | | Brighter dark-mode accent (#60a5fa) | `globals.css` | Trivial | | ||
| 205 | | Add `html[data-theme="light"]` color block | `globals.css` | Small | | ||
| 206 | | Add focus glow styles | `globals.css` | Small | | ||
| 207 | | Add `.app-wordmark` styles | `globals.css` | Trivial | | ||
| 208 | | Add `[dark]`/`[light]` toggle button styles | `globals.css` | Trivial | | ||
| 209 | | Add theme state + `localStorage` init | `App.tsx` | Small | | ||
| 210 | | Pass `theme` + `onToggleTheme` props to Header | `App.tsx` → `Header.tsx` | Small | | ||
| 211 | | Pass `theme` prop to DocumentPane | `App.tsx` → `DocumentPane.tsx` | Small | | ||
| 212 | | Add `CLAUDE FLOW` wordmark element | `Header.tsx` | Trivial | | ||
| 213 | | Add `[dark]`/`[light]` toggle button | `Header.tsx` | Small | | ||
| 214 | | Make CodeMirror theme dynamic | `DocumentPane.tsx` | Small | | ||
| @@ -1,5 +1,4 @@ | |||
| 1 | node_modules | 1 | node_modules |
| 2 | release | 2 | release |
| 3 | dist | 3 | dist |
| 4 | .claude-flow/ | ||
| 5 | *.sync-conflict-* | 4 | *.sync-conflict-* |
| @@ -14,15 +14,12 @@ At each phase, edit the document to add notes (`// REVIEW:`, `// NOTE:`), click | |||
| 14 | 14 | ||
| 15 | ## Sessions | 15 | ## Sessions |
| 16 | 16 | ||
| 17 | Each session has isolated artifacts: | 17 | Each session has isolated artifacts stored in `~/.claude-flow/projects/{projectId}/sessions/{sessionId}/`: |
| 18 | 18 | ||
| 19 | ``` | 19 | - `research.md` — Session research |
| 20 | .claude-flow/sessions/{sessionId}/ | 20 | - `plan.md` — Session plan |
| 21 | ├── research.md # Session research | ||
| 22 | └── plan.md # Session plan | ||
| 23 | ``` | ||
| 24 | 21 | ||
| 25 | Concurrent sessions supported — switch between them freely. | 22 | Concurrent sessions supported — switch between them freely. Artifacts live outside your repo so they never get accidentally committed. |
| 26 | 23 | ||
| 27 | ## Setup | 24 | ## Setup |
| 28 | 25 | ||
diff --git a/renderer/src/App.tsx b/renderer/src/App.tsx index c56f292..f7ba41d 100644 --- a/renderer/src/App.tsx +++ b/renderer/src/App.tsx | |||
| @@ -92,10 +92,10 @@ export function App() { | |||
| 92 | // Load messages | 92 | // Load messages |
| 93 | api.listMessages(selectedSession.id).then(setMessages); | 93 | api.listMessages(selectedSession.id).then(setMessages); |
| 94 | 94 | ||
| 95 | // Load session-specific artifact | 95 | // Load session-specific artifact (from ~/.claude-flow/) |
| 96 | const filename = | 96 | const filename = |
| 97 | selectedSession.phase === "research" ? "research.md" : "plan.md"; | 97 | selectedSession.phase === "research" ? "research.md" : "plan.md"; |
| 98 | api.readSessionArtifact(selectedProject.path, selectedSession.id, filename).then((content) => { | 98 | api.readSessionArtifact(selectedProject.id, selectedSession.id, filename).then((content) => { |
| 99 | const text = content || ""; | 99 | const text = content || ""; |
| 100 | setDocumentContent(text); | 100 | setDocumentContent(text); |
| 101 | setOriginalContent(text); | 101 | setOriginalContent(text); |
| @@ -125,7 +125,7 @@ export function App() { | |||
| 125 | if (selectedProject && selectedSession) { | 125 | if (selectedProject && selectedSession) { |
| 126 | const filename = | 126 | const filename = |
| 127 | selectedSession.phase === "research" ? "research.md" : "plan.md"; | 127 | selectedSession.phase === "research" ? "research.md" : "plan.md"; |
| 128 | api.readSessionArtifact(selectedProject.path, selectedSession.id, filename).then((content) => { | 128 | api.readSessionArtifact(selectedProject.id, selectedSession.id, filename).then((content) => { |
| 129 | const text = content || ""; | 129 | const text = content || ""; |
| 130 | setDocumentContent(text); | 130 | setDocumentContent(text); |
| 131 | setOriginalContent(text); | 131 | setOriginalContent(text); |
| @@ -193,10 +193,10 @@ export function App() { | |||
| 193 | if (!selectedSession || !selectedProject) return; | 193 | if (!selectedSession || !selectedProject) return; |
| 194 | setError(null); | 194 | setError(null); |
| 195 | try { | 195 | try { |
| 196 | // Save user edits first (session-specific) | 196 | // Save user edits first (session-specific, stored in ~/.claude-flow/) |
| 197 | const filename = | 197 | const filename = |
| 198 | selectedSession.phase === "research" ? "research.md" : "plan.md"; | 198 | selectedSession.phase === "research" ? "research.md" : "plan.md"; |
| 199 | await api.writeSessionArtifact(selectedProject.path, selectedSession.id, filename, documentContent); | 199 | await api.writeSessionArtifact(selectedProject.id, selectedSession.id, filename, documentContent); |
| 200 | setOriginalContent(documentContent); | 200 | setOriginalContent(documentContent); |
| 201 | setIsLoading(true); | 201 | setIsLoading(true); |
| 202 | await api.triggerReview(selectedSession.id); | 202 | await api.triggerReview(selectedSession.id); |
| @@ -210,10 +210,10 @@ export function App() { | |||
| 210 | if (!selectedSession || !selectedProject) return; | 210 | if (!selectedSession || !selectedProject) return; |
| 211 | setError(null); | 211 | setError(null); |
| 212 | try { | 212 | try { |
| 213 | // Save any pending edits (session-specific) | 213 | // Save any pending edits (session-specific, stored in ~/.claude-flow/) |
| 214 | const filename = | 214 | const filename = |
| 215 | selectedSession.phase === "research" ? "research.md" : "plan.md"; | 215 | selectedSession.phase === "research" ? "research.md" : "plan.md"; |
| 216 | await api.writeSessionArtifact(selectedProject.path, selectedSession.id, filename, documentContent); | 216 | await api.writeSessionArtifact(selectedProject.id, selectedSession.id, filename, documentContent); |
| 217 | 217 | ||
| 218 | const nextPhase = await api.advancePhase(selectedSession.id); | 218 | const nextPhase = await api.advancePhase(selectedSession.id); |
| 219 | if (nextPhase) { | 219 | if (nextPhase) { |
diff --git a/src/main/claude/index.ts b/src/main/claude/index.ts index 4d8909b..b8c9c07 100644 --- a/src/main/claude/index.ts +++ b/src/main/claude/index.ts | |||
| @@ -6,16 +6,28 @@ import { getProject } from "../db/projects"; | |||
| 6 | import { updateSession } from "../db/sessions"; | 6 | import { updateSession } from "../db/sessions"; |
| 7 | import fs from "node:fs"; | 7 | import fs from "node:fs"; |
| 8 | import path from "node:path"; | 8 | import path from "node:path"; |
| 9 | import os from "node:os"; | ||
| 9 | 10 | ||
| 10 | // Track active queries by session ID | 11 | // Track active queries by session ID |
| 11 | const activeQueries = new Map<string, Query>(); | 12 | const activeQueries = new Map<string, Query>(); |
| 12 | 13 | ||
| 14 | // Global storage in home directory | ||
| 15 | const GLOBAL_CLAUDE_FLOW_DIR = path.join(os.homedir(), ".claude-flow"); | ||
| 16 | |||
| 13 | function ensureDir(dirPath: string): void { | 17 | function ensureDir(dirPath: string): void { |
| 14 | if (!fs.existsSync(dirPath)) { | 18 | if (!fs.existsSync(dirPath)) { |
| 15 | fs.mkdirSync(dirPath, { recursive: true }); | 19 | fs.mkdirSync(dirPath, { recursive: true }); |
| 16 | } | 20 | } |
| 17 | } | 21 | } |
| 18 | 22 | ||
| 23 | function getProjectDir(projectId: string): string { | ||
| 24 | return path.join(GLOBAL_CLAUDE_FLOW_DIR, "projects", projectId); | ||
| 25 | } | ||
| 26 | |||
| 27 | function getSessionDir(projectId: string, sessionId: string): string { | ||
| 28 | return path.join(getProjectDir(projectId), "sessions", sessionId); | ||
| 29 | } | ||
| 30 | |||
| 19 | export interface SendMessageOptions { | 31 | export interface SendMessageOptions { |
| 20 | session: Session; | 32 | session: Session; |
| 21 | message: string; | 33 | message: string; |
| @@ -30,8 +42,8 @@ export async function sendMessage({ | |||
| 30 | const project = getProject(session.project_id); | 42 | const project = getProject(session.project_id); |
| 31 | if (!project) throw new Error("Project not found"); | 43 | if (!project) throw new Error("Project not found"); |
| 32 | 44 | ||
| 33 | // Ensure session artifact directory exists | 45 | // Ensure session artifact directory exists in global storage |
| 34 | const sessionDir = path.join(project.path, getSessionArtifactDir(session.id)); | 46 | const sessionDir = getSessionDir(session.project_id, session.id); |
| 35 | ensureDir(sessionDir); | 47 | ensureDir(sessionDir); |
| 36 | 48 | ||
| 37 | const phaseConfig = getPhaseConfig( | 49 | const phaseConfig = getPhaseConfig( |
| @@ -100,22 +112,22 @@ export function advancePhase(session: Session): Phase | null { | |||
| 100 | } | 112 | } |
| 101 | 113 | ||
| 102 | /** | 114 | /** |
| 103 | * Get the artifact path for a session and phase | 115 | * Get the artifact path for a session and phase (in global storage) |
| 104 | */ | 116 | */ |
| 105 | export function getArtifactPath(session: Session): string { | 117 | export function getArtifactPath(session: Session): string { |
| 106 | const filename = getArtifactFilename(session.phase as Phase); | 118 | const filename = getArtifactFilename(session.phase as Phase); |
| 107 | return path.join(getSessionArtifactDir(session.id), filename); | 119 | return path.join(getSessionDir(session.project_id, session.id), filename); |
| 108 | } | 120 | } |
| 109 | 121 | ||
| 110 | /** | 122 | /** |
| 111 | * Read an artifact file for a session | 123 | * Read an artifact file for a session (from global storage) |
| 112 | */ | 124 | */ |
| 113 | export function readSessionArtifact( | 125 | export function readSessionArtifact( |
| 114 | projectPath: string, | 126 | projectId: string, |
| 115 | sessionId: string, | 127 | sessionId: string, |
| 116 | filename: string | 128 | filename: string |
| 117 | ): string | null { | 129 | ): string | null { |
| 118 | const filePath = path.join(projectPath, getSessionArtifactDir(sessionId), filename); | 130 | const filePath = path.join(getSessionDir(projectId, sessionId), filename); |
| 119 | if (fs.existsSync(filePath)) { | 131 | if (fs.existsSync(filePath)) { |
| 120 | return fs.readFileSync(filePath, "utf-8"); | 132 | return fs.readFileSync(filePath, "utf-8"); |
| 121 | } | 133 | } |
| @@ -123,15 +135,15 @@ export function readSessionArtifact( | |||
| 123 | } | 135 | } |
| 124 | 136 | ||
| 125 | /** | 137 | /** |
| 126 | * Write an artifact file for a session | 138 | * Write an artifact file for a session (to global storage) |
| 127 | */ | 139 | */ |
| 128 | export function writeSessionArtifact( | 140 | export function writeSessionArtifact( |
| 129 | projectPath: string, | 141 | projectId: string, |
| 130 | sessionId: string, | 142 | sessionId: string, |
| 131 | filename: string, | 143 | filename: string, |
| 132 | content: string | 144 | content: string |
| 133 | ): void { | 145 | ): void { |
| 134 | const dir = path.join(projectPath, getSessionArtifactDir(sessionId)); | 146 | const dir = getSessionDir(projectId, sessionId); |
| 135 | ensureDir(dir); | 147 | ensureDir(dir); |
| 136 | fs.writeFileSync(path.join(dir, filename), content, "utf-8"); | 148 | fs.writeFileSync(path.join(dir, filename), content, "utf-8"); |
| 137 | } | 149 | } |
| @@ -156,37 +168,10 @@ export function writeClaudeMd(projectPath: string, content: string): void { | |||
| 156 | } | 168 | } |
| 157 | 169 | ||
| 158 | /** | 170 | /** |
| 159 | * Read an artifact file from the project's .claude-flow directory (legacy path) | 171 | * Clear session artifacts from global storage |
| 160 | */ | ||
| 161 | export function readArtifact( | ||
| 162 | projectPath: string, | ||
| 163 | filename: string | ||
| 164 | ): string | null { | ||
| 165 | const filePath = path.join(projectPath, ".claude-flow", filename); | ||
| 166 | if (fs.existsSync(filePath)) { | ||
| 167 | return fs.readFileSync(filePath, "utf-8"); | ||
| 168 | } | ||
| 169 | return null; | ||
| 170 | } | ||
| 171 | |||
| 172 | /** | ||
| 173 | * Write an artifact file to the project's .claude-flow directory (legacy path) | ||
| 174 | */ | ||
| 175 | export function writeArtifact( | ||
| 176 | projectPath: string, | ||
| 177 | filename: string, | ||
| 178 | content: string | ||
| 179 | ): void { | ||
| 180 | const dir = path.join(projectPath, ".claude-flow"); | ||
| 181 | ensureDir(dir); | ||
| 182 | fs.writeFileSync(path.join(dir, filename), content, "utf-8"); | ||
| 183 | } | ||
| 184 | |||
| 185 | /** | ||
| 186 | * Clear session artifacts | ||
| 187 | */ | 172 | */ |
| 188 | export function clearSessionArtifacts(projectPath: string, sessionId: string): void { | 173 | export function clearSessionArtifacts(projectId: string, sessionId: string): void { |
| 189 | const dir = path.join(projectPath, getSessionArtifactDir(sessionId)); | 174 | const dir = getSessionDir(projectId, sessionId); |
| 190 | if (fs.existsSync(dir)) { | 175 | if (fs.existsSync(dir)) { |
| 191 | fs.rmSync(dir, { recursive: true, force: true }); | 176 | fs.rmSync(dir, { recursive: true, force: true }); |
| 192 | } | 177 | } |
diff --git a/src/main/claude/phases.ts b/src/main/claude/phases.ts index 6678c08..f1df719 100644 --- a/src/main/claude/phases.ts +++ b/src/main/claude/phases.ts | |||
| @@ -10,9 +10,9 @@ export interface PhaseConfig { | |||
| 10 | initialMessage: string; | 10 | initialMessage: string; |
| 11 | } | 11 | } |
| 12 | 12 | ||
| 13 | // Get session-specific artifact path | 13 | // Get session-specific artifact path (relative to ~/.claude-flow/) |
| 14 | export function getSessionArtifactDir(sessionId: string): string { | 14 | export function getSessionArtifactDir(sessionId: string): string { |
| 15 | return `.claude-flow/sessions/${sessionId}`; | 15 | return `sessions/${sessionId}`; |
| 16 | } | 16 | } |
| 17 | 17 | ||
| 18 | export function getArtifactPath(phase: Phase, sessionId: string): string { | 18 | export function getArtifactPath(phase: Phase, sessionId: string): string { |
| @@ -30,7 +30,7 @@ export const phaseConfigs: Record<Phase, PhaseConfig> = { | |||
| 30 | systemPrompt: `You are in RESEARCH mode. Your ONLY job is to understand the codebase. | 30 | systemPrompt: `You are in RESEARCH mode. Your ONLY job is to understand the codebase. |
| 31 | 31 | ||
| 32 | CRITICAL RULES: | 32 | CRITICAL RULES: |
| 33 | 1. You MUST write ALL findings to \`.claude-flow/sessions/{sessionId}/research.md\` — this is your PRIMARY output | 33 | 1. You MUST write ALL findings to the session research.md — this is your PRIMARY output |
| 34 | 2. DO NOT just respond in chat. The document viewer shows research.md, so write there. | 34 | 2. DO NOT just respond in chat. The document viewer shows research.md, so write there. |
| 35 | 3. DO NOT suggest moving to planning or implementation | 35 | 3. DO NOT suggest moving to planning or implementation |
| 36 | 4. DO NOT ask "are you ready to implement?" or similar | 36 | 4. DO NOT ask "are you ready to implement?" or similar |
| @@ -38,16 +38,15 @@ CRITICAL RULES: | |||
| 38 | 6. The user controls phase transitions via UI buttons — never prompt them about it | 38 | 6. The user controls phase transitions via UI buttons — never prompt them about it |
| 39 | 39 | ||
| 40 | CONTEXT: | 40 | CONTEXT: |
| 41 | - You are in a git worktree at \`.claude-flow/worktrees/{sessionId}/\` | 41 | - Read CLAUDE.md in the project root (if it exists) for codebase overview |
| 42 | - Read CLAUDE.md in the project root for shared codebase overview | ||
| 43 | - If CLAUDE.md doesn't exist, create it with your initial findings | ||
| 44 | - This file contains general architecture info shared across all sessions | 42 | - This file contains general architecture info shared across all sessions |
| 43 | - If CLAUDE.md doesn't exist, create it with your initial findings | ||
| 45 | 44 | ||
| 46 | WORKFLOW: | 45 | WORKFLOW: |
| 47 | 1. Read CLAUDE.md (create at project root if missing) | 46 | 1. Read CLAUDE.md (create at project root if missing) |
| 48 | 2. Ask what to research (if unclear) | 47 | 2. Ask what to research (if unclear) |
| 49 | 3. Read files thoroughly using Read, Glob, Grep | 48 | 3. Read files thoroughly using Read, Glob, Grep |
| 50 | 4. Write findings to \`.claude-flow/sessions/{sessionId}/research.md\` | 49 | 4. Write findings to session research.md |
| 51 | 5. Update CLAUDE.md with any new general insights worth sharing | 50 | 5. Update CLAUDE.md with any new general insights worth sharing |
| 52 | 51 | ||
| 53 | FORMAT for research.md: | 52 | FORMAT for research.md: |
| @@ -70,7 +69,7 @@ FORMAT for research.md: | |||
| 70 | [Things that need clarification] | 69 | [Things that need clarification] |
| 71 | \`\`\` | 70 | \`\`\` |
| 72 | 71 | ||
| 73 | Remember: Your output goes in \`.claude-flow/sessions/{sessionId}/research.md\`, not chat. Chat is for clarifying questions only.`, | 72 | Remember: Your output goes in research.md, not chat. Chat is for clarifying questions only.`, |
| 74 | }, | 73 | }, |
| 75 | 74 | ||
| 76 | plan: { | 75 | plan: { |
| @@ -81,7 +80,7 @@ Remember: Your output goes in \`.claude-flow/sessions/{sessionId}/research.md\`, | |||
| 81 | systemPrompt: `You are in PLANNING mode. Your ONLY job is to create an implementation plan. | 80 | systemPrompt: `You are in PLANNING mode. Your ONLY job is to create an implementation plan. |
| 82 | 81 | ||
| 83 | CRITICAL RULES: | 82 | CRITICAL RULES: |
| 84 | 1. You MUST write the plan to \`.claude-flow/sessions/{sessionId}/plan.md\` — this is your PRIMARY output | 83 | 1. You MUST write the plan to session plan.md — this is your PRIMARY output |
| 85 | 2. DO NOT just respond in chat. The document viewer shows plan.md, so write there. | 84 | 2. DO NOT just respond in chat. The document viewer shows plan.md, so write there. |
| 86 | 3. DO NOT implement anything — no code changes to source files | 85 | 3. DO NOT implement anything — no code changes to source files |
| 87 | 4. DO NOT ask "should I start implementing?" or similar | 86 | 4. DO NOT ask "should I start implementing?" or similar |
| @@ -89,14 +88,13 @@ CRITICAL RULES: | |||
| 89 | 6. Base your plan on the session research.md and CLAUDE.md | 88 | 6. Base your plan on the session research.md and CLAUDE.md |
| 90 | 89 | ||
| 91 | CONTEXT: | 90 | CONTEXT: |
| 92 | - You are in a git worktree at \`.claude-flow/worktrees/{sessionId}/\` | ||
| 93 | - Read CLAUDE.md at project root for codebase overview | 91 | - Read CLAUDE.md at project root for codebase overview |
| 94 | - Read \`.claude-flow/sessions/{sessionId}/research.md\` for this specific task | 92 | - Read the session research.md to understand the specific task |
| 95 | 93 | ||
| 96 | WORKFLOW: | 94 | WORKFLOW: |
| 97 | 1. Read CLAUDE.md for codebase overview | 95 | 1. Read CLAUDE.md for codebase overview |
| 98 | 2. Read the session research.md to understand the specific task | 96 | 2. Read the session research.md to understand the specific task |
| 99 | 3. Write a detailed plan to \`.claude-flow/sessions/{sessionId}/plan.md\` | 97 | 3. Write a detailed plan to session plan.md |
| 100 | 4. Include specific code snippets showing proposed changes | 98 | 4. Include specific code snippets showing proposed changes |
| 101 | 5. Make the plan detailed enough that implementation is mechanical | 99 | 5. Make the plan detailed enough that implementation is mechanical |
| 102 | 100 | ||
| @@ -134,47 +132,30 @@ FORMAT for plan.md: | |||
| 134 | 132 | ||
| 135 | When the user adds annotations to plan.md and clicks Review, address each annotation and update the document. | 133 | When the user adds annotations to plan.md and clicks Review, address each annotation and update the document. |
| 136 | 134 | ||
| 137 | Remember: Your output goes in \`.claude-flow/sessions/{sessionId}/plan.md\`, not chat. Chat is for clarifying questions only.`, | 135 | Remember: Your output goes in plan.md, not chat. Chat is for clarifying questions only.`, |
| 138 | }, | 136 | }, |
| 139 | 137 | ||
| 140 | implement: { | 138 | implement: { |
| 141 | permissionMode: "acceptEdits", | 139 | permissionMode: "acceptEdits", |
| 142 | tools: ["Read", "Write", "Edit", "Bash", "Glob", "Grep"], | 140 | tools: ["Read", "Write", "Edit", "Bash", "Glob", "Grep"], |
| 143 | initialMessage: | 141 | initialMessage: |
| 144 | "Starting implementation. I'll follow the plan exactly, commit as I go, and mark tasks complete.", | 142 | "Starting implementation. I'll follow the plan exactly and mark tasks complete as I go.", |
| 145 | systemPrompt: `You are in IMPLEMENTATION mode. Execute the approved plan. | 143 | systemPrompt: `You are in IMPLEMENTATION mode. Execute the approved plan. |
| 146 | 144 | ||
| 147 | CRITICAL RULES: | 145 | CRITICAL RULES: |
| 148 | 1. Read \`.claude-flow/sessions/{sessionId}/plan.md\` and follow it exactly | 146 | 1. Read session plan.md and follow it exactly |
| 149 | 2. Mark tasks complete in plan.md as you finish them: - [ ] → - [x] | 147 | 2. Mark tasks complete in plan.md as you finish them: - [ ] → - [x] |
| 150 | 3. DO NOT deviate from the plan without asking | 148 | 3. DO NOT deviate from the plan without asking |
| 151 | 4. Run tests/typecheck if available | 149 | 4. Run tests/typecheck if available |
| 152 | 5. Make git commits as you complete logical chunks of work | 150 | 5. Stop and ask if you encounter issues not covered by the plan |
| 153 | 6. Stop and ask if you encounter issues not covered by the plan | ||
| 154 | |||
| 155 | CONTEXT: | ||
| 156 | - You are in a git worktree at \`.claude-flow/worktrees/{sessionId}/\` | ||
| 157 | - This is an isolated branch: \`claude-flow/{sessionId}\` | ||
| 158 | - Your commits will not affect the main branch until merged | ||
| 159 | - The user can review your work in this worktree before accepting | ||
| 160 | 151 | ||
| 161 | WORKFLOW: | 152 | WORKFLOW: |
| 162 | 1. Read \`.claude-flow/sessions/{sessionId}/plan.md\` | 153 | 1. Read session plan.md |
| 163 | 2. Execute each task in order | 154 | 2. Execute each task in order |
| 164 | 3. Update plan.md to mark tasks complete | 155 | 3. Update plan.md to mark tasks complete |
| 165 | 4. Make git commits with clear messages as you finish chunks | 156 | 4. Continue until all tasks are done |
| 166 | 5. Continue until all tasks are done | 157 | |
| 167 | 158 | When complete, summarize what was done and any follow-up tasks.`, | |
| 168 | COMMIT GUIDELINES: | ||
| 169 | - Commit after completing logical units of work | ||
| 170 | - Use clear commit messages (e.g., "Add user authentication middleware") | ||
| 171 | - Don't commit broken or incomplete code | ||
| 172 | - Update CLAUDE.md at project root if you discover important architecture info | ||
| 173 | |||
| 174 | When complete, summarize what was done and tell the user how to review: | ||
| 175 | - The work is in worktree: \`.claude-flow/worktrees/{sessionId}/\` | ||
| 176 | - Branch: \`claude-flow/{sessionId}\` | ||
| 177 | - They can review, then merge or discard as needed`, | ||
| 178 | }, | 159 | }, |
| 179 | }; | 160 | }; |
| 180 | 161 | ||
diff --git a/src/main/ipc/handlers.ts b/src/main/ipc/handlers.ts index 2d5e3d3..d9beaf0 100644 --- a/src/main/ipc/handlers.ts +++ b/src/main/ipc/handlers.ts | |||
| @@ -26,11 +26,8 @@ export function registerIpcHandlers(mainWindow: BrowserWindow): void { | |||
| 26 | ipcMain.handle("sessions:delete", (_, id: string) => { | 26 | ipcMain.handle("sessions:delete", (_, id: string) => { |
| 27 | const session = sessions.getSession(id); | 27 | const session = sessions.getSession(id); |
| 28 | if (session) { | 28 | if (session) { |
| 29 | const project = projects.getProject(session.project_id); | 29 | // Clean up session artifacts from global storage |
| 30 | if (project) { | 30 | claude.clearSessionArtifacts(session.project_id, id); |
| 31 | // Clean up session artifacts | ||
| 32 | claude.clearSessionArtifacts(project.path, id); | ||
| 33 | } | ||
| 34 | } | 31 | } |
| 35 | sessions.deleteSession(id); | 32 | sessions.deleteSession(id); |
| 36 | }); | 33 | }); |
| @@ -103,22 +100,22 @@ export function registerIpcHandlers(mainWindow: BrowserWindow): void { | |||
| 103 | } | 100 | } |
| 104 | ); | 101 | ); |
| 105 | 102 | ||
| 106 | // Session Artifacts (new session-specific API) | 103 | // Session Artifacts (stored in ~/.claude-flow/) |
| 107 | ipcMain.handle( | 104 | ipcMain.handle( |
| 108 | "artifact:readSession", | 105 | "artifact:readSession", |
| 109 | (_, projectPath: string, sessionId: string, filename: string) => { | 106 | (_, projectId: string, sessionId: string, filename: string) => { |
| 110 | return claude.readSessionArtifact(projectPath, sessionId, filename); | 107 | return claude.readSessionArtifact(projectId, sessionId, filename); |
| 111 | } | 108 | } |
| 112 | ); | 109 | ); |
| 113 | 110 | ||
| 114 | ipcMain.handle( | 111 | ipcMain.handle( |
| 115 | "artifact:writeSession", | 112 | "artifact:writeSession", |
| 116 | (_, projectPath: string, sessionId: string, filename: string, content: string) => { | 113 | (_, projectId: string, sessionId: string, filename: string, content: string) => { |
| 117 | claude.writeSessionArtifact(projectPath, sessionId, filename, content); | 114 | claude.writeSessionArtifact(projectId, sessionId, filename, content); |
| 118 | } | 115 | } |
| 119 | ); | 116 | ); |
| 120 | 117 | ||
| 121 | // CLAUDE.md | 118 | // CLAUDE.md (stored in project) |
| 122 | ipcMain.handle("claudemd:read", (_, projectPath: string) => { | 119 | ipcMain.handle("claudemd:read", (_, projectPath: string) => { |
| 123 | return claude.readClaudeMd(projectPath); | 120 | return claude.readClaudeMd(projectPath); |
| 124 | }); | 121 | }); |
| @@ -127,21 +124,6 @@ export function registerIpcHandlers(mainWindow: BrowserWindow): void { | |||
| 127 | claude.writeClaudeMd(projectPath, content); | 124 | claude.writeClaudeMd(projectPath, content); |
| 128 | }); | 125 | }); |
| 129 | 126 | ||
| 130 | // Legacy artifact API (for backward compatibility) | ||
| 131 | ipcMain.handle( | ||
| 132 | "artifact:read", | ||
| 133 | (_, projectPath: string, filename: string) => { | ||
| 134 | return claude.readArtifact(projectPath, filename); | ||
| 135 | } | ||
| 136 | ); | ||
| 137 | |||
| 138 | ipcMain.handle( | ||
| 139 | "artifact:write", | ||
| 140 | (_, projectPath: string, filename: string, content: string) => { | ||
| 141 | claude.writeArtifact(projectPath, filename, content); | ||
| 142 | } | ||
| 143 | ); | ||
| 144 | |||
| 145 | // Dialogs | 127 | // Dialogs |
| 146 | ipcMain.handle("dialog:selectDirectory", async () => { | 128 | ipcMain.handle("dialog:selectDirectory", async () => { |
| 147 | const result = await dialog.showOpenDialog(mainWindow, { | 129 | const result = await dialog.showOpenDialog(mainWindow, { |
diff --git a/src/main/preload.ts b/src/main/preload.ts index 7c1d634..2c228dd 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts | |||
| @@ -31,34 +31,23 @@ export interface ClaudeFlowAPI { | |||
| 31 | mode: UserPermissionMode | 31 | mode: UserPermissionMode |
| 32 | ) => Promise<void>; | 32 | ) => Promise<void>; |
| 33 | 33 | ||
| 34 | // Session Artifacts (session-specific) | 34 | // Session Artifacts (stored in ~/.claude-flow/) |
| 35 | readSessionArtifact: ( | 35 | readSessionArtifact: ( |
| 36 | projectPath: string, | 36 | projectId: string, |
| 37 | sessionId: string, | 37 | sessionId: string, |
| 38 | filename: string | 38 | filename: string |
| 39 | ) => Promise<string | null>; | 39 | ) => Promise<string | null>; |
| 40 | writeSessionArtifact: ( | 40 | writeSessionArtifact: ( |
| 41 | projectPath: string, | 41 | projectId: string, |
| 42 | sessionId: string, | 42 | sessionId: string, |
| 43 | filename: string, | 43 | filename: string, |
| 44 | content: string | 44 | content: string |
| 45 | ) => Promise<void>; | 45 | ) => Promise<void>; |
| 46 | 46 | ||
| 47 | // CLAUDE.md | 47 | // CLAUDE.md (stored in project) |
| 48 | readClaudeMd: (projectPath: string) => Promise<string | null>; | 48 | readClaudeMd: (projectPath: string) => Promise<string | null>; |
| 49 | writeClaudeMd: (projectPath: string, content: string) => Promise<void>; | 49 | writeClaudeMd: (projectPath: string, content: string) => Promise<void>; |
| 50 | 50 | ||
| 51 | // Legacy Artifacts (backward compat) | ||
| 52 | readArtifact: ( | ||
| 53 | projectPath: string, | ||
| 54 | filename: string | ||
| 55 | ) => Promise<string | null>; | ||
| 56 | writeArtifact: ( | ||
| 57 | projectPath: string, | ||
| 58 | filename: string, | ||
| 59 | content: string | ||
| 60 | ) => Promise<void>; | ||
| 61 | |||
| 62 | // Events | 51 | // Events |
| 63 | onClaudeMessage: ( | 52 | onClaudeMessage: ( |
| 64 | callback: (sessionId: string, message: SDKMessage) => void | 53 | callback: (sessionId: string, message: SDKMessage) => void |
| @@ -97,23 +86,17 @@ const api: ClaudeFlowAPI = { | |||
| 97 | setPermissionMode: (sessionId, mode) => | 86 | setPermissionMode: (sessionId, mode) => |
| 98 | ipcRenderer.invoke("workflow:setPermissionMode", sessionId, mode), | 87 | ipcRenderer.invoke("workflow:setPermissionMode", sessionId, mode), |
| 99 | 88 | ||
| 100 | // Session Artifacts | 89 | // Session Artifacts (stored in ~/.claude-flow/) |
| 101 | readSessionArtifact: (projectPath, sessionId, filename) => | 90 | readSessionArtifact: (projectId, sessionId, filename) => |
| 102 | ipcRenderer.invoke("artifact:readSession", projectPath, sessionId, filename), | 91 | ipcRenderer.invoke("artifact:readSession", projectId, sessionId, filename), |
| 103 | writeSessionArtifact: (projectPath, sessionId, filename, content) => | 92 | writeSessionArtifact: (projectId, sessionId, filename, content) => |
| 104 | ipcRenderer.invoke("artifact:writeSession", projectPath, sessionId, filename, content), | 93 | ipcRenderer.invoke("artifact:writeSession", projectId, sessionId, filename, content), |
| 105 | 94 | ||
| 106 | // CLAUDE.md | 95 | // CLAUDE.md (stored in project) |
| 107 | readClaudeMd: (projectPath) => ipcRenderer.invoke("claudemd:read", projectPath), | 96 | readClaudeMd: (projectPath) => ipcRenderer.invoke("claudemd:read", projectPath), |
| 108 | writeClaudeMd: (projectPath, content) => | 97 | writeClaudeMd: (projectPath, content) => |
| 109 | ipcRenderer.invoke("claudemd:write", projectPath, content), | 98 | ipcRenderer.invoke("claudemd:write", projectPath, content), |
| 110 | 99 | ||
| 111 | // Legacy Artifacts | ||
| 112 | readArtifact: (projectPath, filename) => | ||
| 113 | ipcRenderer.invoke("artifact:read", projectPath, filename), | ||
| 114 | writeArtifact: (projectPath, filename, content) => | ||
| 115 | ipcRenderer.invoke("artifact:write", projectPath, filename, content), | ||
| 116 | |||
| 117 | // Events | 100 | // Events |
| 118 | onClaudeMessage: (callback) => { | 101 | onClaudeMessage: (callback) => { |
| 119 | const handler = ( | 102 | const handler = ( |
