jettypod 4.4.118 → 4.4.121

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 (240) hide show
  1. package/.env +4 -3
  2. package/Cargo.lock +6450 -0
  3. package/Cargo.toml +35 -0
  4. package/README.md +5 -1
  5. package/TAURI-MIGRATION-PLAN.md +840 -0
  6. package/apps/dashboard/app/connect-claude/page.tsx +5 -6
  7. package/apps/dashboard/app/decision/[id]/page.tsx +63 -58
  8. package/apps/dashboard/app/demo/gates/page.tsx +43 -45
  9. package/apps/dashboard/app/design-system/page.tsx +868 -0
  10. package/apps/dashboard/app/globals.css +80 -4
  11. package/apps/dashboard/app/install-claude/page.tsx +4 -6
  12. package/apps/dashboard/app/login/page.tsx +72 -54
  13. package/apps/dashboard/app/page.tsx +101 -48
  14. package/apps/dashboard/app/settings/page.tsx +61 -13
  15. package/apps/dashboard/app/signup/page.tsx +242 -0
  16. package/apps/dashboard/app/subscribe/page.tsx +0 -2
  17. package/apps/dashboard/app/tests/page.tsx +37 -4
  18. package/apps/dashboard/app/welcome/page.tsx +13 -16
  19. package/apps/dashboard/app/work/[id]/page.tsx +117 -118
  20. package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
  21. package/apps/dashboard/components/AppShell.tsx +92 -85
  22. package/apps/dashboard/components/CardMenu.tsx +45 -12
  23. package/apps/dashboard/components/ClaudePanel.tsx +771 -850
  24. package/apps/dashboard/components/ClaudePanelInput.tsx +43 -15
  25. package/apps/dashboard/components/ConnectClaudeScreen.tsx +17 -34
  26. package/apps/dashboard/components/CopyableId.tsx +3 -4
  27. package/apps/dashboard/components/DetailReviewActions.tsx +100 -0
  28. package/apps/dashboard/components/DragContext.tsx +134 -63
  29. package/apps/dashboard/components/DraggableCard.tsx +3 -5
  30. package/apps/dashboard/components/DropZone.tsx +6 -7
  31. package/apps/dashboard/components/EditableDetailDescription.tsx +7 -13
  32. package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
  33. package/apps/dashboard/components/EditableTitle.tsx +26 -7
  34. package/apps/dashboard/components/ElapsedTimer.tsx +66 -0
  35. package/apps/dashboard/components/EpicGroup.tsx +359 -0
  36. package/apps/dashboard/components/GateCard.tsx +79 -17
  37. package/apps/dashboard/components/GateChoiceCard.tsx +15 -18
  38. package/apps/dashboard/components/InstallClaudeScreen.tsx +15 -32
  39. package/apps/dashboard/components/JettyLoader.tsx +37 -0
  40. package/apps/dashboard/components/KanbanBoard.tsx +368 -958
  41. package/apps/dashboard/components/KanbanCard.tsx +740 -0
  42. package/apps/dashboard/components/LazyCard.tsx +62 -0
  43. package/apps/dashboard/components/LazyMarkdown.tsx +11 -0
  44. package/apps/dashboard/components/MainNav.tsx +38 -73
  45. package/apps/dashboard/components/MessageBlock.tsx +468 -0
  46. package/apps/dashboard/components/ModeStartCard.tsx +15 -16
  47. package/apps/dashboard/components/OnboardingWelcome.tsx +213 -0
  48. package/apps/dashboard/components/PlaceholderCard.tsx +3 -4
  49. package/apps/dashboard/components/ProjectSwitcher.tsx +30 -30
  50. package/apps/dashboard/components/PrototypeTimeline.tsx +72 -51
  51. package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +406 -388
  52. package/apps/dashboard/components/RealTimeTestsWrapper.tsx +373 -235
  53. package/apps/dashboard/components/ReviewFooter.tsx +139 -0
  54. package/apps/dashboard/components/SessionList.tsx +19 -19
  55. package/apps/dashboard/components/SubscribeContent.tsx +91 -47
  56. package/apps/dashboard/components/TestTree.tsx +16 -16
  57. package/apps/dashboard/components/TipCard.tsx +16 -17
  58. package/apps/dashboard/components/Toast.tsx +5 -6
  59. package/apps/dashboard/components/TypeIcon.tsx +55 -0
  60. package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
  61. package/apps/dashboard/components/WaveCompletionAnimation.tsx +52 -65
  62. package/apps/dashboard/components/WelcomeScreen.tsx +19 -35
  63. package/apps/dashboard/components/WorkItemHeader.tsx +4 -5
  64. package/apps/dashboard/components/WorkItemTree.tsx +11 -32
  65. package/apps/dashboard/components/settings/AccountSection.tsx +55 -35
  66. package/apps/dashboard/components/settings/AiContextSection.tsx +89 -0
  67. package/apps/dashboard/components/settings/ContextDocumentsSection.tsx +317 -0
  68. package/apps/dashboard/components/settings/EnvVarsSection.tsx +74 -152
  69. package/apps/dashboard/components/settings/GeneralSection.tsx +162 -56
  70. package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
  71. package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -5
  72. package/apps/dashboard/components/ui/Button.tsx +104 -0
  73. package/apps/dashboard/components/ui/Input.tsx +78 -0
  74. package/apps/dashboard/components.json +1 -1
  75. package/apps/dashboard/contexts/ClaudeSessionContext.tsx +711 -418
  76. package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -5
  77. package/apps/dashboard/contexts/UsageContext.tsx +87 -32
  78. package/apps/dashboard/dev.sh +35 -0
  79. package/apps/dashboard/eslint.config.mjs +9 -9
  80. package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
  81. package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
  82. package/apps/dashboard/hooks/useWebSocket.ts +138 -83
  83. package/apps/dashboard/index.html +73 -0
  84. package/apps/dashboard/lib/constants.ts +43 -0
  85. package/apps/dashboard/lib/data-bridge.ts +722 -0
  86. package/apps/dashboard/lib/db.ts +69 -1265
  87. package/apps/dashboard/lib/environment-config.ts +173 -0
  88. package/apps/dashboard/lib/environment-verification.ts +119 -0
  89. package/apps/dashboard/lib/kanban-utils.ts +270 -0
  90. package/apps/dashboard/lib/proof-run.ts +495 -0
  91. package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
  92. package/apps/dashboard/lib/run-migrations.js +27 -2
  93. package/apps/dashboard/lib/service-recovery.ts +326 -0
  94. package/apps/dashboard/lib/session-state-machine.ts +1 -0
  95. package/apps/dashboard/lib/session-state-utils.ts +0 -164
  96. package/apps/dashboard/lib/session-stream-manager.ts +308 -134
  97. package/apps/dashboard/lib/shadows.ts +7 -0
  98. package/apps/dashboard/lib/stream-manager-registry.ts +46 -6
  99. package/apps/dashboard/lib/tauri-bridge.ts +102 -0
  100. package/apps/dashboard/lib/tauri.ts +106 -0
  101. package/apps/dashboard/lib/utils.ts +6 -0
  102. package/apps/dashboard/next-env.d.ts +1 -1
  103. package/apps/dashboard/package.json +21 -32
  104. package/apps/dashboard/public/bug-icon.png +0 -0
  105. package/apps/dashboard/public/buoy-icon.png +0 -0
  106. package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
  107. package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
  108. package/apps/dashboard/public/in-flight-seagull.png +0 -0
  109. package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
  110. package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
  111. package/apps/dashboard/public/jettypod_logo.png +0 -0
  112. package/apps/dashboard/public/pier-icon.png +0 -0
  113. package/apps/dashboard/public/star-icon.png +0 -0
  114. package/apps/dashboard/public/wrench-icon.png +0 -0
  115. package/apps/dashboard/scripts/tauri-build.js +228 -0
  116. package/apps/dashboard/scripts/upload-tauri-to-r2.js +125 -0
  117. package/apps/dashboard/scripts/ws-server.js +191 -0
  118. package/apps/dashboard/src/main.tsx +12 -0
  119. package/apps/dashboard/src/router.tsx +107 -0
  120. package/apps/dashboard/src/vite-env.d.ts +1 -0
  121. package/apps/dashboard/tsconfig.json +7 -12
  122. package/apps/dashboard/tsconfig.tsbuildinfo +1 -1
  123. package/apps/dashboard/vite.config.ts +33 -0
  124. package/apps/update-server/src/index.ts +228 -80
  125. package/claude-hooks/global-guardrails.js +14 -13
  126. package/crates/jettypod-cli/Cargo.toml +19 -0
  127. package/crates/jettypod-cli/src/commands.rs +1249 -0
  128. package/crates/jettypod-cli/src/main.rs +595 -0
  129. package/crates/jettypod-core/Cargo.toml +26 -0
  130. package/crates/jettypod-core/build.rs +98 -0
  131. package/crates/jettypod-core/migrations/V1__baseline.sql +197 -0
  132. package/crates/jettypod-core/migrations/V2__work_items_indexes.sql +6 -0
  133. package/crates/jettypod-core/migrations/V3__qa_steps.sql +2 -0
  134. package/crates/jettypod-core/src/auth.rs +294 -0
  135. package/crates/jettypod-core/src/config.rs +397 -0
  136. package/crates/jettypod-core/src/db/mod.rs +507 -0
  137. package/crates/jettypod-core/src/db/recovery.rs +114 -0
  138. package/crates/jettypod-core/src/db/startup.rs +101 -0
  139. package/crates/jettypod-core/src/db/validate.rs +149 -0
  140. package/crates/jettypod-core/src/error.rs +76 -0
  141. package/crates/jettypod-core/src/git.rs +458 -0
  142. package/crates/jettypod-core/src/lib.rs +20 -0
  143. package/crates/jettypod-core/src/sessions.rs +625 -0
  144. package/crates/jettypod-core/src/skills.rs +556 -0
  145. package/crates/jettypod-core/src/work.rs +1086 -0
  146. package/crates/jettypod-core/src/worktree.rs +628 -0
  147. package/crates/jettypod-core/src/ws.rs +767 -0
  148. package/cucumber-test.cjs +6 -0
  149. package/cucumber.js +9 -3
  150. package/docs/COMMAND_REFERENCE.md +34 -0
  151. package/hooks/post-checkout +32 -75
  152. package/hooks/post-merge +111 -10
  153. package/jest.setup.js +1 -0
  154. package/jettypod.js +145 -116
  155. package/lib/bdd-preflight.js +96 -0
  156. package/lib/chore-taxonomy.js +33 -10
  157. package/lib/database.js +36 -16
  158. package/lib/db-watcher.js +1 -1
  159. package/lib/git-hooks/pre-commit +1 -1
  160. package/lib/jettypod-backup.js +27 -4
  161. package/lib/merge-lock.js +111 -253
  162. package/lib/migrations/027-plan-at-creation-column.js +3 -1
  163. package/lib/migrations/029-remove-autoincrement.js +307 -0
  164. package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
  165. package/lib/migrations/030-rejection-round-columns.js +54 -0
  166. package/lib/migrations/031-session-isolation-index.js +17 -0
  167. package/lib/migrations/index.js +47 -4
  168. package/lib/schema.js +10 -5
  169. package/lib/seed-onboarding.js +1 -1
  170. package/lib/update-command/index.js +9 -175
  171. package/lib/work-commands/index.js +144 -19
  172. package/lib/work-tracking/index.js +148 -27
  173. package/lib/worktree-diagnostics.js +16 -16
  174. package/lib/worktree-facade.js +1 -1
  175. package/lib/worktree-manager.js +8 -8
  176. package/lib/worktree-reconciler.js +5 -5
  177. package/package.json +9 -2
  178. package/scripts/ndjson-to-cucumber-json.js +152 -0
  179. package/scripts/postinstall.js +25 -0
  180. package/skills-templates/bug-mode/SKILL.md +79 -20
  181. package/skills-templates/bug-planning/SKILL.md +25 -29
  182. package/skills-templates/chore-mode/SKILL.md +171 -69
  183. package/skills-templates/chore-mode/verification.js +51 -10
  184. package/skills-templates/chore-planning/SKILL.md +47 -18
  185. package/skills-templates/design-system-selection/SKILL.md +273 -0
  186. package/skills-templates/epic-planning/SKILL.md +82 -48
  187. package/skills-templates/external-transition/SKILL.md +47 -47
  188. package/skills-templates/feature-planning/SKILL.md +173 -74
  189. package/skills-templates/production-mode/SKILL.md +69 -49
  190. package/skills-templates/request-routing/SKILL.md +4 -4
  191. package/skills-templates/simple-improvement/SKILL.md +74 -29
  192. package/skills-templates/speed-mode/SKILL.md +217 -141
  193. package/skills-templates/stable-mode/SKILL.md +148 -89
  194. package/apps/dashboard/README.md +0 -36
  195. package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -386
  196. package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
  197. package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -167
  198. package/apps/dashboard/app/api/claude/sessions/[sessionId]/content/route.ts +0 -52
  199. package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +0 -378
  200. package/apps/dashboard/app/api/claude/sessions/[sessionId]/pin/route.ts +0 -24
  201. package/apps/dashboard/app/api/claude/sessions/cleanup/route.ts +0 -34
  202. package/apps/dashboard/app/api/claude/sessions/route.ts +0 -184
  203. package/apps/dashboard/app/api/decisions/[id]/route.ts +0 -25
  204. package/apps/dashboard/app/api/internal/set-project/route.ts +0 -17
  205. package/apps/dashboard/app/api/kanban/route.ts +0 -15
  206. package/apps/dashboard/app/api/settings/env-vars/route.ts +0 -125
  207. package/apps/dashboard/app/api/settings/general/route.ts +0 -21
  208. package/apps/dashboard/app/api/tests/route.ts +0 -9
  209. package/apps/dashboard/app/api/tests/run/route.ts +0 -82
  210. package/apps/dashboard/app/api/tests/run/stream/route.ts +0 -71
  211. package/apps/dashboard/app/api/tests/undefined/route.ts +0 -9
  212. package/apps/dashboard/app/api/usage/route.ts +0 -17
  213. package/apps/dashboard/app/api/work/[id]/description/route.ts +0 -21
  214. package/apps/dashboard/app/api/work/[id]/epic/route.ts +0 -21
  215. package/apps/dashboard/app/api/work/[id]/order/route.ts +0 -21
  216. package/apps/dashboard/app/api/work/[id]/status/route.ts +0 -21
  217. package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
  218. package/apps/dashboard/app/layout.tsx +0 -43
  219. package/apps/dashboard/components/UpgradeBanner.tsx +0 -29
  220. package/apps/dashboard/electron/ipc-handlers.js +0 -1028
  221. package/apps/dashboard/electron/main.js +0 -2124
  222. package/apps/dashboard/electron/preload.js +0 -123
  223. package/apps/dashboard/electron/session-manager.js +0 -141
  224. package/apps/dashboard/electron-builder.config.js +0 -357
  225. package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
  226. package/apps/dashboard/lib/claude-process-manager.ts +0 -492
  227. package/apps/dashboard/lib/db-bridge.ts +0 -282
  228. package/apps/dashboard/lib/prototypes.ts +0 -202
  229. package/apps/dashboard/lib/test-results-db.ts +0 -307
  230. package/apps/dashboard/lib/tests.ts +0 -282
  231. package/apps/dashboard/next.config.js +0 -50
  232. package/apps/dashboard/postcss.config.mjs +0 -7
  233. package/apps/dashboard/public/file.svg +0 -1
  234. package/apps/dashboard/public/globe.svg +0 -1
  235. package/apps/dashboard/public/next.svg +0 -1
  236. package/apps/dashboard/public/vercel.svg +0 -1
  237. package/apps/dashboard/public/window.svg +0 -1
  238. package/apps/dashboard/scripts/download-node.js +0 -104
  239. package/apps/dashboard/scripts/upload-to-r2.js +0 -89
  240. package/docs/bdd-guidance.md +0 -390
