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.
- package/.env +7 -0
- package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +124 -48
- package/apps/dashboard/app/api/claude/[workItemId]/route.ts +171 -58
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +161 -10
- package/apps/dashboard/app/api/tests/run/stream/route.ts +13 -1
- package/apps/dashboard/app/api/usage/route.ts +17 -0
- package/apps/dashboard/app/api/work/[id]/route.ts +35 -0
- package/apps/dashboard/app/api/work/[id]/status/route.ts +43 -1
- package/apps/dashboard/app/connect-claude/page.tsx +24 -0
- package/apps/dashboard/app/decision/[id]/page.tsx +14 -14
- package/apps/dashboard/app/demo/gates/page.tsx +42 -42
- package/apps/dashboard/app/design-system/page.tsx +868 -0
- package/apps/dashboard/app/globals.css +6 -2
- package/apps/dashboard/app/install-claude/page.tsx +9 -7
- package/apps/dashboard/app/layout.tsx +17 -5
- package/apps/dashboard/app/login/page.tsx +250 -0
- package/apps/dashboard/app/page.tsx +11 -9
- package/apps/dashboard/app/settings/page.tsx +4 -2
- package/apps/dashboard/app/signup/page.tsx +245 -0
- package/apps/dashboard/app/subscribe/page.tsx +11 -0
- package/apps/dashboard/app/welcome/page.tsx +24 -1
- package/apps/dashboard/app/work/[id]/page.tsx +34 -50
- package/apps/dashboard/components/AppShell.tsx +95 -55
- package/apps/dashboard/components/CardMenu.tsx +56 -13
- package/apps/dashboard/components/ClaudePanel.tsx +301 -582
- package/apps/dashboard/components/ClaudePanelInput.tsx +23 -14
- package/apps/dashboard/components/ConnectClaudeScreen.tsx +210 -0
- package/apps/dashboard/components/CopyableId.tsx +3 -3
- package/apps/dashboard/components/DetailReviewActions.tsx +109 -0
- package/apps/dashboard/components/DragContext.tsx +75 -65
- package/apps/dashboard/components/DraggableCard.tsx +6 -46
- package/apps/dashboard/components/DropZone.tsx +2 -2
- package/apps/dashboard/components/EditableDetailDescription.tsx +1 -1
- package/apps/dashboard/components/EditableTitle.tsx +26 -6
- package/apps/dashboard/components/ElapsedTimer.tsx +54 -0
- package/apps/dashboard/components/EpicGroup.tsx +329 -0
- package/apps/dashboard/components/GateCard.tsx +100 -16
- package/apps/dashboard/components/GateChoiceCard.tsx +15 -17
- package/apps/dashboard/components/InstallClaudeScreen.tsx +140 -51
- package/apps/dashboard/components/JettyLoader.tsx +38 -0
- package/apps/dashboard/components/KanbanBoard.tsx +147 -766
- package/apps/dashboard/components/KanbanCard.tsx +506 -0
- package/apps/dashboard/components/LazyMarkdown.tsx +12 -0
- package/apps/dashboard/components/MainNav.tsx +20 -54
- package/apps/dashboard/components/MessageBlock.tsx +391 -0
- package/apps/dashboard/components/ModeStartCard.tsx +15 -15
- package/apps/dashboard/components/OnboardingWelcome.tsx +214 -0
- package/apps/dashboard/components/PlaceholderCard.tsx +11 -21
- package/apps/dashboard/components/ProjectSwitcher.tsx +36 -8
- package/apps/dashboard/components/PrototypeTimeline.tsx +25 -25
- package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +265 -301
- package/apps/dashboard/components/RealTimeTestsWrapper.tsx +97 -74
- package/apps/dashboard/components/ReviewFooter.tsx +141 -0
- package/apps/dashboard/components/SessionList.tsx +19 -18
- package/apps/dashboard/components/SubscribeContent.tsx +206 -0
- package/apps/dashboard/components/TestTree.tsx +15 -14
- package/apps/dashboard/components/TipCard.tsx +177 -0
- package/apps/dashboard/components/Toast.tsx +5 -5
- package/apps/dashboard/components/TypeIcon.tsx +56 -0
- package/apps/dashboard/components/UpgradeBanner.tsx +30 -0
- package/apps/dashboard/components/WaveCompletionAnimation.tsx +61 -62
- package/apps/dashboard/components/WelcomeScreen.tsx +25 -27
- package/apps/dashboard/components/WorkItemHeader.tsx +4 -4
- package/apps/dashboard/components/WorkItemTree.tsx +9 -28
- package/apps/dashboard/components/settings/AccountSection.tsx +169 -0
- package/apps/dashboard/components/settings/EnvVarsSection.tsx +54 -79
- package/apps/dashboard/components/settings/GeneralSection.tsx +26 -31
- package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -4
- package/apps/dashboard/components/ui/Button.tsx +104 -0
- package/apps/dashboard/components/ui/Input.tsx +78 -0
- package/apps/dashboard/contexts/ClaudeSessionContext.tsx +408 -105
- package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -4
- package/apps/dashboard/contexts/UsageContext.tsx +155 -0
- package/apps/dashboard/contexts/usageHelpers.js +9 -0
- package/apps/dashboard/electron/ipc-handlers.js +281 -88
- package/apps/dashboard/electron/main.js +691 -131
- package/apps/dashboard/electron/preload.js +25 -4
- package/apps/dashboard/electron/session-manager.js +163 -0
- package/apps/dashboard/electron-builder.config.js +3 -5
- package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
- package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
- package/apps/dashboard/lib/backlog-parser.ts +50 -0
- package/apps/dashboard/lib/claude-process-manager.ts +50 -11
- package/apps/dashboard/lib/constants.ts +43 -0
- package/apps/dashboard/lib/db-bridge.ts +33 -0
- package/apps/dashboard/lib/db.ts +136 -20
- package/apps/dashboard/lib/kanban-utils.ts +70 -0
- package/apps/dashboard/lib/run-migrations.js +27 -2
- package/apps/dashboard/lib/session-state-machine.ts +3 -0
- package/apps/dashboard/lib/session-stream-manager.ts +144 -38
- package/apps/dashboard/lib/shadows.ts +7 -0
- package/apps/dashboard/lib/tests.ts +3 -1
- package/apps/dashboard/lib/utils.ts +6 -0
- package/apps/dashboard/next.config.js +35 -14
- package/apps/dashboard/package.json +6 -3
- package/apps/dashboard/public/bug-icon.svg +9 -0
- package/apps/dashboard/public/buoy-icon.svg +9 -0
- package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
- package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
- package/apps/dashboard/public/in-flight-seagull.svg +9 -0
- package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
- package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
- package/apps/dashboard/public/jettypod_logo.png +0 -0
- package/apps/dashboard/public/pier-icon.svg +14 -0
- package/apps/dashboard/public/star-icon.svg +9 -0
- package/apps/dashboard/public/wrench-icon.svg +9 -0
- package/apps/dashboard/scripts/upload-to-r2.js +89 -0
- package/apps/dashboard/scripts/ws-server.js +191 -0
- package/apps/dashboard/tsconfig.tsbuildinfo +1 -0
- package/apps/update-server/package.json +16 -0
- package/apps/update-server/schema.sql +31 -0
- package/apps/update-server/src/index.ts +1085 -0
- package/apps/update-server/tsconfig.json +16 -0
- package/apps/update-server/wrangler.toml +35 -0
- package/cucumber.js +9 -3
- package/docs/COMMAND_REFERENCE.md +34 -0
- package/hooks/post-checkout +32 -75
- package/hooks/post-merge +111 -10
- package/jest.setup.js +1 -0
- package/jettypod.js +54 -116
- package/lib/chore-taxonomy.js +33 -10
- package/lib/database.js +36 -16
- package/lib/db-watcher.js +1 -1
- package/lib/git-hooks/pre-commit +1 -1
- package/lib/jettypod-backup.js +27 -4
- package/lib/migrations/027-plan-at-creation-column.js +33 -0
- package/lib/migrations/028-ready-for-review-column.js +27 -0
- package/lib/migrations/029-remove-autoincrement.js +307 -0
- package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
- package/lib/migrations/index.js +47 -4
- package/lib/schema.js +13 -6
- package/lib/seed-onboarding.js +101 -69
- package/lib/update-command/index.js +9 -175
- package/lib/work-commands/index.js +129 -16
- package/lib/work-tracking/index.js +86 -46
- package/lib/worktree-diagnostics.js +16 -16
- package/lib/worktree-facade.js +1 -1
- package/lib/worktree-manager.js +8 -8
- package/lib/worktree-reconciler.js +5 -5
- package/package.json +9 -2
- package/scripts/ndjson-to-cucumber-json.js +152 -0
- package/scripts/postinstall.js +25 -0
- package/skills-templates/bug-mode/SKILL.md +39 -28
- package/skills-templates/bug-planning/SKILL.md +25 -29
- package/skills-templates/chore-mode/SKILL.md +131 -68
- package/skills-templates/chore-mode/verification.js +51 -10
- package/skills-templates/chore-planning/SKILL.md +47 -18
- package/skills-templates/epic-planning/SKILL.md +68 -48
- package/skills-templates/external-transition/SKILL.md +47 -47
- package/skills-templates/feature-planning/SKILL.md +83 -73
- package/skills-templates/production-mode/SKILL.md +49 -49
- package/skills-templates/request-routing/SKILL.md +27 -14
- package/skills-templates/simple-improvement/SKILL.md +68 -44
- package/skills-templates/speed-mode/SKILL.md +209 -128
- package/skills-templates/stable-mode/SKILL.md +105 -94
- package/templates/bdd-guidance.md +139 -0
- package/templates/bdd-scaffolding/wait.js +18 -0
- package/templates/bdd-scaffolding/world.js +19 -0
- package/.jettypod-backup/work.db +0 -0
- package/apps/dashboard/app/access-code/page.tsx +0 -110
- package/lib/discovery-checkpoint.js +0 -123
- 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 {
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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:
|
|
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
|
-
<
|
|
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
|
-
</
|
|
203
|
+
</m.div>
|
|
204
204
|
|
|
205
205
|
{/* Text - sweeps in from left (instant when reduced motion) */}
|
|
206
|
-
<
|
|
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:
|
|
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:
|
|
236
|
+
marginTop: 5,
|
|
237
237
|
color: config.subtitleColor,
|
|
238
238
|
opacity: 0.6,
|
|
239
239
|
}}>
|
|
240
240
|
{config.subtitle}
|
|
241
241
|
</div>
|
|
242
|
-
</
|
|
242
|
+
</m.div>
|
|
243
243
|
</div>
|
|
244
|
-
</
|
|
244
|
+
</m.div>
|
|
245
245
|
);
|
|
246
246
|
}
|