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.
- package/.env +2 -1
- 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 +54 -49
- package/apps/dashboard/app/demo/gates/page.tsx +3 -5
- package/apps/dashboard/app/design-system/page.tsx +1 -1
- package/apps/dashboard/app/globals.css +74 -2
- package/apps/dashboard/app/install-claude/page.tsx +3 -5
- package/apps/dashboard/app/login/page.tsx +17 -20
- package/apps/dashboard/app/page.tsx +101 -48
- package/apps/dashboard/app/settings/page.tsx +60 -12
- package/apps/dashboard/app/signup/page.tsx +14 -17
- 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 +12 -15
- package/apps/dashboard/app/work/[id]/page.tsx +90 -75
- package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
- package/apps/dashboard/components/AppShell.tsx +70 -61
- package/apps/dashboard/components/CardMenu.tsx +0 -1
- package/apps/dashboard/components/ClaudePanel.tsx +541 -283
- package/apps/dashboard/components/ClaudePanelInput.tsx +23 -4
- package/apps/dashboard/components/ConnectClaudeScreen.tsx +1 -5
- package/apps/dashboard/components/CopyableId.tsx +1 -2
- package/apps/dashboard/components/DetailReviewActions.tsx +11 -20
- package/apps/dashboard/components/DragContext.tsx +132 -62
- package/apps/dashboard/components/DraggableCard.tsx +3 -5
- package/apps/dashboard/components/DropZone.tsx +5 -6
- package/apps/dashboard/components/EditableDetailDescription.tsx +6 -12
- package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
- package/apps/dashboard/components/EditableTitle.tsx +0 -1
- package/apps/dashboard/components/ElapsedTimer.tsx +15 -3
- package/apps/dashboard/components/EpicGroup.tsx +100 -70
- package/apps/dashboard/components/GateCard.tsx +0 -1
- package/apps/dashboard/components/GateChoiceCard.tsx +1 -2
- package/apps/dashboard/components/InstallClaudeScreen.tsx +1 -5
- package/apps/dashboard/components/JettyLoader.tsx +0 -1
- package/apps/dashboard/components/KanbanBoard.tsx +319 -173
- package/apps/dashboard/components/KanbanCard.tsx +341 -107
- package/apps/dashboard/components/LazyCard.tsx +62 -0
- package/apps/dashboard/components/LazyMarkdown.tsx +0 -1
- package/apps/dashboard/components/MainNav.tsx +24 -25
- package/apps/dashboard/components/MessageBlock.tsx +93 -16
- package/apps/dashboard/components/ModeStartCard.tsx +0 -1
- package/apps/dashboard/components/OnboardingWelcome.tsx +0 -1
- package/apps/dashboard/components/PlaceholderCard.tsx +0 -1
- package/apps/dashboard/components/ProjectSwitcher.tsx +20 -20
- package/apps/dashboard/components/PrototypeTimeline.tsx +47 -26
- package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +308 -223
- package/apps/dashboard/components/RealTimeTestsWrapper.tsx +303 -160
- package/apps/dashboard/components/ReviewFooter.tsx +12 -14
- package/apps/dashboard/components/SessionList.tsx +0 -1
- package/apps/dashboard/components/SubscribeContent.tsx +40 -11
- package/apps/dashboard/components/TestTree.tsx +1 -2
- package/apps/dashboard/components/TipCard.tsx +2 -4
- package/apps/dashboard/components/Toast.tsx +0 -1
- package/apps/dashboard/components/TypeIcon.tsx +7 -8
- package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
- package/apps/dashboard/components/WaveCompletionAnimation.tsx +5 -17
- package/apps/dashboard/components/WelcomeScreen.tsx +2 -6
- package/apps/dashboard/components/WorkItemHeader.tsx +0 -1
- package/apps/dashboard/components/WorkItemTree.tsx +2 -4
- package/apps/dashboard/components/settings/AccountSection.tsx +27 -13
- 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 +20 -73
- package/apps/dashboard/components/settings/GeneralSection.tsx +137 -26
- package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
- package/apps/dashboard/components/settings/SettingsLayout.tsx +0 -1
- package/apps/dashboard/components/ui/Button.tsx +1 -1
- package/apps/dashboard/components/ui/Input.tsx +1 -1
- package/apps/dashboard/components.json +1 -1
- package/apps/dashboard/contexts/ClaudeSessionContext.tsx +611 -358
- package/apps/dashboard/contexts/ConnectionStatusContext.tsx +0 -1
- package/apps/dashboard/contexts/UsageContext.tsx +62 -31
- package/apps/dashboard/dev.sh +35 -0
- package/apps/dashboard/eslint.config.mjs +9 -9
- package/apps/dashboard/hooks/useWebSocket.ts +138 -83
- package/apps/dashboard/index.html +73 -0
- package/apps/dashboard/lib/data-bridge.ts +722 -0
- package/apps/dashboard/lib/db.ts +69 -1302
- 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 +226 -26
- package/apps/dashboard/lib/proof-run.ts +495 -0
- package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
- 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 +253 -122
- 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 +3 -3
- package/apps/dashboard/next-env.d.ts +1 -1
- package/apps/dashboard/package.json +21 -33
- package/apps/dashboard/public/bug-icon.png +0 -0
- package/apps/dashboard/public/buoy-icon.png +0 -0
- package/apps/dashboard/public/in-flight-seagull.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/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 +167 -30
- 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/jettypod.js +96 -4
- package/lib/bdd-preflight.js +96 -0
- package/lib/merge-lock.js +111 -253
- package/lib/migrations/030-rejection-round-columns.js +54 -0
- package/lib/migrations/031-session-isolation-index.js +17 -0
- package/lib/work-commands/index.js +58 -16
- package/lib/work-tracking/index.js +108 -8
- package/package.json +1 -1
- package/skills-templates/bug-mode/SKILL.md +43 -1
- package/skills-templates/chore-mode/SKILL.md +40 -1
- package/skills-templates/design-system-selection/SKILL.md +273 -0
- package/skills-templates/epic-planning/SKILL.md +14 -0
- package/skills-templates/feature-planning/SKILL.md +90 -1
- package/skills-templates/production-mode/SKILL.md +20 -0
- package/skills-templates/simple-improvement/SKILL.md +39 -2
- package/skills-templates/speed-mode/SKILL.md +10 -15
- package/skills-templates/stable-mode/SKILL.md +47 -0
- package/apps/dashboard/README.md +0 -36
- package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -446
- package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
- package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -280
- 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 -525
- 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]/route.ts +0 -35
- package/apps/dashboard/app/api/work/[id]/status/route.ts +0 -63
- package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
- package/apps/dashboard/app/layout.tsx +0 -55
- package/apps/dashboard/components/UpgradeBanner.tsx +0 -30
- package/apps/dashboard/electron/ipc-handlers.js +0 -1026
- package/apps/dashboard/electron/main.js +0 -2306
- package/apps/dashboard/electron/preload.js +0 -125
- package/apps/dashboard/electron/session-manager.js +0 -163
- package/apps/dashboard/electron-builder.config.js +0 -357
- package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
- package/apps/dashboard/lib/backlog-parser.ts +0 -50
- package/apps/dashboard/lib/claude-process-manager.ts +0 -529
- package/apps/dashboard/lib/db-bridge.ts +0 -283
- 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 -66
- package/apps/dashboard/postcss.config.mjs +0 -7
- package/apps/dashboard/public/bug-icon.svg +0 -9
- package/apps/dashboard/public/buoy-icon.svg +0 -9
- package/apps/dashboard/public/file.svg +0 -1
- package/apps/dashboard/public/globe.svg +0 -1
- package/apps/dashboard/public/in-flight-seagull.svg +0 -9
- package/apps/dashboard/public/next.svg +0 -1
- package/apps/dashboard/public/pier-icon.svg +0 -14
- package/apps/dashboard/public/star-icon.svg +0 -9
- package/apps/dashboard/public/vercel.svg +0 -1
- package/apps/dashboard/public/window.svg +0 -1
- package/apps/dashboard/public/wrench-icon.svg +0 -9
- package/apps/dashboard/scripts/download-node.js +0 -104
- package/apps/dashboard/scripts/upload-to-r2.js +0 -89
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
|
|
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 {
|
|
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 =
|
|
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
|
-
<
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
//
|
|
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
|
|
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-
|
|
336
|
-
{displayValue && <span className="text-xs text-
|
|
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
|
-
//
|
|
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,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
|
|
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 [
|
|
16
|
+
const [isTauriApp, setIsTauriApp] = useState(false);
|
|
17
17
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
|
18
18
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
19
19
|
|
|
20
|
-
// Detect
|
|
20
|
+
// Detect Tauri after mount to avoid hydration mismatch
|
|
21
21
|
useEffect(() => {
|
|
22
|
-
|
|
22
|
+
setIsTauriApp(isTauri());
|
|
23
23
|
}, []);
|
|
24
24
|
|
|
25
25
|
// Load recent projects when dropdown opens
|
|
26
26
|
useEffect(() => {
|
|
27
|
-
if (isOpen &&
|
|
28
|
-
|
|
27
|
+
if (isOpen && isTauriApp) {
|
|
28
|
+
project.getRecent().then(setRecentProjects);
|
|
29
29
|
}
|
|
30
|
-
}, [isOpen,
|
|
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 (
|
|
60
|
+
const handleProjectClick = async (p: RecentProject) => {
|
|
61
61
|
setError(null);
|
|
62
62
|
setIsOpen(false);
|
|
63
63
|
try {
|
|
64
|
-
const result = await
|
|
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 "${
|
|
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 "${
|
|
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
|
|
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
|
|
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-
|
|
103
|
-
if (!
|
|
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((
|
|
133
|
+
.map((p) => (
|
|
134
134
|
<button
|
|
135
|
-
key={
|
|
136
|
-
onClick={() => handleProjectClick(
|
|
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">{
|
|
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/
|
|
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 (
|
|
123
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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">
|