@@ -0,0 +1,468 @@
1
+ import { useState, memo } from 'react';
2
+ import { lazy, Suspense } from 'react';
3
+ import type { ClaudeMessage, StreamStatus } from '../lib/session-stream-manager';
4
+ import { Button } from '@/components/ui/Button';
5
+ import { claudeCode } from '@/lib/tauri-bridge';
6
+
7
+ const LazyMarkdown = lazy(() => import('./LazyMarkdown'));
8
+
9
+ // Tool name → human-friendly verb mapping for activity indicator
10
+ export const TOOL_VERBS: Record<string, string> = {
11
+ Read: 'Reading',
12
+ Grep: 'Searching for',
13
+ Glob: 'Finding files matching',
14
+ Bash: 'Running',
15
+ Edit: 'Editing',
16
+ Write: 'Writing',
17
+ Task: 'Delegating',
18
+ WebFetch: 'Fetching',
19
+ WebSearch: 'Searching web for',
20
+ };
21
+
22
+ function extractFilename(path: string): string {
23
+ return path.split('/').pop() || path;
24
+ }
25
+
26
+ export function humanizeToolCall(toolName: string, param: string): string {
27
+ const verb = TOOL_VERBS[toolName] || toolName;
28
+ if (['Read', 'Edit', 'Write'].includes(toolName)) {
29
+ return `${verb} ${extractFilename(param)}...`;
30
+ }
31
+ if (toolName === 'Bash') {
32
+ const short = param.length > 40 ? param.slice(0, 40) : param;
33
+ return `${verb} ${short}...`;
34
+ }
35
+ if (['Grep', 'Glob', 'WebSearch'].includes(toolName)) {
36
+ const short = param.length > 30 ? param.slice(0, 30) : param;
37
+ return `${verb} ${short}...`;
38
+ }
39
+ return `${verb} ${param}...`;
40
+ }
41
+
42
+ // Unescape content that may have literal \n, \t, \r from JSON stringification
43
+ export function unescapeContent(content: string | undefined): string {
44
+ if (!content) return '';
45
+ return content
46
+ .replace(/\\n/g, '\n')
47
+ .replace(/\\t/g, '\t')
48
+ .replace(/\\r/g, '\r')
49
+ .replace(/\\"/g, '"');
50
+ }
51
+
52
+ // Detect if error message is about Claude CLI needing an update
53
+ function isVersionUpdateError(content: string | undefined): boolean {
54
+ if (!content) return false;
55
+ return content.includes('needs an update') ||
56
+ content.includes('version') && content.includes('required');
57
+ }
58
+
59
+ // Collapse repeated phrases in tool output (e.g. repeated warnings, stack traces)
60
+ // Finds substantial phrases (50+ chars) appearing 3+ times and shows each once with a count
61
+ function deduplicateToolOutput(text: string): string {
62
+ // Split on sentence/line boundaries to extract candidate phrases
63
+ const phrases = text.split(/(?<=[\.\n])\s*/);
64
+ const counts = new Map<string, number>();
65
+
66
+ for (const phrase of phrases) {
67
+ const key = phrase.trim();
68
+ if (key.length >= 50) {
69
+ counts.set(key, (counts.get(key) || 0) + 1);
70
+ }
71
+ }
72
+
73
+ // Get repeated phrases, longest first to avoid partial match issues
74
+ const repeated = [...counts.entries()]
75
+ .filter(([, c]) => c >= 3)
76
+ .sort((a, b) => b[0].length - a[0].length);
77
+
78
+ if (repeated.length === 0) return text;
79
+
80
+ let result = text;
81
+ for (const [phrase, count] of repeated) {
82
+ const escaped = phrase.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
83
+ let idx = 0;
84
+ result = result.replace(new RegExp(escaped, 'g'), () => {
85
+ idx++;
86
+ return idx === 1 ? `${phrase} [×${count}]` : '';
87
+ });
88
+ }
89
+
90
+ // Clean up artifacts from removal
91
+ result = result.replace(/\n{3,}/g, '\n');
92
+ result = result.replace(/ {2,}/g, ' ');
93
+
94
+ return result;
95
+ }
96
+
97
+ // Noise patterns - truly internal/system content that users shouldn't see
98
+ const NOISE_PATTERNS = [
99
+ // Skill headers and metadata (internal prompt injections)
100
+ 'Base directory for this skill:',
101
+ '# Request Routing Skill',
102
+ '# Simple Improvement Skill',
103
+ '# Bug Planning Skill',
104
+ '# Chore Planning Skill',
105
+ '# Feature Planning Skill',
106
+ '# Epic Planning Skill',
107
+ '# Bug Mode Skill',
108
+ '# Chore Mode Skill',
109
+ '# Speed Mode Skill',
110
+ '# Stable Mode Skill',
111
+ '# Production Mode Skill',
112
+ 'FORBIDDEN during this skill',
113
+ 'ALLOWED during this skill',
114
+ 'ARGUMENTS:',
115
+ // System/context tags
116
+ '<system-reminder>',
117
+ '</system-reminder>',
118
+ '<claude_context',
119
+ '</claude_context>',
120
+ '<jettypod_essentials>',
121
+ '<communication_style>',
122
+ // File content dumps (usually from Read tool)
123
+ 'Contents of /',
124
+ 'File: /',
125
+ // Internal skill invocation phrases (Claude talking to system, not user)
126
+ 'Let me invoke',
127
+ 'I\'ll invoke',
128
+ 'I will invoke',
129
+ 'I need to invoke',
130
+ 'I should invoke',
131
+ 'invoke request-routing',
132
+ 'invoke bug-planning',
133
+ 'invoke chore-planning',
134
+ 'invoke feature-planning',
135
+ 'invoke epic-planning',
136
+ 'invoke simple-improvement',
137
+ 'invoke bug-mode',
138
+ 'invoke chore-mode',
139
+ 'invoke speed-mode',
140
+ 'invoke stable-mode',
141
+ 'invoke production-mode',
142
+ 'Launching skill:',
143
+ 'Invoking skill:',
144
+ // Routing decision arrows (internal logging)
145
+ '→ bug-planning',
146
+ '→ chore-planning',
147
+ '→ feature-planning',
148
+ '→ epic-planning',
149
+ '→ simple-improvement',
150
+ '→ bug-mode',
151
+ '→ chore-mode',
152
+ '→ speed-mode',
153
+ '→ stable-mode',
154
+ // Claude CLI initialization metadata
155
+ '"apiKeySource"',
156
+ '"claude_code_version"',
157
+ '"output_style"',
158
+ '"skills":',
159
+ '"agents":',
160
+ '"plugins":',
161
+ // Tool response metadata (from Read, Glob, Grep, etc.)
162
+ '"numLines":',
163
+ '"startLine":',
164
+ '"totalLines":',
165
+ // Gate markers (already parsed by stream manager, hide raw output)
166
+ '[GATE:',
167
+ '[/GATE]',
168
+ ];
169
+
170
+ // Filter for system noise - returns true if content should be HIDDEN.
171
+ // Used for standalone messages (assistant text, unpaired tool_results).
172
+ // NOT used inside MergedToolBlock — merged blocks show raw tool output
173
+ // (file reads, grep results, etc.) which would otherwise be filtered here.
174
+ export function isSystemNoise(content: string | undefined): boolean {
175
+ if (!content) return true;
176
+
177
+ const trimmed = content.trim();
178
+
179
+ // Hide raw JSON messages (system init, tool calls, etc.)
180
+ if (trimmed.startsWith('{"') || trimmed.startsWith('[{"')) {
181
+ return true;
182
+ }
183
+
184
+ if (NOISE_PATTERNS.some(p => content.includes(p))) {
185
+ return true;
186
+ }
187
+
188
+ // Hide if it has line number prefixes (file reads): "123→" anywhere in content
189
+ // This catches file content from Read tool
190
+ if (/\d+→/.test(content)) {
191
+ return true;
192
+ }
193
+
194
+ // Hide if content ends with JSON-like tool response metadata
195
+ if (/"\w+":\s*\d+\s*\}\}\}?\s*$/.test(trimmed)) {
196
+ return true;
197
+ }
198
+
199
+ // Hide if >50% of lines start with numbers (grep/search results)
200
+ const lines = content.split('\n').filter(l => l.trim());
201
+ const numberedLines = lines.filter(l => /^\s*\d+[→|:]/.test(l));
202
+ if (lines.length > 3 && numberedLines.length / lines.length > 0.5) {
203
+ return true;
204
+ }
205
+
206
+ return false;
207
+ }
208
+
209
+ const STATUS_COLORS: Record<StreamStatus, string> = {
210
+ idle: 'bg-zinc-500',
211
+ connecting: 'bg-yellow-500 animate-pulse',
212
+ creating: 'bg-yellow-500 animate-pulse',
213
+ streaming: 'bg-[#819D9F] animate-pulse',
214
+ done: 'bg-green-500',
215
+ error: 'bg-red-500',
216
+ };
217
+
218
+ export function StatusIndicator({ status }: { status: StreamStatus }) {
219
+ const colorClass = STATUS_COLORS[status];
220
+ return <div className={`w-2 h-2 rounded-full ${colorClass}`} />;
221
+ }
222
+
223
+ export function ErrorIcon() {
224
+ return (
225
+ <svg className="w-3.5 h-3.5 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
226
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
227
+ </svg>
228
+ );
229
+ }
230
+
231
+ export function UserIcon() {
232
+ return (
233
+ <svg className="w-3.5 h-3.5 text-[#819D9F]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
234
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
235
+ </svg>
236
+ );
237
+ }
238
+
239
+ function UpdateClaudeButton() {
240
+ const [isUpdating, setIsUpdating] = useState(false);
241
+ const [updateResult, setUpdateResult] = useState<{ success: boolean; error?: string } | null>(null);
242
+
243
+ const handleUpdate = async () => {
244
+ setIsUpdating(true);
245
+ setUpdateResult(null);
246
+
247
+ try {
248
+ const result = await claudeCode.update();
249
+ setUpdateResult(result);
250
+ if (result.success) {
251
+ // Reload after successful update
252
+ setTimeout(() => window.location.reload(), 1500);
253
+ }
254
+ } catch (err) {
255
+ setUpdateResult({ success: false, error: String(err) });
256
+ } finally {
257
+ setIsUpdating(false);
258
+ }
259
+ };
260
+
261
+ if (updateResult?.success) {
262
+ return (
263
+ <div className="mt-2 text-base text-green-600" data-testid="update-success">
264
+ Update successful! Reloading...
265
+ </div>
266
+ );
267
+ }
268
+
269
+ return (
270
+ <div className="mt-2">
271
+ <Button
272
+ onClick={handleUpdate}
273
+ disabled={isUpdating}
274
+ variant="destructive"
275
+ size="sm"
276
+ loading={isUpdating}
277
+ data-testid="update-claude-button"
278
+ >
279
+ {isUpdating ? 'Updating...' : 'Update Claude'}
280
+ </Button>
281
+ {updateResult?.error && (
282
+ <p className="mt-1 text-base text-red-500">{updateResult.error}</p>
283
+ )}
284
+ </div>
285
+ );
286
+ }
287
+
288
+ export const MessageBlock = memo(function MessageBlock({ message }: { message: ClaudeMessage }) {
289
+ if (message.type === 'user') {
290
+ return (
291
+ <div className="bg-[#e8f0f0] border-2 border-[#819D9F]/30 rounded-lg p-4 ml-8" data-testid="user-message">
292
+ <div className="flex items-center gap-3 mb-1.5">
293
+ <UserIcon />
294
+ <span className="text-base font-medium text-[#5a7d7f]">You</span>
295
+ </div>
296
+ <div className="text-base text-zinc-900 [&_p]:my-1 [&_h1]:text-lg [&_h1]:font-bold [&_h1]:my-2 [&_h2]:text-base [&_h2]:font-semibold [&_h2]:my-2 [&_h3]:font-semibold [&_h3]:my-1 [&_pre]:bg-[#d8e8e8] [&_pre]:p-2 [&_pre]:rounded [&_pre]:overflow-x-auto [&_pre]:whitespace-pre-wrap [&_pre]:break-words [&_pre]:my-2 [&_code]:text-[#5a7d7f] [&_code]:bg-[#d8e8e8] [&_code]:px-1 [&_code]:rounded [&_pre_code]:bg-transparent [&_pre_code]:p-0 [&_ul]:list-disc [&_ul]:ml-4 [&_ol]:list-decimal [&_ol]:ml-4 [&_li]:my-0.5 [&_a]:text-[#5a7d7f] [&_a]:underline [&_blockquote]:border-l-2 [&_blockquote]:border-[#819D9F] [&_blockquote]:pl-3 [&_blockquote]:italic">
297
+ <LazyMarkdown>{unescapeContent(message.content)}</LazyMarkdown>
298
+ </div>
299
+ </div>
300
+ );
301
+ }
302
+
303
+ if (message.type === 'assistant' || message.type === 'text') {
304
+ // Aggressive filtering: hide everything that's not genuine Claude conversation
305
+ if (isSystemNoise(message.content)) {
306
+ return null;
307
+ }
308
+
309
+ const displayContent = message.content;
310
+ if (!displayContent) {
311
+ return null;
312
+ }
313
+
314
+ return (
315
+ <div className="bg-zinc-50 rounded-lg p-4" data-testid="output-block">
316
+ <div className="text-zinc-700 text-base [&_p]:my-1 [&_h1]:text-lg [&_h1]:font-bold [&_h1]:my-2 [&_h2]:text-base [&_h2]:font-semibold [&_h2]:my-2 [&_h3]:font-semibold [&_h3]:my-1 [&_pre]:bg-zinc-100 [&_pre]:p-2 [&_pre]:rounded [&_pre]:overflow-x-auto [&_pre]:whitespace-pre-wrap [&_pre]:break-words [&_pre]:my-2 [&_pre]:text-xs [&_code]:text-zinc-600 [&_code]:bg-zinc-100 [&_code]:px-1 [&_code]:rounded [&_pre_code]:bg-transparent [&_pre_code]:p-0 [&_ul]:list-disc [&_ul]:ml-4 [&_ol]:list-decimal [&_ol]:ml-4 [&_li]:my-0.5 [&_a]:text-[#5a7d7f] [&_a]:underline [&_blockquote]:border-l-2 [&_blockquote]:border-zinc-400 [&_blockquote]:pl-3 [&_blockquote]:italic [&_table]:text-xs [&_table]:w-full [&_th]:bg-zinc-100 [&_th]:px-2 [&_th]:py-1 [&_th]:text-left [&_td]:px-2 [&_td]:py-1 [&_td]:border-t [&_td]:border-zinc-200">
317
+ <LazyMarkdown>{unescapeContent(displayContent)}</LazyMarkdown>
318
+ </div>
319
+ </div>
320
+ );
321
+ }
322
+
323
+ if (message.type === 'tool_use') {
324
+ // In detail mode, tool_use is rendered via MergedToolBlock from ClaudePanel.
325
+ // This fallback renders in summary/raw modes or when not paired.
326
+ const firstParamValue = message.tool_input ? Object.values(message.tool_input)[0] : null;
327
+ const displayValue = typeof firstParamValue === 'string'
328
+ ? (firstParamValue.length > 50 ? firstParamValue.slice(0, 50) + '...' : firstParamValue)
329
+ : null;
330
+
331
+ return (
332
+ <div className="flex items-center gap-3 py-1.5" data-testid="tool-call">
333
+ <span className="bg-zinc-200 text-zinc-700 px-3 py-1 rounded text-xs">{message.tool_name}</span>
334
+ {displayValue && <span className="text-xs text-zinc-500 truncate">{displayValue}</span>}
335
+ </div>
336
+ );
337
+ }
338
+
339
+ // Standalone tool_result fallback (unpaired results not consumed by MergedToolBlock).
340
+ // Noise filtering applies here — line-numbered file reads, grep output, etc. get hidden
341
+ // because there's no tool_use header to give them context. In detail mode, these are
342
+ // paired into MergedToolBlock which shows them with the tool name for context.
343
+ if (message.type === 'tool_result') {
344
+ const result = message.result || '';
345
+
346
+ if (isSystemNoise(result)) {
347
+ return null;
348
+ }
349
+
350
+ const deduped = deduplicateToolOutput(result);
351
+ const isLong = deduped.length > 200;
352
+ const preview = isLong ? deduped.slice(0, 200) + '...' : deduped;
353
+
354
+ return (
355
+ <details className="bg-zinc-100 rounded-lg text-xs group" data-testid="tool-result">
356
+ <summary className="px-4 py-3 cursor-pointer text-zinc-500 hover:text-zinc-700 flex items-center gap-3 list-none">
357
+ <svg className="w-3 h-3 transition-transform duration-200 ease-out group-open:rotate-90" fill="none" stroke="currentColor" viewBox="0 0 24 24">
358
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
359
+ </svg>
360
+ <span className="font-medium">Tool result</span>
361
+ {!isLong && <span className="text-zinc-400 truncate max-w-[200px]">{preview}</span>}
362
+ </summary>
363
+ <div className="px-4 pb-3 pt-0">
364
+ <pre className="text-zinc-600 whitespace-pre-wrap break-words overflow-x-auto max-h-[300px] overflow-y-auto">
365
+ {deduped}
366
+ </pre>
367
+ </div>
368
+ </details>
369
+ );
370
+ }
371
+
372
+ if (message.type === 'error') {
373
+ const isVersionError = isVersionUpdateError(message.content);
374
+ return (
375
+ <div className="bg-red-50 border-2 border-red-200 rounded-lg p-4">
376
+ <div className="flex items-center gap-3 mb-1.5">
377
+ <ErrorIcon />
378
+ <span className="text-xs font-medium text-red-600">Error</span>
379
+ </div>
380
+ <pre className="text-base text-red-700 whitespace-pre-wrap font-sans">{unescapeContent(message.content)}</pre>
381
+ {isVersionError && <UpdateClaudeButton />}
382
+ </div>
383
+ );
384
+ }
385
+
386
+ if (message.type === 'done') {
387
+ return null;
388
+ }
389
+
390
+ return null;
391
+ });
392
+
393
+ // Number of lines to show in collapsed tool block preview
394
+ const PREVIEW_LINES = 4;
395
+
396
+ // Merged tool block: combines tool_use + tool_result into a single expandable block.
397
+ // Claude Code-style: bold tool name, param in parens, 4-line preview, click to expand.
398
+ // Intentionally does NOT apply isSystemNoise — tool output (file content, grep results)
399
+ // is legitimate content here because it's shown with the tool name header for context.
400
+ export const MergedToolBlock = memo(function MergedToolBlock({
401
+ toolMessage,
402
+ resultMessage,
403
+ }: {
404
+ toolMessage: ClaudeMessage;
405
+ resultMessage?: ClaudeMessage;
406
+ }) {
407
+ const [expanded, setExpanded] = useState(false);
408
+
409
+ const toolName = toolMessage.tool_name || 'Tool';
410
+ const firstParamValue = toolMessage.tool_input ? Object.values(toolMessage.tool_input)[0] : null;
411
+ const displayValue = typeof firstParamValue === 'string'
412
+ ? (firstParamValue.length > 60 ? firstParamValue.slice(0, 60) + '...' : firstParamValue)
413
+ : null;
414
+
415
+ const rawResult = resultMessage?.result || '';
416
+ const result = deduplicateToolOutput(unescapeContent(rawResult));
417
+ const lines = result.split('\n');
418
+ const hasMoreLines = lines.length > PREVIEW_LINES;
419
+ const previewLines = lines.slice(0, PREVIEW_LINES).join('\n');
420
+ const remaining = lines.length - PREVIEW_LINES;
421
+
422
+ return (
423
+ <div className="bg-zinc-100 rounded-xl text-sm" data-testid="merged-tool-block">
424
+ <div
425
+ className="flex items-center gap-2.5 px-3.5 py-2.5 cursor-pointer select-none transition-[background-color] duration-200 ease-out hover:bg-zinc-200 rounded-t-xl"
426
+ onClick={() => setExpanded(prev => !prev)}
427
+ data-testid="tool-block-header"
428
+ >
429
+ <svg
430
+ className={`w-3 h-3 text-zinc-400 flex-shrink-0 transition-transform duration-200 ease-out ${expanded ? 'rotate-90' : ''}`}
431
+ fill="none"
432
+ stroke="currentColor"
433
+ viewBox="0 0 24 24"
434
+ >
435
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
436
+ </svg>
437
+ <span className="font-semibold text-zinc-900 text-sm">{toolName}</span>
438
+ {displayValue && (
439
+ <span className="text-zinc-500 text-sm truncate flex-1 min-w-0">({displayValue})</span>
440
+ )}
441
+ </div>
442
+ {result && (
443
+ <>
444
+ {expanded ? (
445
+ <div className="px-3.5 pb-3 pt-0 pl-9">
446
+ <pre className="text-zinc-600 text-xs font-mono whitespace-pre-wrap break-words overflow-x-auto max-h-[400px] overflow-y-auto leading-relaxed">
447
+ {result}
448
+ </pre>
449
+ </div>
450
+ ) : hasMoreLines ? (
451
+ <div className="px-3.5 pb-3 pt-0 pl-9">
452
+ <pre className="text-zinc-500 text-xs font-mono whitespace-pre-wrap break-words leading-relaxed">
453
+ {previewLines}
454
+ </pre>
455
+ <span className="text-zinc-400 text-xs italic">... {remaining} more line{remaining !== 1 ? 's' : ''}</span>
456
+ </div>
457
+ ) : (
458
+ <div className="px-3.5 pb-3 pt-0 pl-9">
459
+ <pre className="text-zinc-500 text-xs font-mono whitespace-pre-wrap break-words leading-relaxed">
460
+ {result}
461
+ </pre>
462
+ </div>
463
+ )}
464
+ </>
465
+ )}
466
+ </div>
467
+ );
468
+ });
@@ -1,6 +1,5 @@
1
- 'use client';
2
1
 
