jettypod 4.4.120 → 4.4.121

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/.env +2 -1
  2. package/Cargo.lock +6450 -0
  3. package/Cargo.toml +35 -0
  4. package/README.md +5 -1
  5. package/TAURI-MIGRATION-PLAN.md +840 -0
  6. package/apps/dashboard/app/connect-claude/page.tsx +5 -6
  7. package/apps/dashboard/app/decision/[id]/page.tsx +54 -49
  8. package/apps/dashboard/app/demo/gates/page.tsx +3 -5
  9. package/apps/dashboard/app/design-system/page.tsx +1 -1
  10. package/apps/dashboard/app/globals.css +74 -2
  11. package/apps/dashboard/app/install-claude/page.tsx +3 -5
  12. package/apps/dashboard/app/login/page.tsx +17 -20
  13. package/apps/dashboard/app/page.tsx +101 -48
  14. package/apps/dashboard/app/settings/page.tsx +60 -12
  15. package/apps/dashboard/app/signup/page.tsx +14 -17
  16. package/apps/dashboard/app/subscribe/page.tsx +0 -2
  17. package/apps/dashboard/app/tests/page.tsx +37 -4
  18. package/apps/dashboard/app/welcome/page.tsx +12 -15
  19. package/apps/dashboard/app/work/[id]/page.tsx +90 -75
  20. package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
  21. package/apps/dashboard/components/AppShell.tsx +70 -61
  22. package/apps/dashboard/components/CardMenu.tsx +0 -1
  23. package/apps/dashboard/components/ClaudePanel.tsx +541 -283
  24. package/apps/dashboard/components/ClaudePanelInput.tsx +23 -4
  25. package/apps/dashboard/components/ConnectClaudeScreen.tsx +1 -5
  26. package/apps/dashboard/components/CopyableId.tsx +1 -2
  27. package/apps/dashboard/components/DetailReviewActions.tsx +11 -20
  28. package/apps/dashboard/components/DragContext.tsx +132 -62
  29. package/apps/dashboard/components/DraggableCard.tsx +3 -5
  30. package/apps/dashboard/components/DropZone.tsx +5 -6
  31. package/apps/dashboard/components/EditableDetailDescription.tsx +6 -12
  32. package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
  33. package/apps/dashboard/components/EditableTitle.tsx +0 -1
  34. package/apps/dashboard/components/ElapsedTimer.tsx +15 -3
  35. package/apps/dashboard/components/EpicGroup.tsx +100 -70
  36. package/apps/dashboard/components/GateCard.tsx +0 -1
  37. package/apps/dashboard/components/GateChoiceCard.tsx +1 -2
  38. package/apps/dashboard/components/InstallClaudeScreen.tsx +1 -5
  39. package/apps/dashboard/components/JettyLoader.tsx +0 -1
  40. package/apps/dashboard/components/KanbanBoard.tsx +319 -173
  41. package/apps/dashboard/components/KanbanCard.tsx +341 -107
  42. package/apps/dashboard/components/LazyCard.tsx +62 -0
  43. package/apps/dashboard/components/LazyMarkdown.tsx +0 -1
  44. package/apps/dashboard/components/MainNav.tsx +24 -25
  45. package/apps/dashboard/components/MessageBlock.tsx +93 -16
  46. package/apps/dashboard/components/ModeStartCard.tsx +0 -1
  47. package/apps/dashboard/components/OnboardingWelcome.tsx +0 -1
  48. package/apps/dashboard/components/PlaceholderCard.tsx +0 -1
  49. package/apps/dashboard/components/ProjectSwitcher.tsx +20 -20
  50. package/apps/dashboard/components/PrototypeTimeline.tsx +47 -26
  51. package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +308 -223
  52. package/apps/dashboard/components/RealTimeTestsWrapper.tsx +303 -160
  53. package/apps/dashboard/components/ReviewFooter.tsx +12 -14
  54. package/apps/dashboard/components/SessionList.tsx +0 -1
  55. package/apps/dashboard/components/SubscribeContent.tsx +40 -11
  56. package/apps/dashboard/components/TestTree.tsx +1 -2
  57. package/apps/dashboard/components/TipCard.tsx +2 -4
  58. package/apps/dashboard/components/Toast.tsx +0 -1
  59. package/apps/dashboard/components/TypeIcon.tsx +7 -8
  60. package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
  61. package/apps/dashboard/components/WaveCompletionAnimation.tsx +5 -17
  62. package/apps/dashboard/components/WelcomeScreen.tsx +2 -6
  63. package/apps/dashboard/components/WorkItemHeader.tsx +0 -1
  64. package/apps/dashboard/components/WorkItemTree.tsx +2 -4
  65. package/apps/dashboard/components/settings/AccountSection.tsx +27 -13
  66. package/apps/dashboard/components/settings/AiContextSection.tsx +89 -0
  67. package/apps/dashboard/components/settings/ContextDocumentsSection.tsx +317 -0
  68. package/apps/dashboard/components/settings/EnvVarsSection.tsx +20 -73
  69. package/apps/dashboard/components/settings/GeneralSection.tsx +137 -26
  70. package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
  71. package/apps/dashboard/components/settings/SettingsLayout.tsx +0 -1
  72. package/apps/dashboard/components/ui/Button.tsx +1 -1
  73. package/apps/dashboard/components/ui/Input.tsx +1 -1
  74. package/apps/dashboard/components.json +1 -1
  75. package/apps/dashboard/contexts/ClaudeSessionContext.tsx +611 -358
  76. package/apps/dashboard/contexts/ConnectionStatusContext.tsx +0 -1
  77. package/apps/dashboard/contexts/UsageContext.tsx +62 -31
  78. package/apps/dashboard/dev.sh +35 -0
  79. package/apps/dashboard/eslint.config.mjs +9 -9
  80. package/apps/dashboard/hooks/useWebSocket.ts +138 -83
  81. package/apps/dashboard/index.html +73 -0
  82. package/apps/dashboard/lib/data-bridge.ts +722 -0
  83. package/apps/dashboard/lib/db.ts +69 -1302
  84. package/apps/dashboard/lib/environment-config.ts +173 -0
  85. package/apps/dashboard/lib/environment-verification.ts +119 -0
  86. package/apps/dashboard/lib/kanban-utils.ts +226 -26
  87. package/apps/dashboard/lib/proof-run.ts +495 -0
  88. package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
  89. package/apps/dashboard/lib/service-recovery.ts +326 -0
  90. package/apps/dashboard/lib/session-state-machine.ts +1 -0
  91. package/apps/dashboard/lib/session-state-utils.ts +0 -164
  92. package/apps/dashboard/lib/session-stream-manager.ts +253 -122
  93. package/apps/dashboard/lib/stream-manager-registry.ts +46 -6
  94. package/apps/dashboard/lib/tauri-bridge.ts +102 -0
  95. package/apps/dashboard/lib/tauri.ts +106 -0
  96. package/apps/dashboard/lib/utils.ts +3 -3
  97. package/apps/dashboard/next-env.d.ts +1 -1
  98. package/apps/dashboard/package.json +21 -33
  99. package/apps/dashboard/public/bug-icon.png +0 -0
  100. package/apps/dashboard/public/buoy-icon.png +0 -0
  101. package/apps/dashboard/public/in-flight-seagull.png +0 -0
  102. package/apps/dashboard/public/pier-icon.png +0 -0
  103. package/apps/dashboard/public/star-icon.png +0 -0
  104. package/apps/dashboard/public/wrench-icon.png +0 -0
  105. package/apps/dashboard/scripts/tauri-build.js +228 -0
  106. package/apps/dashboard/scripts/upload-tauri-to-r2.js +125 -0
  107. package/apps/dashboard/src/main.tsx +12 -0
  108. package/apps/dashboard/src/router.tsx +107 -0
  109. package/apps/dashboard/src/vite-env.d.ts +1 -0
  110. package/apps/dashboard/tsconfig.json +7 -12
  111. package/apps/dashboard/tsconfig.tsbuildinfo +1 -1
  112. package/apps/dashboard/vite.config.ts +33 -0
  113. package/apps/update-server/src/index.ts +167 -30
  114. package/claude-hooks/global-guardrails.js +14 -13
  115. package/crates/jettypod-cli/Cargo.toml +19 -0
  116. package/crates/jettypod-cli/src/commands.rs +1249 -0
  117. package/crates/jettypod-cli/src/main.rs +595 -0
  118. package/crates/jettypod-core/Cargo.toml +26 -0
  119. package/crates/jettypod-core/build.rs +98 -0
  120. package/crates/jettypod-core/migrations/V1__baseline.sql +197 -0
  121. package/crates/jettypod-core/migrations/V2__work_items_indexes.sql +6 -0
  122. package/crates/jettypod-core/migrations/V3__qa_steps.sql +2 -0
  123. package/crates/jettypod-core/src/auth.rs +294 -0
  124. package/crates/jettypod-core/src/config.rs +397 -0
  125. package/crates/jettypod-core/src/db/mod.rs +507 -0
  126. package/crates/jettypod-core/src/db/recovery.rs +114 -0
  127. package/crates/jettypod-core/src/db/startup.rs +101 -0
  128. package/crates/jettypod-core/src/db/validate.rs +149 -0
  129. package/crates/jettypod-core/src/error.rs +76 -0
  130. package/crates/jettypod-core/src/git.rs +458 -0
  131. package/crates/jettypod-core/src/lib.rs +20 -0
  132. package/crates/jettypod-core/src/sessions.rs +625 -0
  133. package/crates/jettypod-core/src/skills.rs +556 -0
  134. package/crates/jettypod-core/src/work.rs +1086 -0
  135. package/crates/jettypod-core/src/worktree.rs +628 -0
  136. package/crates/jettypod-core/src/ws.rs +767 -0
  137. package/cucumber-test.cjs +6 -0
  138. package/jettypod.js +96 -4
  139. package/lib/bdd-preflight.js +96 -0
  140. package/lib/merge-lock.js +111 -253
  141. package/lib/migrations/030-rejection-round-columns.js +54 -0
  142. package/lib/migrations/031-session-isolation-index.js +17 -0
  143. package/lib/work-commands/index.js +58 -16
  144. package/lib/work-tracking/index.js +108 -8
  145. package/package.json +1 -1
  146. package/skills-templates/bug-mode/SKILL.md +43 -1
  147. package/skills-templates/chore-mode/SKILL.md +40 -1
  148. package/skills-templates/design-system-selection/SKILL.md +273 -0
  149. package/skills-templates/epic-planning/SKILL.md +14 -0
  150. package/skills-templates/feature-planning/SKILL.md +90 -1
  151. package/skills-templates/production-mode/SKILL.md +20 -0
  152. package/skills-templates/simple-improvement/SKILL.md +39 -2
  153. package/skills-templates/speed-mode/SKILL.md +10 -15
  154. package/skills-templates/stable-mode/SKILL.md +47 -0
  155. package/apps/dashboard/README.md +0 -36
  156. package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -446
  157. package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
  158. package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -280
  159. package/apps/dashboard/app/api/claude/sessions/[sessionId]/content/route.ts +0 -52
  160. package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +0 -525
  161. package/apps/dashboard/app/api/claude/sessions/[sessionId]/pin/route.ts +0 -24
  162. package/apps/dashboard/app/api/claude/sessions/cleanup/route.ts +0 -34
  163. package/apps/dashboard/app/api/claude/sessions/route.ts +0 -184
  164. package/apps/dashboard/app/api/decisions/[id]/route.ts +0 -25
  165. package/apps/dashboard/app/api/internal/set-project/route.ts +0 -17
  166. package/apps/dashboard/app/api/kanban/route.ts +0 -15
  167. package/apps/dashboard/app/api/settings/env-vars/route.ts +0 -125
  168. package/apps/dashboard/app/api/settings/general/route.ts +0 -21
  169. package/apps/dashboard/app/api/tests/route.ts +0 -9
  170. package/apps/dashboard/app/api/tests/run/route.ts +0 -82
  171. package/apps/dashboard/app/api/tests/run/stream/route.ts +0 -71
  172. package/apps/dashboard/app/api/tests/undefined/route.ts +0 -9
  173. package/apps/dashboard/app/api/usage/route.ts +0 -17
  174. package/apps/dashboard/app/api/work/[id]/description/route.ts +0 -21
  175. package/apps/dashboard/app/api/work/[id]/epic/route.ts +0 -21
  176. package/apps/dashboard/app/api/work/[id]/order/route.ts +0 -21
  177. package/apps/dashboard/app/api/work/[id]/route.ts +0 -35
  178. package/apps/dashboard/app/api/work/[id]/status/route.ts +0 -63
  179. package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
  180. package/apps/dashboard/app/layout.tsx +0 -55
  181. package/apps/dashboard/components/UpgradeBanner.tsx +0 -30
  182. package/apps/dashboard/electron/ipc-handlers.js +0 -1026
  183. package/apps/dashboard/electron/main.js +0 -2306
  184. package/apps/dashboard/electron/preload.js +0 -125
  185. package/apps/dashboard/electron/session-manager.js +0 -163
  186. package/apps/dashboard/electron-builder.config.js +0 -357
  187. package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
  188. package/apps/dashboard/lib/backlog-parser.ts +0 -50
  189. package/apps/dashboard/lib/claude-process-manager.ts +0 -529
  190. package/apps/dashboard/lib/db-bridge.ts +0 -283
  191. package/apps/dashboard/lib/prototypes.ts +0 -202
  192. package/apps/dashboard/lib/test-results-db.ts +0 -307
  193. package/apps/dashboard/lib/tests.ts +0 -282
  194. package/apps/dashboard/next.config.js +0 -66
  195. package/apps/dashboard/postcss.config.mjs +0 -7
  196. package/apps/dashboard/public/bug-icon.svg +0 -9
  197. package/apps/dashboard/public/buoy-icon.svg +0 -9
  198. package/apps/dashboard/public/file.svg +0 -1
  199. package/apps/dashboard/public/globe.svg +0 -1
  200. package/apps/dashboard/public/in-flight-seagull.svg +0 -9
  201. package/apps/dashboard/public/next.svg +0 -1
  202. package/apps/dashboard/public/pier-icon.svg +0 -14
  203. package/apps/dashboard/public/star-icon.svg +0 -9
  204. package/apps/dashboard/public/vercel.svg +0 -1
  205. package/apps/dashboard/public/window.svg +0 -1
  206. package/apps/dashboard/public/wrench-icon.svg +0 -9
  207. package/apps/dashboard/scripts/download-node.js +0 -104
  208. package/apps/dashboard/scripts/upload-to-r2.js +0 -89
