jettypod 4.4.116 → 4.4.120

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 (162) hide show
  1. package/.env +7 -0
  2. package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +124 -48
  3. package/apps/dashboard/app/api/claude/[workItemId]/route.ts +171 -58
  4. package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +161 -10
  5. package/apps/dashboard/app/api/tests/run/stream/route.ts +13 -1
  6. package/apps/dashboard/app/api/usage/route.ts +17 -0
  7. package/apps/dashboard/app/api/work/[id]/route.ts +35 -0
  8. package/apps/dashboard/app/api/work/[id]/status/route.ts +43 -1
  9. package/apps/dashboard/app/connect-claude/page.tsx +24 -0
  10. package/apps/dashboard/app/decision/[id]/page.tsx +14 -14
  11. package/apps/dashboard/app/demo/gates/page.tsx +42 -42
  12. package/apps/dashboard/app/design-system/page.tsx +868 -0
  13. package/apps/dashboard/app/globals.css +6 -2
  14. package/apps/dashboard/app/install-claude/page.tsx +9 -7
  15. package/apps/dashboard/app/layout.tsx +17 -5
  16. package/apps/dashboard/app/login/page.tsx +250 -0
  17. package/apps/dashboard/app/page.tsx +11 -9
  18. package/apps/dashboard/app/settings/page.tsx +4 -2
  19. package/apps/dashboard/app/signup/page.tsx +245 -0
  20. package/apps/dashboard/app/subscribe/page.tsx +11 -0
  21. package/apps/dashboard/app/welcome/page.tsx +24 -1
  22. package/apps/dashboard/app/work/[id]/page.tsx +34 -50
  23. package/apps/dashboard/components/AppShell.tsx +95 -55
  24. package/apps/dashboard/components/CardMenu.tsx +56 -13
  25. package/apps/dashboard/components/ClaudePanel.tsx +301 -582
  26. package/apps/dashboard/components/ClaudePanelInput.tsx +23 -14
  27. package/apps/dashboard/components/ConnectClaudeScreen.tsx +210 -0
  28. package/apps/dashboard/components/CopyableId.tsx +3 -3
  29. package/apps/dashboard/components/DetailReviewActions.tsx +109 -0
  30. package/apps/dashboard/components/DragContext.tsx +75 -65
  31. package/apps/dashboard/components/DraggableCard.tsx +6 -46
  32. package/apps/dashboard/components/DropZone.tsx +2 -2
  33. package/apps/dashboard/components/EditableDetailDescription.tsx +1 -1
  34. package/apps/dashboard/components/EditableTitle.tsx +26 -6
  35. package/apps/dashboard/components/ElapsedTimer.tsx +54 -0
  36. package/apps/dashboard/components/EpicGroup.tsx +329 -0
  37. package/apps/dashboard/components/GateCard.tsx +100 -16
  38. package/apps/dashboard/components/GateChoiceCard.tsx +15 -17
  39. package/apps/dashboard/components/InstallClaudeScreen.tsx +140 -51
  40. package/apps/dashboard/components/JettyLoader.tsx +38 -0
  41. package/apps/dashboard/components/KanbanBoard.tsx +147 -766
  42. package/apps/dashboard/components/KanbanCard.tsx +506 -0
  43. package/apps/dashboard/components/LazyMarkdown.tsx +12 -0
  44. package/apps/dashboard/components/MainNav.tsx +20 -54
  45. package/apps/dashboard/components/MessageBlock.tsx +391 -0
  46. package/apps/dashboard/components/ModeStartCard.tsx +15 -15
  47. package/apps/dashboard/components/OnboardingWelcome.tsx +214 -0
  48. package/apps/dashboard/components/PlaceholderCard.tsx +11 -21
  49. package/apps/dashboard/components/ProjectSwitcher.tsx +36 -8
  50. package/apps/dashboard/components/PrototypeTimeline.tsx +25 -25
  51. package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +265 -301
  52. package/apps/dashboard/components/RealTimeTestsWrapper.tsx +97 -74
  53. package/apps/dashboard/components/ReviewFooter.tsx +141 -0
  54. package/apps/dashboard/components/SessionList.tsx +19 -18
  55. package/apps/dashboard/components/SubscribeContent.tsx +206 -0
  56. package/apps/dashboard/components/TestTree.tsx +15 -14
  57. package/apps/dashboard/components/TipCard.tsx +177 -0
  58. package/apps/dashboard/components/Toast.tsx +5 -5
  59. package/apps/dashboard/components/TypeIcon.tsx +56 -0
  60. package/apps/dashboard/components/UpgradeBanner.tsx +30 -0
  61. package/apps/dashboard/components/WaveCompletionAnimation.tsx +61 -62
  62. package/apps/dashboard/components/WelcomeScreen.tsx +25 -27
  63. package/apps/dashboard/components/WorkItemHeader.tsx +4 -4
  64. package/apps/dashboard/components/WorkItemTree.tsx +9 -28
  65. package/apps/dashboard/components/settings/AccountSection.tsx +169 -0
  66. package/apps/dashboard/components/settings/EnvVarsSection.tsx +54 -79
  67. package/apps/dashboard/components/settings/GeneralSection.tsx +26 -31
  68. package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -4
  69. package/apps/dashboard/components/ui/Button.tsx +104 -0
  70. package/apps/dashboard/components/ui/Input.tsx +78 -0
  71. package/apps/dashboard/contexts/ClaudeSessionContext.tsx +408 -105
  72. package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -4
  73. package/apps/dashboard/contexts/UsageContext.tsx +155 -0
  74. package/apps/dashboard/contexts/usageHelpers.js +9 -0
  75. package/apps/dashboard/electron/ipc-handlers.js +281 -88
  76. package/apps/dashboard/electron/main.js +691 -131
  77. package/apps/dashboard/electron/preload.js +25 -4
  78. package/apps/dashboard/electron/session-manager.js +163 -0
  79. package/apps/dashboard/electron-builder.config.js +3 -5
  80. package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
  81. package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
  82. package/apps/dashboard/lib/backlog-parser.ts +50 -0
  83. package/apps/dashboard/lib/claude-process-manager.ts +50 -11
  84. package/apps/dashboard/lib/constants.ts +43 -0
  85. package/apps/dashboard/lib/db-bridge.ts +33 -0
  86. package/apps/dashboard/lib/db.ts +136 -20
  87. package/apps/dashboard/lib/kanban-utils.ts +70 -0
  88. package/apps/dashboard/lib/run-migrations.js +27 -2
  89. package/apps/dashboard/lib/session-state-machine.ts +3 -0
  90. package/apps/dashboard/lib/session-stream-manager.ts +144 -38
  91. package/apps/dashboard/lib/shadows.ts +7 -0
  92. package/apps/dashboard/lib/tests.ts +3 -1
  93. package/apps/dashboard/lib/utils.ts +6 -0
  94. package/apps/dashboard/next.config.js +35 -14
  95. package/apps/dashboard/package.json +6 -3
  96. package/apps/dashboard/public/bug-icon.svg +9 -0
  97. package/apps/dashboard/public/buoy-icon.svg +9 -0
  98. package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
  99. package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
  100. package/apps/dashboard/public/in-flight-seagull.svg +9 -0
  101. package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
  102. package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
  103. package/apps/dashboard/public/jettypod_logo.png +0 -0
  104. package/apps/dashboard/public/pier-icon.svg +14 -0
  105. package/apps/dashboard/public/star-icon.svg +9 -0
  106. package/apps/dashboard/public/wrench-icon.svg +9 -0
  107. package/apps/dashboard/scripts/upload-to-r2.js +89 -0
  108. package/apps/dashboard/scripts/ws-server.js +191 -0
  109. package/apps/dashboard/tsconfig.tsbuildinfo +1 -0
  110. package/apps/update-server/package.json +16 -0
  111. package/apps/update-server/schema.sql +31 -0
  112. package/apps/update-server/src/index.ts +1085 -0
  113. package/apps/update-server/tsconfig.json +16 -0
  114. package/apps/update-server/wrangler.toml +35 -0
  115. package/cucumber.js +9 -3
  116. package/docs/COMMAND_REFERENCE.md +34 -0
  117. package/hooks/post-checkout +32 -75
  118. package/hooks/post-merge +111 -10
  119. package/jest.setup.js +1 -0
  120. package/jettypod.js +54 -116
  121. package/lib/chore-taxonomy.js +33 -10
  122. package/lib/database.js +36 -16
  123. package/lib/db-watcher.js +1 -1
  124. package/lib/git-hooks/pre-commit +1 -1
  125. package/lib/jettypod-backup.js +27 -4
  126. package/lib/migrations/027-plan-at-creation-column.js +33 -0
  127. package/lib/migrations/028-ready-for-review-column.js +27 -0
  128. package/lib/migrations/029-remove-autoincrement.js +307 -0
  129. package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
  130. package/lib/migrations/index.js +47 -4
  131. package/lib/schema.js +13 -6
  132. package/lib/seed-onboarding.js +101 -69
  133. package/lib/update-command/index.js +9 -175
  134. package/lib/work-commands/index.js +129 -16
  135. package/lib/work-tracking/index.js +86 -46
  136. package/lib/worktree-diagnostics.js +16 -16
  137. package/lib/worktree-facade.js +1 -1
  138. package/lib/worktree-manager.js +8 -8
  139. package/lib/worktree-reconciler.js +5 -5
  140. package/package.json +9 -2
  141. package/scripts/ndjson-to-cucumber-json.js +152 -0
  142. package/scripts/postinstall.js +25 -0
  143. package/skills-templates/bug-mode/SKILL.md +39 -28
  144. package/skills-templates/bug-planning/SKILL.md +25 -29
  145. package/skills-templates/chore-mode/SKILL.md +131 -68
  146. package/skills-templates/chore-mode/verification.js +51 -10
  147. package/skills-templates/chore-planning/SKILL.md +47 -18
  148. package/skills-templates/epic-planning/SKILL.md +68 -48
  149. package/skills-templates/external-transition/SKILL.md +47 -47
  150. package/skills-templates/feature-planning/SKILL.md +83 -73
  151. package/skills-templates/production-mode/SKILL.md +49 -49
  152. package/skills-templates/request-routing/SKILL.md +27 -14
  153. package/skills-templates/simple-improvement/SKILL.md +68 -44
  154. package/skills-templates/speed-mode/SKILL.md +209 -128
  155. package/skills-templates/stable-mode/SKILL.md +105 -94
  156. package/templates/bdd-guidance.md +139 -0
  157. package/templates/bdd-scaffolding/wait.js +18 -0
  158. package/templates/bdd-scaffolding/world.js +19 -0
  159. package/.jettypod-backup/work.db +0 -0
  160. package/apps/dashboard/app/access-code/page.tsx +0 -110
  161. package/lib/discovery-checkpoint.js +0 -123
  162. package/skills-templates/project-discovery/SKILL.md +0 -372
