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.
- package/.env +4 -3
- package/Cargo.lock +6450 -0
- package/Cargo.toml +35 -0
- package/README.md +5 -1
- package/TAURI-MIGRATION-PLAN.md +840 -0
- package/apps/dashboard/app/connect-claude/page.tsx +5 -6
- package/apps/dashboard/app/decision/[id]/page.tsx +63 -58
- package/apps/dashboard/app/demo/gates/page.tsx +43 -45
- package/apps/dashboard/app/design-system/page.tsx +868 -0
- package/apps/dashboard/app/globals.css +80 -4
- package/apps/dashboard/app/install-claude/page.tsx +4 -6
- package/apps/dashboard/app/login/page.tsx +72 -54
- package/apps/dashboard/app/page.tsx +101 -48
- package/apps/dashboard/app/settings/page.tsx +61 -13
- package/apps/dashboard/app/signup/page.tsx +242 -0
- package/apps/dashboard/app/subscribe/page.tsx +0 -2
- package/apps/dashboard/app/tests/page.tsx +37 -4
- package/apps/dashboard/app/welcome/page.tsx +13 -16
- package/apps/dashboard/app/work/[id]/page.tsx +117 -118
- package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
- package/apps/dashboard/components/AppShell.tsx +92 -85
- package/apps/dashboard/components/CardMenu.tsx +45 -12
- package/apps/dashboard/components/ClaudePanel.tsx +771 -850
- package/apps/dashboard/components/ClaudePanelInput.tsx +43 -15
- package/apps/dashboard/components/ConnectClaudeScreen.tsx +17 -34
- package/apps/dashboard/components/CopyableId.tsx +3 -4
- package/apps/dashboard/components/DetailReviewActions.tsx +100 -0
- package/apps/dashboard/components/DragContext.tsx +134 -63
- package/apps/dashboard/components/DraggableCard.tsx +3 -5
- package/apps/dashboard/components/DropZone.tsx +6 -7
- package/apps/dashboard/components/EditableDetailDescription.tsx +7 -13
- package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
- package/apps/dashboard/components/EditableTitle.tsx +26 -7
- package/apps/dashboard/components/ElapsedTimer.tsx +66 -0
- package/apps/dashboard/components/EpicGroup.tsx +359 -0
- package/apps/dashboard/components/GateCard.tsx +79 -17
- package/apps/dashboard/components/GateChoiceCard.tsx +15 -18
- package/apps/dashboard/components/InstallClaudeScreen.tsx +15 -32
- package/apps/dashboard/components/JettyLoader.tsx +37 -0
- package/apps/dashboard/components/KanbanBoard.tsx +368 -958
- package/apps/dashboard/components/KanbanCard.tsx +740 -0
- package/apps/dashboard/components/LazyCard.tsx +62 -0
- package/apps/dashboard/components/LazyMarkdown.tsx +11 -0
- package/apps/dashboard/components/MainNav.tsx +38 -73
- package/apps/dashboard/components/MessageBlock.tsx +468 -0
- package/apps/dashboard/components/ModeStartCard.tsx +15 -16
- package/apps/dashboard/components/OnboardingWelcome.tsx +213 -0
- package/apps/dashboard/components/PlaceholderCard.tsx +3 -4
- package/apps/dashboard/components/ProjectSwitcher.tsx +30 -30
- package/apps/dashboard/components/PrototypeTimeline.tsx +72 -51
- package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +406 -388
- package/apps/dashboard/components/RealTimeTestsWrapper.tsx +373 -235
- package/apps/dashboard/components/ReviewFooter.tsx +139 -0
- package/apps/dashboard/components/SessionList.tsx +19 -19
- package/apps/dashboard/components/SubscribeContent.tsx +91 -47
- package/apps/dashboard/components/TestTree.tsx +16 -16
- package/apps/dashboard/components/TipCard.tsx +16 -17
- package/apps/dashboard/components/Toast.tsx +5 -6
- package/apps/dashboard/components/TypeIcon.tsx +55 -0
- package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
- package/apps/dashboard/components/WaveCompletionAnimation.tsx +52 -65
- package/apps/dashboard/components/WelcomeScreen.tsx +19 -35
- package/apps/dashboard/components/WorkItemHeader.tsx +4 -5
- package/apps/dashboard/components/WorkItemTree.tsx +11 -32
- package/apps/dashboard/components/settings/AccountSection.tsx +55 -35
- package/apps/dashboard/components/settings/AiContextSection.tsx +89 -0
- package/apps/dashboard/components/settings/ContextDocumentsSection.tsx +317 -0
- package/apps/dashboard/components/settings/EnvVarsSection.tsx +74 -152
- package/apps/dashboard/components/settings/GeneralSection.tsx +162 -56
- package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
- package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -5
- package/apps/dashboard/components/ui/Button.tsx +104 -0
- package/apps/dashboard/components/ui/Input.tsx +78 -0
- package/apps/dashboard/components.json +1 -1
- package/apps/dashboard/contexts/ClaudeSessionContext.tsx +711 -418
- package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -5
- package/apps/dashboard/contexts/UsageContext.tsx +87 -32
- package/apps/dashboard/dev.sh +35 -0
- package/apps/dashboard/eslint.config.mjs +9 -9
- package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
- package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
- package/apps/dashboard/hooks/useWebSocket.ts +138 -83
- package/apps/dashboard/index.html +73 -0
- package/apps/dashboard/lib/constants.ts +43 -0
- package/apps/dashboard/lib/data-bridge.ts +722 -0
- package/apps/dashboard/lib/db.ts +69 -1265
- package/apps/dashboard/lib/environment-config.ts +173 -0
- package/apps/dashboard/lib/environment-verification.ts +119 -0
- package/apps/dashboard/lib/kanban-utils.ts +270 -0
- package/apps/dashboard/lib/proof-run.ts +495 -0
- package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
- package/apps/dashboard/lib/run-migrations.js +27 -2
- package/apps/dashboard/lib/service-recovery.ts +326 -0
- package/apps/dashboard/lib/session-state-machine.ts +1 -0
- package/apps/dashboard/lib/session-state-utils.ts +0 -164
- package/apps/dashboard/lib/session-stream-manager.ts +308 -134
- package/apps/dashboard/lib/shadows.ts +7 -0
- package/apps/dashboard/lib/stream-manager-registry.ts +46 -6
- package/apps/dashboard/lib/tauri-bridge.ts +102 -0
- package/apps/dashboard/lib/tauri.ts +106 -0
- package/apps/dashboard/lib/utils.ts +6 -0
- package/apps/dashboard/next-env.d.ts +1 -1
- package/apps/dashboard/package.json +21 -32
- package/apps/dashboard/public/bug-icon.png +0 -0
- package/apps/dashboard/public/buoy-icon.png +0 -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.png +0 -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.png +0 -0
- package/apps/dashboard/public/star-icon.png +0 -0
- package/apps/dashboard/public/wrench-icon.png +0 -0
- package/apps/dashboard/scripts/tauri-build.js +228 -0
- package/apps/dashboard/scripts/upload-tauri-to-r2.js +125 -0
- package/apps/dashboard/scripts/ws-server.js +191 -0
- package/apps/dashboard/src/main.tsx +12 -0
- package/apps/dashboard/src/router.tsx +107 -0
- package/apps/dashboard/src/vite-env.d.ts +1 -0
- package/apps/dashboard/tsconfig.json +7 -12
- package/apps/dashboard/tsconfig.tsbuildinfo +1 -1
- package/apps/dashboard/vite.config.ts +33 -0
- package/apps/update-server/src/index.ts +228 -80
- package/claude-hooks/global-guardrails.js +14 -13
- package/crates/jettypod-cli/Cargo.toml +19 -0
- package/crates/jettypod-cli/src/commands.rs +1249 -0
- package/crates/jettypod-cli/src/main.rs +595 -0
- package/crates/jettypod-core/Cargo.toml +26 -0
- package/crates/jettypod-core/build.rs +98 -0
- package/crates/jettypod-core/migrations/V1__baseline.sql +197 -0
- package/crates/jettypod-core/migrations/V2__work_items_indexes.sql +6 -0
- package/crates/jettypod-core/migrations/V3__qa_steps.sql +2 -0
- package/crates/jettypod-core/src/auth.rs +294 -0
- package/crates/jettypod-core/src/config.rs +397 -0
- package/crates/jettypod-core/src/db/mod.rs +507 -0
- package/crates/jettypod-core/src/db/recovery.rs +114 -0
- package/crates/jettypod-core/src/db/startup.rs +101 -0
- package/crates/jettypod-core/src/db/validate.rs +149 -0
- package/crates/jettypod-core/src/error.rs +76 -0
- package/crates/jettypod-core/src/git.rs +458 -0
- package/crates/jettypod-core/src/lib.rs +20 -0
- package/crates/jettypod-core/src/sessions.rs +625 -0
- package/crates/jettypod-core/src/skills.rs +556 -0
- package/crates/jettypod-core/src/work.rs +1086 -0
- package/crates/jettypod-core/src/worktree.rs +628 -0
- package/crates/jettypod-core/src/ws.rs +767 -0
- package/cucumber-test.cjs +6 -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 +145 -116
- package/lib/bdd-preflight.js +96 -0
- 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/merge-lock.js +111 -253
- package/lib/migrations/027-plan-at-creation-column.js +3 -1
- package/lib/migrations/029-remove-autoincrement.js +307 -0
- package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
- package/lib/migrations/030-rejection-round-columns.js +54 -0
- package/lib/migrations/031-session-isolation-index.js +17 -0
- package/lib/migrations/index.js +47 -4
- package/lib/schema.js +10 -5
- package/lib/seed-onboarding.js +1 -1
- package/lib/update-command/index.js +9 -175
- package/lib/work-commands/index.js +144 -19
- package/lib/work-tracking/index.js +148 -27
- 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 +79 -20
- package/skills-templates/bug-planning/SKILL.md +25 -29
- package/skills-templates/chore-mode/SKILL.md +171 -69
- package/skills-templates/chore-mode/verification.js +51 -10
- package/skills-templates/chore-planning/SKILL.md +47 -18
- package/skills-templates/design-system-selection/SKILL.md +273 -0
- package/skills-templates/epic-planning/SKILL.md +82 -48
- package/skills-templates/external-transition/SKILL.md +47 -47
- package/skills-templates/feature-planning/SKILL.md +173 -74
- package/skills-templates/production-mode/SKILL.md +69 -49
- package/skills-templates/request-routing/SKILL.md +4 -4
- package/skills-templates/simple-improvement/SKILL.md +74 -29
- package/skills-templates/speed-mode/SKILL.md +217 -141
- package/skills-templates/stable-mode/SKILL.md +148 -89
- package/apps/dashboard/README.md +0 -36
- package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -386
- package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
- package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -167
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/content/route.ts +0 -52
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +0 -378
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/pin/route.ts +0 -24
- package/apps/dashboard/app/api/claude/sessions/cleanup/route.ts +0 -34
- package/apps/dashboard/app/api/claude/sessions/route.ts +0 -184
- package/apps/dashboard/app/api/decisions/[id]/route.ts +0 -25
- package/apps/dashboard/app/api/internal/set-project/route.ts +0 -17
- package/apps/dashboard/app/api/kanban/route.ts +0 -15
- package/apps/dashboard/app/api/settings/env-vars/route.ts +0 -125
- package/apps/dashboard/app/api/settings/general/route.ts +0 -21
- package/apps/dashboard/app/api/tests/route.ts +0 -9
- package/apps/dashboard/app/api/tests/run/route.ts +0 -82
- package/apps/dashboard/app/api/tests/run/stream/route.ts +0 -71
- package/apps/dashboard/app/api/tests/undefined/route.ts +0 -9
- package/apps/dashboard/app/api/usage/route.ts +0 -17
- package/apps/dashboard/app/api/work/[id]/description/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/epic/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/order/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/status/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
- package/apps/dashboard/app/layout.tsx +0 -43
- package/apps/dashboard/components/UpgradeBanner.tsx +0 -29
- package/apps/dashboard/electron/ipc-handlers.js +0 -1028
- package/apps/dashboard/electron/main.js +0 -2124
- package/apps/dashboard/electron/preload.js +0 -123
- package/apps/dashboard/electron/session-manager.js +0 -141
- package/apps/dashboard/electron-builder.config.js +0 -357
- package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
- package/apps/dashboard/lib/claude-process-manager.ts +0 -492
- package/apps/dashboard/lib/db-bridge.ts +0 -282
- package/apps/dashboard/lib/prototypes.ts +0 -202
- package/apps/dashboard/lib/test-results-db.ts +0 -307
- package/apps/dashboard/lib/tests.ts +0 -282
- package/apps/dashboard/next.config.js +0 -50
- package/apps/dashboard/postcss.config.mjs +0 -7
- package/apps/dashboard/public/file.svg +0 -1
- package/apps/dashboard/public/globe.svg +0 -1
- package/apps/dashboard/public/next.svg +0 -1
- package/apps/dashboard/public/vercel.svg +0 -1
- package/apps/dashboard/public/window.svg +0 -1
- package/apps/dashboard/scripts/download-node.js +0 -104
- package/apps/dashboard/scripts/upload-to-r2.js +0 -89
- 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 {
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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:
|
|
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
|
-
<
|
|
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
|
-
</
|
|
202
|
+
</m.div>
|
|
204
203
|
|
|
205
204
|
{/* Text - sweeps in from left (instant when reduced motion) */}
|
|
206
|
-
<
|
|
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:
|
|
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:
|
|
235
|
+
marginTop: 5,
|
|
237
236
|
color: config.subtitleColor,
|
|
238
237
|
opacity: 0.6,
|
|
239
238
|
}}>
|
|
240
239
|
{config.subtitle}
|
|
241
240
|
</div>
|
|
242
|
-
</
|
|
241
|
+
</m.div>
|
|
243
242
|
</div>
|
|
244
|
-
</
|
|
243
|
+
</m.div>
|
|
245
244
|
);
|
|
246
245
|
}
|