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,7 +1,7 @@
|
|
|
1
|
-
'use client';
|
|
2
1
|
|
|
3
2
|
import { useState, useEffect, useRef } from 'react';
|
|
4
3
|
import { Button } from '@/components/ui/Button';
|
|
4
|
+
import { isTauri, auth, shell } from '@/lib/tauri-bridge';
|
|
5
5
|
|
|
6
6
|
const API_BASE = 'https://jettypod-update-server.spangbaryn2.workers.dev';
|
|
7
7
|
|
|
@@ -20,17 +20,18 @@ export function SubscribeContent({ onClose }: SubscribeContentProps) {
|
|
|
20
20
|
const [userPlan, setUserPlan] = useState<string | null>(null);
|
|
21
21
|
const [pollingPlan, setPollingPlan] = useState<PlanType | null>(null);
|
|
22
22
|
const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
23
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
23
24
|
|
|
24
25
|
useEffect(() => {
|
|
25
26
|
async function loadAuth() {
|
|
26
|
-
if (!
|
|
27
|
-
const status = await
|
|
27
|
+
if (!isTauri()) return;
|
|
28
|
+
const status = await auth.getStatus();
|
|
28
29
|
if (status.authenticated && status.user) {
|
|
29
30
|
setUserEmail(status.user.email);
|
|
30
31
|
setUserPlan(status.user.plan || 'free');
|
|
31
32
|
|
|
32
33
|
// Check server-side plan in case webhook updated it after local JWT was issued
|
|
33
|
-
const token = await
|
|
34
|
+
const token = await auth.getToken();
|
|
34
35
|
if (token) {
|
|
35
36
|
try {
|
|
36
37
|
const res = await fetch(`${API_BASE}/auth/me`, {
|
|
@@ -40,7 +41,7 @@ export function SubscribeContent({ onClose }: SubscribeContentProps) {
|
|
|
40
41
|
const data = await res.json() as { user: { plan: string; email: string }; token?: string };
|
|
41
42
|
if (data.user.plan !== 'free') {
|
|
42
43
|
if (data.token) {
|
|
43
|
-
await
|
|
44
|
+
await auth.saveToken(data.token, data.user);
|
|
44
45
|
}
|
|
45
46
|
setUserPlan(data.user.plan);
|
|
46
47
|
setPageState('upgraded');
|
|
@@ -59,19 +60,22 @@ export function SubscribeContent({ onClose }: SubscribeContentProps) {
|
|
|
59
60
|
useEffect(() => {
|
|
60
61
|
return () => {
|
|
61
62
|
if (pollingRef.current) clearInterval(pollingRef.current);
|
|
63
|
+
abortRef.current?.abort();
|
|
62
64
|
};
|
|
63
65
|
}, []);
|
|
64
66
|
|
|
65
67
|
const startPolling = () => {
|
|
66
68
|
setPageState('polling');
|
|
69
|
+
abortRef.current = new AbortController();
|
|
67
70
|
|
|
68
71
|
pollingRef.current = setInterval(async () => {
|
|
69
72
|
try {
|
|
70
|
-
const token = await
|
|
73
|
+
const token = await auth.getToken();
|
|
71
74
|
if (!token) return;
|
|
72
75
|
|
|
73
76
|
const res = await fetch(`${API_BASE}/auth/me`, {
|
|
74
77
|
headers: { 'Authorization': `Bearer ${token}` },
|
|
78
|
+
signal: abortRef.current?.signal,
|
|
75
79
|
});
|
|
76
80
|
|
|
77
81
|
if (!res.ok) return;
|
|
@@ -80,7 +84,7 @@ export function SubscribeContent({ onClose }: SubscribeContentProps) {
|
|
|
80
84
|
|
|
81
85
|
if (data.user.plan !== 'free') {
|
|
82
86
|
if (data.token) {
|
|
83
|
-
await
|
|
87
|
+
await auth.saveToken(data.token, data.user);
|
|
84
88
|
}
|
|
85
89
|
setUserPlan(data.user.plan);
|
|
86
90
|
setPageState('upgraded');
|
|
@@ -93,13 +97,38 @@ export function SubscribeContent({ onClose }: SubscribeContentProps) {
|
|
|
93
97
|
};
|
|
94
98
|
|
|
95
99
|
const handleCheckout = async (plan: 'monthly' | 'lifetime') => {
|
|
96
|
-
if (!
|
|
100
|
+
if (!isTauri()) return;
|
|
97
101
|
setCheckoutPlan(plan);
|
|
98
102
|
setError(null);
|
|
99
103
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
104
|
+
try {
|
|
105
|
+
const token = await auth.getToken();
|
|
106
|
+
if (!token) {
|
|
107
|
+
setError('Not authenticated');
|
|
108
|
+
setCheckoutPlan(null);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const res = await fetch(`${API_BASE}/billing/create-checkout`, {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
114
|
+
body: JSON.stringify({ plan }),
|
|
115
|
+
});
|
|
116
|
+
if (res.ok) {
|
|
117
|
+
const data = await res.json() as { url?: string };
|
|
118
|
+
if (data.url) {
|
|
119
|
+
await shell.openUrl(data.url);
|
|
120
|
+
} else {
|
|
121
|
+
setError('Failed to start checkout.');
|
|
122
|
+
setCheckoutPlan(null);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
setError('Failed to start checkout.');
|
|
127
|
+
setCheckoutPlan(null);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
setError('Failed to start checkout.');
|
|
103
132
|
setCheckoutPlan(null);
|
|
104
133
|
return;
|
|
105
134
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
'use client';
|
|
2
1
|
|
|
3
2
|
import { useState } from 'react';
|
|
4
|
-
import type { TestDashboardData, TestEpic, TestFeature, TestScenario } from '@/lib/
|
|
3
|
+
import type { TestDashboardData, TestEpic, TestFeature, TestScenario } from '@/lib/db';
|
|
5
4
|
import { TypeIcon } from './TypeIcon';
|
|
6
5
|
|
|
7
6
|
const statusIcons: Record<string, string> = {
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
1
|
import { useState, useEffect } from 'react';
|
|
4
2
|
import { m, AnimatePresence, useReducedMotion } from 'framer-motion';
|
|
5
|
-
import
|
|
3
|
+
import { lazy, Suspense } from 'react';
|
|
6
4
|
|
|
7
|
-
const LazyMarkdown =
|
|
5
|
+
const LazyMarkdown = lazy(() => import('./LazyMarkdown'));
|
|
8
6
|
|
|
9
7
|
const STORAGE_KEY = 'jettypod-dismissed-tips';
|
|
10
8
|
|
|
@@ -1,19 +1,18 @@
|
|
|
1
|
-
'use client';
|
|
2
1
|
|
|
3
2
|
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
4
3
|
import { createPortal } from 'react-dom';
|
|
5
4
|
import { TYPE_ICONS } from '@/lib/constants';
|
|
6
5
|
import { shadow } from '@/lib/shadows';
|
|
7
6
|
|
|
8
|
-
const
|
|
9
|
-
bug: { src: '/bug-icon.
|
|
10
|
-
chore: { src: '/wrench-icon.
|
|
11
|
-
epic: { src: '/buoy-icon.
|
|
12
|
-
feature: { src: '/star-icon.
|
|
7
|
+
const TYPE_ICON_SRCS: Record<string, { src: string; label: string }> = {
|
|
8
|
+
bug: { src: '/bug-icon.png', label: 'Bug' },
|
|
9
|
+
chore: { src: '/wrench-icon.png', label: 'Chore' },
|
|
10
|
+
epic: { src: '/buoy-icon.png', label: 'Epic' },
|
|
11
|
+
feature: { src: '/star-icon.png', label: 'Feature' },
|
|
13
12
|
};
|
|
14
13
|
|
|
15
14
|
export function TypeIcon({ type, className }: { type: string; className?: string }) {
|
|
16
|
-
const svg =
|
|
15
|
+
const svg = TYPE_ICON_SRCS[type];
|
|
17
16
|
const [tooltip, setTooltip] = useState<{ x: number; y: number } | null>(null);
|
|
18
17
|
const [mounted, setMounted] = useState(false);
|
|
19
18
|
const ref = useRef<HTMLSpanElement>(null);
|
|
@@ -37,7 +36,7 @@ export function TypeIcon({ type, className }: { type: string; className?: string
|
|
|
37
36
|
if (svg) {
|
|
38
37
|
return (
|
|
39
38
|
<span ref={ref} className="inline-flex" onMouseEnter={showTooltip} onMouseLeave={hideTooltip}>
|
|
40
|
-
<img src={svg.src} alt="" className={className || "w-
|
|
39
|
+
<img src={svg.src} alt="" className={`object-contain ${className || "w-6 h-6"}`} />
|
|
41
40
|
{mounted && tooltip && portalRoot && createPortal(
|
|
42
41
|
<span
|
|
43
42
|
className="pointer-events-none fixed"
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
|
|
2
|
+
import { useCallback } from 'react';
|
|
3
|
+
|
|
4
|
+
type ViewMode = 'summary' | 'detail' | 'raw';
|
|
5
|
+
|
|
6
|
+
const READOUT_FILTERS = [
|
|
7
|
+
{ id: 'init', label: 'Init', types: ['system'] },
|
|
8
|
+
{ id: 'streaming', label: 'Streaming', types: ['content_block_start', 'content_block_delta', 'content_block_stop', 'message_start', 'message_delta', 'message_stop'] },
|
|
9
|
+
{ id: 'messages', label: 'Messages', types: ['assistant'] },
|
|
10
|
+
{ id: 'tools', label: 'Tools', types: ['user'] },
|
|
11
|
+
{ id: 'completion', label: 'Completion', types: ['result', 'done'] },
|
|
12
|
+
{ id: 'errors', label: 'Errors', types: ['error'] },
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
type ReadoutFilterId = typeof READOUT_FILTERS[number]['id'];
|
|
16
|
+
|
|
17
|
+
interface ViewModeToolbarProps {
|
|
18
|
+
viewMode: ViewMode;
|
|
19
|
+
onViewModeChange: (mode: ViewMode) => void;
|
|
20
|
+
hasIntermediates: boolean;
|
|
21
|
+
allExpanded: boolean;
|
|
22
|
+
onToggleExpandAll: () => void;
|
|
23
|
+
activeFilters: Set<ReadoutFilterId>;
|
|
24
|
+
onToggleFilter: (id: ReadoutFilterId) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const MODE_LABELS: { mode: ViewMode; label: string }[] = [
|
|
28
|
+
{ mode: 'summary', label: 'Summary' },
|
|
29
|
+
{ mode: 'detail', label: 'Detail' },
|
|
30
|
+
{ mode: 'raw', label: 'Raw' },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
export function ViewModeToolbar({
|
|
34
|
+
viewMode,
|
|
35
|
+
onViewModeChange,
|
|
36
|
+
hasIntermediates,
|
|
37
|
+
allExpanded,
|
|
38
|
+
onToggleExpandAll,
|
|
39
|
+
activeFilters,
|
|
40
|
+
onToggleFilter,
|
|
41
|
+
}: ViewModeToolbarProps) {
|
|
42
|
+
return (
|
|
43
|
+
<div className="px-5 py-2 border-b border-zinc-100 flex-shrink-0" data-testid="view-mode-toolbar">
|
|
44
|
+
<div className="flex items-center justify-between">
|
|
45
|
+
{/* Left: mode toggle */}
|
|
46
|
+
<div className="flex items-center gap-1" data-testid="mode-toggle">
|
|
47
|
+
{MODE_LABELS.map(({ mode, label }, i) => (
|
|
48
|
+
<span key={mode} className="flex items-center">
|
|
49
|
+
{i > 0 && <span className="text-zinc-300 mx-1 text-xs select-none">|</span>}
|
|
50
|
+
<button
|
|
51
|
+
onClick={() => onViewModeChange(mode)}
|
|
52
|
+
className={`text-xs cursor-pointer transition-colors duration-200 ease-out ${
|
|
53
|
+
viewMode === mode
|
|
54
|
+
? 'text-zinc-900 font-semibold underline underline-offset-4 decoration-zinc-900'
|
|
55
|
+
: 'text-zinc-400 hover:text-zinc-600'
|
|
56
|
+
}`}
|
|
57
|
+
data-testid={`mode-${mode}`}
|
|
58
|
+
aria-pressed={viewMode === mode}
|
|
59
|
+
>
|
|
60
|
+
{label}
|
|
61
|
+
</button>
|
|
62
|
+
</span>
|
|
63
|
+
))}
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
{/* Right: contextual action (non-raw modes) */}
|
|
67
|
+
{viewMode === 'detail' && hasIntermediates && (
|
|
68
|
+
<button
|
|
69
|
+
onClick={onToggleExpandAll}
|
|
70
|
+
className="text-xs text-zinc-400 hover:text-zinc-600 cursor-pointer transition-colors duration-200 ease-out"
|
|
71
|
+
data-testid="expand-collapse-all"
|
|
72
|
+
>
|
|
73
|
+
{allExpanded ? 'Collapse all' : 'Expand all'}
|
|
74
|
+
</button>
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
{/* Raw filter chips: own row, centered */}
|
|
79
|
+
{viewMode === 'raw' && (
|
|
80
|
+
<div className="flex justify-center mt-2" data-testid="readout-filter-chips">
|
|
81
|
+
<div className="flex gap-1.5 flex-wrap justify-center">
|
|
82
|
+
{READOUT_FILTERS.map(f => (
|
|
83
|
+
<button
|
|
84
|
+
key={f.id}
|
|
85
|
+
onClick={() => onToggleFilter(f.id)}
|
|
86
|
+
className={`text-xs px-2 py-0.5 rounded-full cursor-pointer transition-colors duration-200 ease-out ${
|
|
87
|
+
activeFilters.has(f.id)
|
|
88
|
+
? 'bg-purple-100 text-purple-700'
|
|
89
|
+
: 'bg-zinc-100 text-zinc-400'
|
|
90
|
+
}`}
|
|
91
|
+
data-testid={`readout-filter-${f.id}`}
|
|
92
|
+
>
|
|
93
|
+
{f.label}
|
|
94
|
+
</button>
|
|
95
|
+
))}
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export type { ViewMode, ReadoutFilterId, ViewModeToolbarProps };
|
|
104
|
+
export { READOUT_FILTERS };
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
'use client';
|
|
2
1
|
|
|
3
2
|
import { useState, useEffect, useRef, ReactNode } from 'react';
|
|
4
3
|
|
|
@@ -16,7 +15,6 @@ export function WaveCompletionAnimation({ isPlaying, onComplete, children }: Wav
|
|
|
16
15
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
17
16
|
const hasStartedRef = useRef(false);
|
|
18
17
|
const timeoutRefs = useRef<NodeJS.Timeout[]>([]);
|
|
19
|
-
const measuredHeightRef = useRef<number | null>(null);
|
|
20
18
|
|
|
21
19
|
const clearAllTimeouts = () => {
|
|
22
20
|
timeoutRefs.current.forEach(clearTimeout);
|
|
@@ -35,8 +33,7 @@ export function WaveCompletionAnimation({ isPlaying, onComplete, children }: Wav
|
|
|
35
33
|
if (isPlaying && !hasStartedRef.current) {
|
|
36
34
|
hasStartedRef.current = true;
|
|
37
35
|
|
|
38
|
-
const prefersReducedMotion =
|
|
39
|
-
window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
36
|
+
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
40
37
|
|
|
41
38
|
if (prefersReducedMotion) {
|
|
42
39
|
setPhase('complete');
|
|
@@ -44,12 +41,6 @@ export function WaveCompletionAnimation({ isPlaying, onComplete, children }: Wav
|
|
|
44
41
|
return;
|
|
45
42
|
}
|
|
46
43
|
|
|
47
|
-
// Capture height before animation for smooth collapse later
|
|
48
|
-
if (containerRef.current) {
|
|
49
|
-
measuredHeightRef.current = containerRef.current.offsetHeight;
|
|
50
|
-
containerRef.current.style.maxHeight = `${measuredHeightRef.current}px`;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
44
|
// Phase 1: Content disappears instantly, video starts playing
|
|
54
45
|
setPhase('video-playing');
|
|
55
46
|
|
|
@@ -80,10 +71,6 @@ export function WaveCompletionAnimation({ isPlaying, onComplete, children }: Wav
|
|
|
80
71
|
clearAllTimeouts();
|
|
81
72
|
hasStartedRef.current = false;
|
|
82
73
|
setPhase('idle');
|
|
83
|
-
measuredHeightRef.current = null;
|
|
84
|
-
if (containerRef.current) {
|
|
85
|
-
containerRef.current.style.maxHeight = '';
|
|
86
|
-
}
|
|
87
74
|
}
|
|
88
75
|
}, [isPlaying, phase]);
|
|
89
76
|
|
|
@@ -100,9 +87,10 @@ export function WaveCompletionAnimation({ isPlaying, onComplete, children }: Wav
|
|
|
100
87
|
style={{
|
|
101
88
|
overflow: 'hidden',
|
|
102
89
|
opacity: isCollapsing ? 0 : 1,
|
|
103
|
-
|
|
90
|
+
transform: isCollapsing ? 'scaleY(0)' : 'scaleY(1)',
|
|
91
|
+
transformOrigin: 'top',
|
|
104
92
|
transition: isCollapsing
|
|
105
|
-
? 'opacity 0.4s ease-out,
|
|
93
|
+
? 'opacity 0.4s ease-out, transform 0.5s ease-out'
|
|
106
94
|
: undefined,
|
|
107
95
|
}}
|
|
108
96
|
>
|
|
@@ -118,7 +106,7 @@ export function WaveCompletionAnimation({ isPlaying, onComplete, children }: Wav
|
|
|
118
106
|
}}
|
|
119
107
|
muted
|
|
120
108
|
playsInline
|
|
121
|
-
preload="
|
|
109
|
+
preload="none"
|
|
122
110
|
src="/assets/wave-completion.mp4"
|
|
123
111
|
onError={handleVideoError}
|
|
124
112
|
/>
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import Image from 'next/image';
|
|
4
1
|
import { Button } from '@/components/ui/Button';
|
|
5
|
-
import type { RecentProject } from '@/lib/
|
|
2
|
+
import type { RecentProject } from '@/lib/tauri-bridge';
|
|
6
3
|
|
|
7
4
|
interface WelcomeScreenProps {
|
|
8
5
|
recentProjects?: RecentProject[];
|
|
@@ -22,12 +19,11 @@ export function WelcomeScreen({
|
|
|
22
19
|
<div className="max-w-md w-full space-y-6">
|
|
23
20
|
{/* Logo */}
|
|
24
21
|
<div className="flex flex-col items-center space-y-4">
|
|
25
|
-
<
|
|
22
|
+
<img
|
|
26
23
|
src="/jettypod_wordmark.png"
|
|
27
24
|
alt="JettyPod"
|
|
28
25
|
width={160}
|
|
29
26
|
height={40}
|
|
30
|
-
priority
|
|
31
27
|
/>
|
|
32
28
|
<p className="text-zinc-500 dark:text-zinc-400 text-center">
|
|
33
29
|
Select a project to get started
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
1
|
import { useState } from 'react';
|
|
4
|
-
import Link from '
|
|
2
|
+
import { Link } from 'react-router-dom';
|
|
5
3
|
import type { WorkItem } from '@/lib/db';
|
|
6
4
|
import { CopyableId } from './CopyableId';
|
|
7
5
|
import { STATUS_COLORS, MODE_LABELS } from '@/lib/constants';
|
|
@@ -42,7 +40,7 @@ function WorkItemNode({ item, depth = 0 }: WorkItemNodeProps) {
|
|
|
42
40
|
|
|
43
41
|
{/* Title */}
|
|
44
42
|
<Link
|
|
45
|
-
|
|
43
|
+
to={`/work/${item.id}`}
|
|
46
44
|
className={`flex-1 truncate hover:underline ${STATUS_COLORS[item.status]}`}
|
|
47
45
|
>
|
|
48
46
|
{item.title}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
1
|
import { useState, useEffect } from 'react';
|
|
4
|
-
import {
|
|
2
|
+
import { useNavigate } from 'react-router-dom';
|
|
5
3
|
import { useUsage } from '@/contexts/UsageContext';
|
|
6
4
|
import { SubscribeContent } from '@/components/SubscribeContent';
|
|
7
5
|
import { Button } from '@/components/ui/Button';
|
|
6
|
+
import { isTauri, auth, shell } from '@/lib/tauri-bridge';
|
|
7
|
+
|
|
8
|
+
const API_BASE = 'https://jettypod-update-server.spangbaryn2.workers.dev';
|
|
8
9
|
|
|
9
10
|
const planColors: Record<string, string> = {
|
|
10
11
|
free: 'bg-zinc-200 dark:bg-zinc-700 text-zinc-700 dark:text-zinc-300',
|
|
@@ -13,7 +14,7 @@ const planColors: Record<string, string> = {
|
|
|
13
14
|
};
|
|
14
15
|
|
|
15
16
|
export function AccountSection() {
|
|
16
|
-
const
|
|
17
|
+
const navigate = useNavigate();
|
|
17
18
|
const { plan, used, limit, loading, refresh } = useUsage();
|
|
18
19
|
const [email, setEmail] = useState<string | null>(null);
|
|
19
20
|
const [portalError, setPortalError] = useState<string | null>(null);
|
|
@@ -22,9 +23,9 @@ export function AccountSection() {
|
|
|
22
23
|
|
|
23
24
|
useEffect(() => {
|
|
24
25
|
async function loadAuth() {
|
|
25
|
-
if (!
|
|
26
|
+
if (!isTauri()) return;
|
|
26
27
|
try {
|
|
27
|
-
const status = await
|
|
28
|
+
const status = await auth.getStatus();
|
|
28
29
|
if (status.authenticated && status.user) {
|
|
29
30
|
setEmail(status.user.email);
|
|
30
31
|
}
|
|
@@ -39,13 +40,27 @@ export function AccountSection() {
|
|
|
39
40
|
const isAtCapacity = isFree && limit > 0 && used >= limit;
|
|
40
41
|
|
|
41
42
|
const handleManageSubscription = async () => {
|
|
42
|
-
if (!window.electronAPI?.billing?.openCustomerPortal) return;
|
|
43
43
|
setPortalError(null);
|
|
44
44
|
setPortalLoading(true);
|
|
45
45
|
try {
|
|
46
|
-
const
|
|
47
|
-
if (
|
|
48
|
-
setPortalError(
|
|
46
|
+
const token = await auth.getToken();
|
|
47
|
+
if (!token) {
|
|
48
|
+
setPortalError('Not authenticated');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const res = await fetch(`${API_BASE}/billing/customer-portal`, {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
headers: { 'Authorization': `Bearer ${token}` },
|
|
54
|
+
});
|
|
55
|
+
if (res.ok) {
|
|
56
|
+
const data = await res.json() as { url?: string };
|
|
57
|
+
if (data.url) {
|
|
58
|
+
await shell.openUrl(data.url);
|
|
59
|
+
} else {
|
|
60
|
+
setPortalError('Unable to open billing portal');
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
setPortalError('Unable to open billing portal');
|
|
49
64
|
}
|
|
50
65
|
} catch {
|
|
51
66
|
setPortalError('Unable to open billing portal');
|
|
@@ -55,9 +70,8 @@ export function AccountSection() {
|
|
|
55
70
|
};
|
|
56
71
|
|
|
57
72
|
const handleLogout = async () => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
router.push('/login');
|
|
73
|
+
await auth.logout();
|
|
74
|
+
navigate('/login');
|
|
61
75
|
};
|
|
62
76
|
|
|
63
77
|
const handleUpgradeClose = () => {
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { invoke } from '@/lib/tauri';
|
|
3
|
+
import { openDialog } from '@/lib/tauri';
|
|
4
|
+
import { Button } from '@/components/ui/Button';
|
|
5
|
+
import { ContextDocumentsSection } from './ContextDocumentsSection';
|
|
6
|
+
import type { ContextDocument } from '@/lib/data-bridge';
|
|
7
|
+
|
|
8
|
+
interface AiContextSectionProps {
|
|
9
|
+
initialDesignSystemDir: string | null;
|
|
10
|
+
initialContextDocuments: ContextDocument[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function AiContextSection({ initialDesignSystemDir, initialContextDocuments }: AiContextSectionProps) {
|
|
14
|
+
const [designSystemDir, setDesignSystemDir] = useState<string | null>(initialDesignSystemDir);
|
|
15
|
+
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
|
16
|
+
|
|
17
|
+
const showSuccess = (message: string) => {
|
|
18
|
+
setSuccessMessage(message);
|
|
19
|
+
setTimeout(() => setSuccessMessage(null), 3000);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const handleBrowse = async () => {
|
|
23
|
+
const selected = await openDialog({ directory: true, title: 'Select Design System Directory' });
|
|
24
|
+
if (selected && typeof selected === 'string') {
|
|
25
|
+
await invoke('db_set_design_system_dir', { dir: selected });
|
|
26
|
+
setDesignSystemDir(selected);
|
|
27
|
+
showSuccess('Design system directory set');
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const handleClear = async () => {
|
|
32
|
+
await invoke('db_reset_design_system_dir');
|
|
33
|
+
setDesignSystemDir(null);
|
|
34
|
+
showSuccess('Design system directory cleared');
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<section id="ai-context">
|
|
39
|
+
<div className="flex items-center justify-between mb-6">
|
|
40
|
+
<h2 className="text-lg font-medium text-zinc-900 dark:text-zinc-100">
|
|
41
|
+
AI Context
|
|
42
|
+
</h2>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
{successMessage && (
|
|
46
|
+
<div className="mb-4 px-6 py-3 bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300 rounded-lg text-base">
|
|
47
|
+
{successMessage}
|
|
48
|
+
</div>
|
|
49
|
+
)}
|
|
50
|
+
|
|
51
|
+
{/* Design System Directory Setting */}
|
|
52
|
+
<div className="p-6 bg-zinc-50 dark:bg-zinc-800/50 rounded-lg">
|
|
53
|
+
<div>
|
|
54
|
+
<label className="block text-base font-medium text-zinc-900 dark:text-zinc-100">
|
|
55
|
+
Design System Directory
|
|
56
|
+
</label>
|
|
57
|
+
<p className="text-base text-zinc-500 dark:text-zinc-400 mt-1">
|
|
58
|
+
Select a folder that includes your design system. Claude will use this when prototyping and building parts of your project.
|
|
59
|
+
</p>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div className="mt-4">
|
|
63
|
+
{designSystemDir ? (
|
|
64
|
+
<div className="flex items-center gap-3">
|
|
65
|
+
<span className="font-mono text-sm text-zinc-900 dark:text-zinc-100 bg-zinc-200 dark:bg-zinc-700 px-3 py-2 rounded flex-1 truncate">
|
|
66
|
+
{designSystemDir}
|
|
67
|
+
</span>
|
|
68
|
+
<Button onClick={handleBrowse} variant="ghost" size="sm">
|
|
69
|
+
Change
|
|
70
|
+
</Button>
|
|
71
|
+
<Button onClick={handleClear} variant="ghost" size="sm">
|
|
72
|
+
Clear
|
|
73
|
+
</Button>
|
|
74
|
+
</div>
|
|
75
|
+
) : (
|
|
76
|
+
<div className="flex items-center gap-3">
|
|
77
|
+
<Button onClick={handleBrowse} size="sm">
|
|
78
|
+
Browse...
|
|
79
|
+
</Button>
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
{/* Context Documents */}
|
|
86
|
+
<ContextDocumentsSection initialDocuments={initialContextDocuments} />
|
|
87
|
+
</section>
|
|
88
|
+
);
|
|
89
|
+
}
|