@@ -0,0 +1,391 @@
1
+ 'use client';
2
+
3
+ import { useState, memo } from 'react';
4
+ import dynamic from 'next/dynamic';
5
+ import type { ClaudeMessage, StreamStatus } from '../lib/session-stream-manager';
6
+ import { Button } from '@/components/ui/Button';
7
+
8
+ const LazyMarkdown = dynamic(() => import('./LazyMarkdown'), { ssr: false });
9
+
10
+ // Tool name → human-friendly verb mapping for activity indicator
11
+ export const TOOL_VERBS: Record<string, string> = {
12
+ Read: 'Reading',
13
+ Grep: 'Searching for',
14
+ Glob: 'Finding files matching',
15
+ Bash: 'Running',
16
+ Edit: 'Editing',
17
+ Write: 'Writing',
18
+ Task: 'Delegating',
19
+ WebFetch: 'Fetching',
20
+ WebSearch: 'Searching web for',
21
+ };
22
+
23
+ function extractFilename(path: string): string {
24
+ return path.split('/').pop() || path;
25
+ }
26
+
27
+ export function humanizeToolCall(toolName: string, param: string): string {
28
+ const verb = TOOL_VERBS[toolName] || toolName;
29
+ if (['Read', 'Edit', 'Write'].includes(toolName)) {
30
+ return `${verb} ${extractFilename(param)}...`;
31
+ }
32
+ if (toolName === 'Bash') {
33
+ const short = param.length > 40 ? param.slice(0, 40) : param;
34
+ return `${verb} ${short}...`;
35
+ }
36
+ if (['Grep', 'Glob', 'WebSearch'].includes(toolName)) {
37
+ const short = param.length > 30 ? param.slice(0, 30) : param;
38
+ return `${verb} ${short}...`;
39
+ }
40
+ return `${verb} ${param}...`;
41
+ }
42
+
43
+ // Unescape content that may have literal \n, \t, \r from JSON stringification
44
+ export function unescapeContent(content: string | undefined): string {
45
+ if (!content) return '';
46
+ return content
47
+ .replace(/\\n/g, '\n')
48
+ .replace(/\\t/g, '\t')
49
+ .replace(/\\r/g, '\r')
50
+ .replace(/\\"/g, '"');
51
+ }
52
+
53
+ // Detect if error message is about Claude CLI needing an update
54
+ function isVersionUpdateError(content: string | undefined): boolean {
55
+ if (!content) return false;
56
+ return content.includes('needs an update') ||
57
+ content.includes('version') && content.includes('required');
58
+ }
59
+
60
+ // Collapse repeated phrases in tool output (e.g. repeated warnings, stack traces)
61
+ // Finds substantial phrases (50+ chars) appearing 3+ times and shows each once with a count
62
+ function deduplicateToolOutput(text: string): string {
63
+ // Split on sentence/line boundaries to extract candidate phrases
64
+ const phrases = text.split(/(?<=[\.\n])\s*/);
65
+ const counts = new Map<string, number>();
66
+
67
+ for (const phrase of phrases) {
68
+ const key = phrase.trim();
69
+ if (key.length >= 50) {
70
+ counts.set(key, (counts.get(key) || 0) + 1);
71
+ }
72
+ }
73
+
74
+ // Get repeated phrases, longest first to avoid partial match issues
75
+ const repeated = [...counts.entries()]
76
+ .filter(([, c]) => c >= 3)
77
+ .sort((a, b) => b[0].length - a[0].length);
78
+
79
+ if (repeated.length === 0) return text;
80
+
81
+ let result = text;
82
+ for (const [phrase, count] of repeated) {
83
+ const escaped = phrase.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
84
+ let idx = 0;
85
+ result = result.replace(new RegExp(escaped, 'g'), () => {
86
+ idx++;
87
+ return idx === 1 ? `${phrase} [×${count}]` : '';
88
+ });
89
+ }
90
+
91
+ // Clean up artifacts from removal
92
+ result = result.replace(/\n{3,}/g, '\n');
93
+ result = result.replace(/ {2,}/g, ' ');
94
+
95
+ return result;
96
+ }
97
+
98
+ // Noise patterns - truly internal/system content that users shouldn't see
99
+ const NOISE_PATTERNS = [
100
+ // Skill headers and metadata (internal prompt injections)
101
+ 'Base directory for this skill:',
102
+ '# Request Routing Skill',
103
+ '# Simple Improvement Skill',
104
+ '# Bug Planning Skill',
105
+ '# Chore Planning Skill',
106
+ '# Feature Planning Skill',
107
+ '# Epic Planning Skill',
108
+ '# Bug Mode Skill',
109
+ '# Chore Mode Skill',
110
+ '# Speed Mode Skill',
111
+ '# Stable Mode Skill',
112
+ '# Production Mode Skill',
113
+ 'FORBIDDEN during this skill',
114
+ 'ALLOWED during this skill',
115
+ 'ARGUMENTS:',
116
+ // System/context tags
117
+ '<system-reminder>',
118
+ '</system-reminder>',
119
+ '<claude_context',
120
+ '</claude_context>',
121
+ '<jettypod_essentials>',
122
+ '<communication_style>',
123
+ // File content dumps (usually from Read tool)
124
+ 'Contents of /',
125
+ 'File: /',
126
+ // Internal skill invocation phrases (Claude talking to system, not user)
127
+ 'Let me invoke',
128
+ 'I\'ll invoke',
129
+ 'I will invoke',
130
+ 'I need to invoke',
131
+ 'I should invoke',
132
+ 'invoke request-routing',
133
+ 'invoke bug-planning',
134
+ 'invoke chore-planning',
135
+ 'invoke feature-planning',
136
+ 'invoke epic-planning',
137
+ 'invoke simple-improvement',
138
+ 'invoke bug-mode',
139
+ 'invoke chore-mode',
140
+ 'invoke speed-mode',
141
+ 'invoke stable-mode',
142
+ 'invoke production-mode',
143
+ 'Launching skill:',
144
+ 'Invoking skill:',
145
+ // Routing decision arrows (internal logging)
146
+ '→ bug-planning',
147
+ '→ chore-planning',
148
+ '→ feature-planning',
149
+ '→ epic-planning',
150
+ '→ simple-improvement',
151
+ '→ bug-mode',
152
+ '→ chore-mode',
153
+ '→ speed-mode',
154
+ '→ stable-mode',
155
+ // Claude CLI initialization metadata
156
+ '"apiKeySource"',
157
+ '"claude_code_version"',
158
+ '"output_style"',
159
+ '"skills":',
160
+ '"agents":',
161
+ '"plugins":',
162
+ // Tool response metadata (from Read, Glob, Grep, etc.)
163
+ '"numLines":',
164
+ '"startLine":',
165
+ '"totalLines":',
166
+ // Gate markers (already parsed by stream manager, hide raw output)
167
+ '[GATE:',
168
+ '[/GATE]',
169
+ ];
170
+
171
+ // Filter for system noise - returns true if content should be HIDDEN
172
+ // Focus on truly internal/system content, NOT Claude's explanatory messages
173
+ export function isSystemNoise(content: string | undefined): boolean {
174
+ if (!content) return true;
175
+
176
+ const trimmed = content.trim();
177
+
178
+ // Hide raw JSON messages (system init, tool calls, etc.)
179
+ if (trimmed.startsWith('{"') || trimmed.startsWith('[{"')) {
180
+ return true;
181
+ }
182
+
183
+ if (NOISE_PATTERNS.some(p => content.includes(p))) {
184
+ return true;
185
+ }
186
+
187
+ // Hide if it has line number prefixes (file reads): "123→" anywhere in content
188
+ // This catches file content from Read tool
189
+ if (/\d+→/.test(content)) {
190
+ return true;
191
+ }
192
+
193
+ // Hide if content ends with JSON-like tool response metadata
194
+ if (/"\w+":\s*\d+\s*\}\}\}?\s*$/.test(trimmed)) {
195
+ return true;
196
+ }
197
+
198
+ // Hide if >50% of lines start with numbers (grep/search results)
199
+ const lines = content.split('\n').filter(l => l.trim());
200
+ const numberedLines = lines.filter(l => /^\s*\d+[→|:]/.test(l));
201
+ if (lines.length > 3 && numberedLines.length / lines.length > 0.5) {
202
+ return true;
203
+ }
204
+
205
+ return false;
206
+ }
207
+
208
+ const STATUS_COLORS: Record<StreamStatus, string> = {
209
+ idle: 'bg-zinc-500',
210
+ connecting: 'bg-yellow-500 animate-pulse',
211
+ creating: 'bg-yellow-500 animate-pulse',
212
+ streaming: 'bg-[#819D9F] animate-pulse',
213
+ done: 'bg-green-500',
214
+ error: 'bg-red-500',
215
+ };
216
+
217
+ export function StatusIndicator({ status }: { status: StreamStatus }) {
218
+ const colorClass = STATUS_COLORS[status];
219
+ return <div className={`w-2 h-2 rounded-full ${colorClass}`} />;
220
+ }
221
+
222
+ export function ErrorIcon() {
223
+ return (
224
+ <svg className="w-3.5 h-3.5 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
225
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
226
+ </svg>
227
+ );
228
+ }
229
+
230
+ export function UserIcon() {
231
+ return (
232
+ <svg className="w-3.5 h-3.5 text-[#819D9F]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
233
+ <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" />
234
+ </svg>
235
+ );
236
+ }
237
+
238
+ function UpdateClaudeButton() {
239
+ const [isUpdating, setIsUpdating] = useState(false);
240
+ const [updateResult, setUpdateResult] = useState<{ success: boolean; error?: string } | null>(null);
241
+
242
+ const handleUpdate = async () => {
243
+ if (!window.electronAPI?.claudeCode?.update) {
244
+ setUpdateResult({ success: false, error: 'Update is only available in the desktop app.' });
245
+ return;
246
+ }
247
+
248
+ setIsUpdating(true);
249
+ setUpdateResult(null);
250
+
251
+ try {
252
+ const result = await window.electronAPI.claudeCode.update();
253
+ setUpdateResult(result);
254
+ if (result.success) {
255
+ // Reload after successful update
256
+ setTimeout(() => window.location.reload(), 1500);
257
+ }
258
+ } catch (err) {
259
+ setUpdateResult({ success: false, error: String(err) });
260
+ } finally {
261
+ setIsUpdating(false);
262
+ }
263
+ };
264
+
265
+ if (updateResult?.success) {
266
+ return (
267
+ <div className="mt-2 text-base text-green-600" data-testid="update-success">
268
+ Update successful! Reloading...
269
+ </div>
270
+ );
271
+ }
272
+
273
+ return (
274
+ <div className="mt-2">
275
+ <Button
276
+ onClick={handleUpdate}
277
+ disabled={isUpdating}
278
+ variant="destructive"
279
+ size="sm"
280
+ loading={isUpdating}
281
+ data-testid="update-claude-button"
282
+ >
283
+ {isUpdating ? 'Updating...' : 'Update Claude'}
284
+ </Button>
285
+ {updateResult?.error && (
286
+ <p className="mt-1 text-base text-red-500">{updateResult.error}</p>
287
+ )}
288
+ </div>
289
+ );
290
+ }
291
+
292
+ export const MessageBlock = memo(function MessageBlock({ message }: { message: ClaudeMessage }) {
293
+ if (message.type === 'user') {
294
+ return (
295
+ <div className="bg-[#e8f0f0] border-2 border-[#819D9F]/30 rounded-lg p-4 ml-8" data-testid="user-message">
296
+ <div className="flex items-center gap-3 mb-1.5">
297
+ <UserIcon />
298
+ <span className="text-base font-medium text-[#5a7d7f]">You</span>
299
+ </div>
300
+ <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">
301
+ <LazyMarkdown>{unescapeContent(message.content)}</LazyMarkdown>
302
+ </div>
303
+ </div>
304
+ );
305
+ }
306
+
307
+ if (message.type === 'assistant' || message.type === 'text') {
308
+ // Aggressive filtering: hide everything that's not genuine Claude conversation
309
+ if (isSystemNoise(message.content)) {
310
+ return null;
311
+ }
312
+
313
+ const displayContent = message.content;
314
+ if (!displayContent) {
315
+ return null;
316
+ }
317
+
318
+ return (
319
+ <div className="bg-zinc-50 rounded-lg p-4" data-testid="output-block">
320
+ <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">
321
+ <LazyMarkdown>{unescapeContent(displayContent)}</LazyMarkdown>
322
+ </div>
323
+ </div>
324
+ );
325
+ }
326
+
327
+ if (message.type === 'tool_use') {
328
+ const firstParamValue = message.tool_input ? Object.values(message.tool_input)[0] : null;
329
+ const displayValue = typeof firstParamValue === 'string'
330
+ ? (firstParamValue.length > 50 ? firstParamValue.slice(0, 50) + '...' : firstParamValue)
331
+ : null;
332
+
333
+ return (
334
+ <div className="flex items-center gap-3 py-1.5" data-testid="tool-call">
335
+ <span className="bg-purple-100 text-purple-700 px-3 py-1 rounded text-xs">{message.tool_name}</span>
336
+ {displayValue && <span className="text-xs text-purple-500 truncate">{displayValue}</span>}
337
+ </div>
338
+ );
339
+ }
340
+
341
+ // Show tool_result messages in collapsible format
342
+ if (message.type === 'tool_result') {
343
+ const result = message.result || '';
344
+
345
+ // Apply same noise filtering as assistant/text messages
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
+ });
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { motion, useReducedMotion } from 'framer-motion';
3
+ import { m, useReducedMotion } from 'framer-motion';
4
4
  import { useState, useRef } from 'react';
5
5
 
6
6
  // Mode configuration - icons, labels, colors, animations
@@ -24,7 +24,7 @@ const MODE_CONFIGS: Record<string, {
24
24
  title: 'Building the happy path',
25
25
  subtitle: 'Making it work first',
26
26
  gradient: 'linear-gradient(135deg, #fffbeb 0%, #fef3c7 40%, #fde68a 100%)',
27
- border: '1px solid #fbbf24',
27
+ border: '2px solid #fbbf24',
28
28
  iconBg: 'rgba(245, 158, 11, 0.15)',
29
29
  labelColor: '#92400e',
30
30
  titleColor: '#78350f',
@@ -38,7 +38,7 @@ const MODE_CONFIGS: Record<string, {
38
38
  title: 'Hardening with error handling',
39
39
  subtitle: 'Making it resilient',
40
40
  gradient: 'linear-gradient(135deg, #eff6ff 0%, #dbeafe 40%, #bfdbfe 100%)',
41
- border: '1px solid #60a5fa',
41
+ border: '2px solid #60a5fa',
42
42
  iconBg: 'rgba(59, 130, 246, 0.15)',
43
43
  labelColor: '#1e40af',
44
44
  titleColor: '#1e3a5f',
@@ -52,7 +52,7 @@ const MODE_CONFIGS: Record<string, {
52
52
  title: 'Final hardening & validation',
53
53
  subtitle: 'Making it bulletproof',
54
54
  gradient: 'linear-gradient(135deg, #faf5ff 0%, #f3e8ff 40%, #e9d5ff 100%)',
55
- border: '1px solid #a78bfa',
55
+ border: '2px solid #a78bfa',
56
56
  iconBg: 'rgba(139, 92, 246, 0.15)',
57
57
  labelColor: '#5b21b6',
58
58
  titleColor: '#4c1d95',
@@ -80,7 +80,7 @@ function Particle({ color, delay, left, top, size }: {
80
80
  size: number;
81
81
  }) {
82
82
  return (
83
- <motion.div
83
+ <m.div
84
84
  style={{
85
85
  position: 'absolute',
86
86
  left,
@@ -101,7 +101,7 @@ function Particle({ color, delay, left, top, size }: {
101
101
  // Light streak overlay
102
102
  function LightStreak({ color }: { color: string }) {
103
103
  return (
104
- <motion.div
104
+ <m.div
105
105
  style={{
106
106
  position: 'absolute',
107
107
  top: '-50%',
@@ -140,7 +140,7 @@ export function ModeStartCard({ gateType }: ModeStartCardProps) {
140
140
  });
141
141
 
142
142
  return (
143
- <motion.div
143
+ <m.div
144
144
  data-testid={`mode-start-card-${gateType}`}
145
145
  style={{
146
146
  position: 'relative',
@@ -177,9 +177,9 @@ export function ModeStartCard({ gateType }: ModeStartCardProps) {
177
177
  )}
178
178
 
179
179
  {/* Content */}
180
- <div style={{ display: 'flex', alignItems: 'center', gap: 14, padding: 16, position: 'relative', zIndex: 2 }}>
180
+ <div style={{ display: 'flex', alignItems: 'center', gap: 16, padding: 18, position: 'relative', zIndex: 2 }}>
181
181
  {/* Icon - drops in with spring (instant when reduced motion) */}
182
- <motion.div
182
+ <m.div
183
183
  style={{
184
184
  width: 44,
185
185
  height: 44,
@@ -200,10 +200,10 @@ export function ModeStartCard({ gateType }: ModeStartCardProps) {
200
200
  }}
201
201
  >
202
202
  {config.icon}
203
- </motion.div>
203
+ </m.div>
204
204
 
205
205
  {/* Text - sweeps in from left (instant when reduced motion) */}
206
- <motion.div
206
+ <m.div
207
207
  style={{ flex: 1 }}
208
208
  initial={prefersReducedMotion ? { opacity: 0 } : { opacity: 0, x: -16 }}
209
209
  animate={prefersReducedMotion ? { opacity: 1 } : { opacity: 1, x: 0 }}
@@ -226,21 +226,21 @@ export function ModeStartCard({ gateType }: ModeStartCardProps) {
226
226
  <div style={{
227
227
  fontSize: 15,
228
228
  fontWeight: 600,
229
- marginTop: 2,
229
+ marginTop: 3,
230
230
  color: config.titleColor,
231
231
  }}>
232
232
  {config.title}
233
233
  </div>
234
234
  <div style={{
235
235
  fontSize: 12,
236
- marginTop: 4,
236
+ marginTop: 5,
237
237
  color: config.subtitleColor,
238
238
  opacity: 0.6,
239
239
  }}>
240
240
  {config.subtitle}
241
241
  </div>
242
- </motion.div>
242
+ </m.div>
243
243
  </div>
244
- </motion.div>
244
+ </m.div>
245
245
  );
246
246
  }