@@ -1,36 +1,36 @@
1
- 'use client';
2
-
3
- import Image from 'next/image';
4
- import Link from 'next/link';
5
- import { usePathname } from 'next/navigation';
6
- import { useSessionActions } from '../contexts/ClaudeSessionContext';
1
+ import { useCallback } from 'react';
2
+ import { Link, useLocation } from 'react-router-dom';
7
3
  import { ProjectSwitcher } from './ProjectSwitcher';
8
- import { Button } from '@/components/ui/Button';
4
+ import { prefetch } from '@/lib/data-bridge';
9
5
 
10
6
  interface MainNavProps {
11
7
  projectName: string;
12
8
  }
13
9
 
14
10
  export function MainNav({ projectName }: MainNavProps) {
15
- const pathname = usePathname();
16
- const { openSessionPanel } = useSessionActions();
17
-
11
+ const { pathname } = useLocation();
18
12
  const isBacklogActive = pathname === '/';
19
13
  const isTestsActive = pathname === '/tests';
20
14
  const isPrototypesActive = pathname === '/prototypes';
21
15
  const isSettingsActive = pathname === '/settings';
22
16
 
17
+ // Prefetch page data on hover — by the time the click fires and the component
18
+ // mounts, data is already cached and ready.
19
+ const prefetchBacklog = useCallback(() => { prefetch.backlog(); }, []);
20
+ const prefetchTests = useCallback(() => { prefetch.tests(); }, []);
21
+ const prefetchPrototypes = useCallback(() => { prefetch.prototypes(); }, []);
22
+ const prefetchSettings = useCallback(() => { prefetch.settings(); }, []);
23
+
23
24
  return (
24
25
  <header className="sticky top-0 z-10 border-b border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 flex-shrink-0">
25
26
  <div className="px-5 py-5">
26
27
  <div className="flex items-center justify-between">
27
28
  <div className="flex items-center gap-4">
28
- <Image
29
+ <img
29
30
  src="/jettypod_logo.png"
30
31
  alt="JettyPod"
31
32
  width={36}
32
33
  height={36}
33
- priority
34
34
  className="rounded-full"
35
35
  />
36
36
  <ProjectSwitcher projectName={projectName} />
@@ -40,7 +40,9 @@ export function MainNav({ projectName }: MainNavProps) {
40
40
  </span>
41
41
  ) : (
42
42
  <Link
43
- href="/"
43
+ to="/"
44
+ viewTransition
45
+ onMouseEnter={prefetchBacklog}
44
46
  className="px-3 py-1.5 text-base text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100 transition-colors duration-200 ease-out"
45
47
  >
46
48
  Backlog
@@ -52,7 +54,9 @@ export function MainNav({ projectName }: MainNavProps) {
52
54
  </span>
53
55
  ) : (
54
56
  <Link
55
- href="/tests"
57
+ to="/tests"
58
+ viewTransition
59
+ onMouseEnter={prefetchTests}
56
60
  className="px-3 py-1.5 text-base text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100 transition-colors duration-200 ease-out"
57
61
  >
58
62
  Tests
@@ -64,7 +68,9 @@ export function MainNav({ projectName }: MainNavProps) {
64
68
  </span>
65
69
  ) : (
66
70
  <Link
67
- href="/prototypes"
71
+ to="/prototypes"
72
+ viewTransition
73
+ onMouseEnter={prefetchPrototypes}
68
74
  className="px-3 py-1.5 text-base text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100 transition-colors duration-200 ease-out"
69
75
  >
70
76
  Prototypes
@@ -76,22 +82,15 @@ export function MainNav({ projectName }: MainNavProps) {
76
82
  </span>
77
83
  ) : (
78
84
  <Link
79
- href="/settings"
85
+ to="/settings"
86
+ viewTransition
87
+ onMouseEnter={prefetchSettings}
80
88
  className="px-3 py-1.5 text-base text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100 transition-colors duration-200 ease-out"
81
89
  >
82
90
  Settings
83
91
  </Link>
84
92
  )}
85
93
  </div>
86
- <div className="flex items-center">
87
- <Button
88
- onClick={openSessionPanel}
89
- size="sm"
90
- data-testid="nav-claude-sessions-button"
91
- >
92
- Claude Sessions
93
- </Button>
94
- </div>
95
94
  </div>
96
95
  </div>
97
96
  </header>
@@ -1,11 +1,10 @@
1
- 'use client';
2
-
3
1
  import { useState, memo } from 'react';
4
- import dynamic from 'next/dynamic';
2
+ import { lazy, Suspense } from 'react';
5
3
  import type { ClaudeMessage, StreamStatus } from '../lib/session-stream-manager';
6
4
  import { Button } from '@/components/ui/Button';
5
+ import { claudeCode } from '@/lib/tauri-bridge';
7
6
 
8
- const LazyMarkdown = dynamic(() => import('./LazyMarkdown'), { ssr: false });
7
+ const LazyMarkdown = lazy(() => import('./LazyMarkdown'));
9
8
 
10
9
  // Tool name → human-friendly verb mapping for activity indicator
11
10
  export const TOOL_VERBS: Record<string, string> = {
@@ -168,8 +167,10 @@ const NOISE_PATTERNS = [
168
167
  '[/GATE]',
169
168
  ];
170
169
 
171
- // Filter for system noise - returns true if content should be HIDDEN
172
- // Focus on truly internal/system content, NOT Claude's explanatory messages
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.
173
174
  export function isSystemNoise(content: string | undefined): boolean {
174
175
  if (!content) return true;
175
176
 
@@ -240,16 +241,11 @@ function UpdateClaudeButton() {
240
241
  const [updateResult, setUpdateResult] = useState<{ success: boolean; error?: string } | null>(null);
241
242
 
242
243
  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
244
  setIsUpdating(true);
249
245
  setUpdateResult(null);
250
246
 
251
247
  try {
252
- const result = await window.electronAPI.claudeCode.update();
248
+ const result = await claudeCode.update();
253
249
  setUpdateResult(result);
254
250
  if (result.success) {
255
251
  // Reload after successful update
@@ -325,6 +321,8 @@ export const MessageBlock = memo(function MessageBlock({ message }: { message: C
325
321
  }
326
322
 
327
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.
328
326
  const firstParamValue = message.tool_input ? Object.values(message.tool_input)[0] : null;
329
327
  const displayValue = typeof firstParamValue === 'string'
330
328
  ? (firstParamValue.length > 50 ? firstParamValue.slice(0, 50) + '...' : firstParamValue)
@@ -332,17 +330,19 @@ export const MessageBlock = memo(function MessageBlock({ message }: { message: C
332
330
 
333
331
  return (
334
332
  <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>}
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>}
337
335
  </div>
338
336
  );
339
337
  }
340
338
 
341
- // Show tool_result messages in collapsible format
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.
342
343
  if (message.type === 'tool_result') {
343
344
  const result = message.result || '';
344
345
 
345
- // Apply same noise filtering as assistant/text messages
346
346
  if (isSystemNoise(result)) {
347
347
  return null;
348
348
  }
@@ -389,3 +389,80 @@ export const MessageBlock = memo(function MessageBlock({ message }: { message: C
389
389
 
390
390
  return null;
391
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,4 +1,3 @@
1
- 'use client';
2
1
 
3
2
  import { m, useReducedMotion } from 'framer-motion';
4
3
  import { useState, useRef } from 'react';
@@ -1,4 +1,3 @@
1
- 'use client';
2
1
 
3
2
  import { useState, useCallback } from 'react';
4
3
  import { m, AnimatePresence, useReducedMotion } from 'framer-motion';
@@ -1,4 +1,3 @@
1
- 'use client';
2
1
 
3
2
  import { m } from 'framer-motion';
4
3
 
@@ -1,8 +1,8 @@
1
- 'use client';
2
1
 
3
2
  import { useState, useRef, useEffect } from 'react';
4
3
  import { createPortal } from 'react-dom';
5
- import type { RecentProject } from '@/lib/db-bridge';
4
+ import { isTauri, project } from '@/lib/tauri-bridge';
5
+ import type { RecentProject } from '@/lib/tauri-bridge';
6
6
 
7
7
  interface ProjectSwitcherProps {
8
8
  projectName: string;
@@ -13,21 +13,21 @@ export function ProjectSwitcher({ projectName }: ProjectSwitcherProps) {
13
13
  const [recentProjects, setRecentProjects] = useState<RecentProject[]>([]);
14
14
  const [dropdownPosition, setDropdownPosition] = useState<{ top: number; left: number } | null>(null);
15
15
  const [error, setError] = useState<string | null>(null);
16
- const [isElectron, setIsElectron] = useState(false);
16
+ const [isTauriApp, setIsTauriApp] = useState(false);
17
17
  const buttonRef = useRef<HTMLButtonElement>(null);
18
18
  const dropdownRef = useRef<HTMLDivElement>(null);
19
19
 
20
- // Detect Electron after mount to avoid hydration mismatch
20
+ // Detect Tauri after mount to avoid hydration mismatch
21
21
  useEffect(() => {
22
- setIsElectron(!!window.electronAPI?.isElectron);
22
+ setIsTauriApp(isTauri());
23
23
  }, []);
24
24
 
25
25
  // Load recent projects when dropdown opens
26
26
  useEffect(() => {
27
- if (isOpen && isElectron) {
28
- window.electronAPI!.project.getRecent().then(setRecentProjects);
27
+ if (isOpen && isTauriApp) {
28
+ project.getRecent().then(setRecentProjects);
29
29
  }
30
- }, [isOpen, isElectron]);
30
+ }, [isOpen, isTauriApp]);
31
31
 
32
32
  // Calculate dropdown position when opening
33
33
  useEffect(() => {
@@ -57,18 +57,18 @@ export function ProjectSwitcher({ projectName }: ProjectSwitcherProps) {
57
57
  }
58
58
  }, [isOpen]);
59
59
 
60
- const handleProjectClick = async (project: RecentProject) => {
60
+ const handleProjectClick = async (p: RecentProject) => {
61
61
  setError(null);
62
62
  setIsOpen(false);
63
63
  try {
64
- const result = await window.electronAPI!.project.openRecent(project.path);
64
+ const result = await project.openRecent(p.path);
65
65
  if (result.success) {
66
66
  window.location.reload();
67
67
  } else {
68
- setError(`Failed to switch to "${project.name}". The project may no longer exist.`);
68
+ setError(`Failed to switch to "${p.name}". The project may no longer exist.`);
69
69
  }
70
70
  } catch {
71
- setError(`Failed to switch to "${project.name}". The project may no longer exist.`);
71
+ setError(`Failed to switch to "${p.name}". The project may no longer exist.`);
72
72
  }
73
73
  };
74
74
 
@@ -76,7 +76,7 @@ export function ProjectSwitcher({ projectName }: ProjectSwitcherProps) {
76
76
  setError(null);
77
77
  setIsOpen(false);
78
78
  try {
79
- const result = await window.electronAPI!.project.newProject();
79
+ const result = await project.newProject();
80
80
  if (result.success) {
81
81
  window.location.reload();
82
82
  }
@@ -89,7 +89,7 @@ export function ProjectSwitcher({ projectName }: ProjectSwitcherProps) {
89
89
  setError(null);
90
90
  setIsOpen(false);
91
91
  try {
92
- const result = await window.electronAPI!.project.openDialog();
92
+ const result = await project.openDialog();
93
93
  if (result.success) {
94
94
  window.location.reload();
95
95
  }
@@ -99,8 +99,8 @@ export function ProjectSwitcher({ projectName }: ProjectSwitcherProps) {
99
99
  }
100
100
  };
101
101
 
102
- // Non-Electron: render static pill (no switching possible)
103
- if (!isElectron) {
102
+ // Non-Tauri: render static pill (no switching possible)
103
+ if (!isTauriApp) {
104
104
  return (
105
105
  <span className="px-5 py-1.5 text-base bg-zinc-100 text-zinc-600 rounded-full border-2 border-zinc-200 dark:bg-zinc-800 dark:text-zinc-400 dark:border-zinc-700">
106
106
  {projectName}
@@ -130,14 +130,14 @@ export function ProjectSwitcher({ projectName }: ProjectSwitcherProps) {
130
130
  {recentProjects
131
131
  .filter(p => p.name !== projectName)
132
132
  .slice(0, 4)
133
- .map((project) => (
133
+ .map((p) => (
134
134
  <button
135
- key={project.path}
136
- onClick={() => handleProjectClick(project)}
135
+ key={p.path}
136
+ onClick={() => handleProjectClick(p)}
137
137
  className="w-full px-4 py-3 text-left text-base text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700 flex items-center gap-3"
138
138
  >
139
139
  <span className="w-1.5 h-1.5 flex-shrink-0" />
140
- <span className="truncate">{project.name}</span>
140
+ <span className="truncate">{p.name}</span>
141
141
  </button>
142
142
  ))}
143
143
 
@@ -1,7 +1,8 @@
1
- 'use client';
2
1
 
3
2
  import { useState, useEffect } from 'react';
4
- import type { PrototypeDashboardData, Prototype } from '@/lib/prototypes';
3
+ import type { PrototypeDashboardData, Prototype } from '@/lib/db';
4
+ import { shell } from '@/lib/tauri-bridge';
5
+ import { invoke } from '@/lib/tauri';
5
6
 
6
7
  interface PrototypeCardProps {
7
8
  prototype: Prototype;
@@ -89,15 +90,6 @@ function MonthGroup({ month, prototypes, selectedId, onSelect, compact }: MonthG
89
90
  );
90
91
  }
91
92
 
92
- // Convert absolute file path to API URL
93
- // e.g., /Users/.../prototypes/feature-123/file.html -> /api/prototypes/feature-123/file.html
94
- function filePathToApiUrl(filePath: string): string {
95
- const prototypesIndex = filePath.indexOf('/prototypes/');
96
- if (prototypesIndex === -1) return filePath;
97
- const relativePath = filePath.slice(prototypesIndex + '/prototypes/'.length);
98
- return `/api/prototypes/${relativePath}`;
99
- }
100
-
101
93
  interface PreviewPanelProps {
102
94
  prototype: Prototype;
103
95
  onClose: () => void;
@@ -107,20 +99,31 @@ function PreviewPanel({ prototype, onClose }: PreviewPanelProps) {
107
99
  // Find first previewable file (HTML preferred)
108
100
  const getDefaultFile = () => prototype.files.find(f => f.type === 'html') || prototype.files[0];
109
101
  const [selectedFile, setSelectedFile] = useState(getDefaultFile);
102
+ const [fileContent, setFileContent] = useState<string | null>(null);
103
+ const [loadError, setLoadError] = useState<string | null>(null);
110
104
 
111
105
  // Reset selected file when prototype changes
112
106
  useEffect(() => {
113
107
  setSelectedFile(getDefaultFile());
114
108
  }, [prototype.id]);
115
109
 
110
+ // Load file content via Tauri IPC
111
+ useEffect(() => {
112
+ if (!selectedFile) return;
113
+ setFileContent(null);
114
+ setLoadError(null);
115
+ invoke<string>('read_prototype_file', { path: selectedFile.path })
116
+ .then(setFileContent)
117
+ .catch((err) => setLoadError(String(err)));
118
+ }, [selectedFile?.path]);
119
+
116
120
  // Check if file is previewable in iframe
117
121
  const previewableTypes = ['html', 'htm', 'txt', 'md', 'json', 'js', 'ts', 'css'];
118
122
  const isPreviewable = selectedFile && previewableTypes.includes(selectedFile.type);
119
- const previewUrl = selectedFile ? filePathToApiUrl(selectedFile.path) : null;
120
123
 
121
124
  const handleOpen = () => {
122
- if (window.electronAPI?.shell?.openPath && selectedFile) {
123
- window.electronAPI.shell.openPath(selectedFile.path);
125
+ if (selectedFile) {
126
+ shell.openPath(selectedFile.path);
124
127
  }
125
128
  };
126
129
 
@@ -173,19 +176,37 @@ function PreviewPanel({ prototype, onClose }: PreviewPanelProps) {
173
176
 
174
177
  {/* Preview content - 75% zoom using transform scale */}
175
178
  <div className="flex-1 overflow-hidden">
176
- {previewUrl && isPreviewable ? (
179
+ {loadError ? (
180
+ <div className="flex items-center justify-center h-full text-red-500">
181
+ <div className="text-center">
182
+ <p className="mb-3">Failed to load preview</p>
183
+ <p className="text-base">{loadError}</p>
184
+ </div>
185
+ </div>
186
+ ) : fileContent && isPreviewable ? (
177
187
  <div className="w-full h-full overflow-hidden">
178
- <iframe
179
- key={previewUrl}
180
- src={previewUrl}
181
- className="border-0 bg-white origin-top-left"
182
- style={{
183
- width: '133.33%',
184
- height: '133.33%',
185
- transform: 'scale(0.75)',
186
- }}
187
- title={`Preview: ${selectedFile?.name}`}
188
- />
188
+ {selectedFile && ['html', 'htm'].includes(selectedFile.type) ? (
189
+ <iframe
190
+ key={selectedFile.path}
191
+ srcDoc={fileContent}
192
+ className="border-0 bg-white origin-top-left"
193
+ style={{
194
+ width: '133.33%',
195
+ height: '133.33%',
196
+ transform: 'scale(0.75)',
197
+ }}
198
+ sandbox="allow-scripts allow-same-origin"
199
+ title={`Preview: ${selectedFile.name}`}
200
+ />
201
+ ) : (
202
+ <pre className="p-6 text-sm font-mono text-zinc-700 overflow-auto h-full whitespace-pre-wrap">
203
+ {fileContent}
204
+ </pre>
205
+ )}
206
+ </div>
207
+ ) : fileContent === null && !loadError ? (
208
+ <div className="flex items-center justify-center h-full text-zinc-400">
209
+ Loading...
189
210
  </div>
190
211
  ) : (
191
212
  <div className="flex items-center justify-center h-full text-zinc-500">