3
- import { motion, useReducedMotion } from 'framer-motion';
2
+ import { m, useReducedMotion } from 'framer-motion';
4
3
  import { useState, useRef } from 'react';
5
4
 
6
5
  // Mode configuration - icons, labels, colors, animations
@@ -24,7 +23,7 @@ const MODE_CONFIGS: Record<string, {
24
23
  title: 'Building the happy path',
25
24
  subtitle: 'Making it work first',
26
25
  gradient: 'linear-gradient(135deg, #fffbeb 0%, #fef3c7 40%, #fde68a 100%)',
27
- border: '1px solid #fbbf24',
26
+ border: '2px solid #fbbf24',
28
27
  iconBg: 'rgba(245, 158, 11, 0.15)',
29
28
  labelColor: '#92400e',
30
29
  titleColor: '#78350f',
@@ -38,7 +37,7 @@ const MODE_CONFIGS: Record<string, {
38
37
  title: 'Hardening with error handling',
39
38
  subtitle: 'Making it resilient',
40
39
  gradient: 'linear-gradient(135deg, #eff6ff 0%, #dbeafe 40%, #bfdbfe 100%)',
41
- border: '1px solid #60a5fa',
40
+ border: '2px solid #60a5fa',
42
41
  iconBg: 'rgba(59, 130, 246, 0.15)',
43
42
  labelColor: '#1e40af',
44
43
  titleColor: '#1e3a5f',
@@ -52,7 +51,7 @@ const MODE_CONFIGS: Record<string, {
52
51
  title: 'Final hardening & validation',
53
52
  subtitle: 'Making it bulletproof',
54
53
  gradient: 'linear-gradient(135deg, #faf5ff 0%, #f3e8ff 40%, #e9d5ff 100%)',
55
- border: '1px solid #a78bfa',
54
+ border: '2px solid #a78bfa',
56
55
  iconBg: 'rgba(139, 92, 246, 0.15)',
57
56
  labelColor: '#5b21b6',
58
57
  titleColor: '#4c1d95',
@@ -80,7 +79,7 @@ function Particle({ color, delay, left, top, size }: {
80
79
  size: number;
81
80
  }) {
82
81
  return (
83
- <motion.div
82
+ <m.div
84
83
  style={{
85
84
  position: 'absolute',
86
85
  left,
@@ -101,7 +100,7 @@ function Particle({ color, delay, left, top, size }: {
101
100
  // Light streak overlay
102
101
  function LightStreak({ color }: { color: string }) {
103
102
  return (
104
- <motion.div
103
+ <m.div
105
104
  style={{
106
105
  position: 'absolute',
107
106
  top: '-50%',
@@ -140,7 +139,7 @@ export function ModeStartCard({ gateType }: ModeStartCardProps) {
140
139
  });
141
140
 
142
141
  return (
143
- <motion.div
142
+ <m.div
144
143
  data-testid={`mode-start-card-${gateType}`}
145
144
  style={{
146
145
  position: 'relative',
@@ -177,9 +176,9 @@ export function ModeStartCard({ gateType }: ModeStartCardProps) {
177
176
  )}
178
177
 
179
178
  {/* Content */}
180
- <div style={{ display: 'flex', alignItems: 'center', gap: 14, padding: 16, position: 'relative', zIndex: 2 }}>
179
+ <div style={{ display: 'flex', alignItems: 'center', gap: 16, padding: 18, position: 'relative', zIndex: 2 }}>
181
180
  {/* Icon - drops in with spring (instant when reduced motion) */}
182
- <motion.div
181
+ <m.div
183
182
  style={{
184
183
  width: 44,
185
184
  height: 44,
@@ -200,10 +199,10 @@ export function ModeStartCard({ gateType }: ModeStartCardProps) {
200
199
  }}
201
200
  >
202
201
  {config.icon}
203
- </motion.div>
202
+ </m.div>
204
203
 
205
204
  {/* Text - sweeps in from left (instant when reduced motion) */}
206
- <motion.div
205
+ <m.div
207
206
  style={{ flex: 1 }}
208
207
  initial={prefersReducedMotion ? { opacity: 0 } : { opacity: 0, x: -16 }}
209
208
  animate={prefersReducedMotion ? { opacity: 1 } : { opacity: 1, x: 0 }}
@@ -226,21 +225,21 @@ export function ModeStartCard({ gateType }: ModeStartCardProps) {
226
225
  <div style={{
227
226
  fontSize: 15,
228
227
  fontWeight: 600,
229
- marginTop: 2,
228
+ marginTop: 3,
230
229
  color: config.titleColor,
231
230
  }}>
232
231
  {config.title}
233
232
  </div>
234
233
  <div style={{
235
234
  fontSize: 12,
236
- marginTop: 4,
235
+ marginTop: 5,
237
236
  color: config.subtitleColor,
238
237
  opacity: 0.6,
239
238
  }}>
240
239
  {config.subtitle}
241
240
  </div>
242
- </motion.div>
241
+ </m.div>
243
242
  </div>
244
- </motion.div>
243
+ </m.div>
245
244
  );
246
245
  }