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,27 +1,75 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
2
|
import { AccountSection } from '@/components/settings/AccountSection';
|
|
3
3
|
import { EnvVarsSection } from '@/components/settings/EnvVarsSection';
|
|
4
4
|
import { GeneralSection } from '@/components/settings/GeneralSection';
|
|
5
|
+
import { AiContextSection } from '@/components/settings/AiContextSection';
|
|
6
|
+
import { ProjectStackSection } from '@/components/settings/ProjectStackSection';
|
|
5
7
|
import { SettingsLayout } from '@/components/settings/SettingsLayout';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
import { dataBridge, prefetch } from '@/lib/data-bridge';
|
|
9
|
+
import type { ContextDocument } from '@/lib/data-bridge';
|
|
10
|
+
import type { EnvironmentConfig } from '@/lib/environment-config';
|
|
8
11
|
|
|
9
12
|
export default function SettingsPage() {
|
|
10
|
-
const envFiles =
|
|
11
|
-
const selectedFile =
|
|
12
|
-
const envVars =
|
|
13
|
-
const mainBranch =
|
|
13
|
+
const [envFiles, setEnvFiles] = useState<string[]>([]);
|
|
14
|
+
const [selectedFile, setSelectedFile] = useState<string | null>(null);
|
|
15
|
+
const [envVars, setEnvVars] = useState<Array<{ key: string; value: string }>>([]);
|
|
16
|
+
const [mainBranch, setMainBranch] = useState('main');
|
|
17
|
+
const [claudeModel, setClaudeModel] = useState<string | null>(null);
|
|
18
|
+
const [designSystemDir, setDesignSystemDir] = useState<string | null>(null);
|
|
19
|
+
const [contextDocuments, setContextDocuments] = useState<ContextDocument[]>([]);
|
|
20
|
+
const [environmentConfig, setEnvironmentConfig] = useState<EnvironmentConfig | null>(null);
|
|
21
|
+
const [loading, setLoading] = useState(true);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
async function loadSettings() {
|
|
25
|
+
try {
|
|
26
|
+
const { files, selected, branch, claudeModel: model, designSystemDir: dsDir, contextDocuments: ctxDocs, environmentConfig: envConfig } = await prefetch.settings();
|
|
27
|
+
setEnvFiles(files);
|
|
28
|
+
const activeFile = selected || (files.includes('.env') ? '.env' : files[0] || null);
|
|
29
|
+
setSelectedFile(activeFile);
|
|
30
|
+
if (activeFile) {
|
|
31
|
+
const vars = await dataBridge.getEnvVars(activeFile);
|
|
32
|
+
setEnvVars(vars);
|
|
33
|
+
}
|
|
34
|
+
setMainBranch(branch);
|
|
35
|
+
setClaudeModel(model);
|
|
36
|
+
setDesignSystemDir(dsDir);
|
|
37
|
+
setContextDocuments(ctxDocs);
|
|
38
|
+
setEnvironmentConfig(envConfig);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
console.error('Failed to load settings:', err);
|
|
41
|
+
} finally {
|
|
42
|
+
setLoading(false);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
loadSettings();
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
if (loading) return (
|
|
49
|
+
<div className="flex-1 overflow-auto max-w-7xl w-full mx-auto px-4 py-4">
|
|
50
|
+
<div className="h-8 w-40 bg-zinc-200 dark:bg-zinc-800 rounded mb-8" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
|
|
51
|
+
<div className="flex gap-4 mb-6">
|
|
52
|
+
{[1, 2, 3, 4, 5].map(i => (
|
|
53
|
+
<div key={i} className="h-9 w-32 bg-zinc-200 dark:bg-zinc-800 rounded-lg" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
|
|
54
|
+
))}
|
|
55
|
+
</div>
|
|
56
|
+
<div className="space-y-4">
|
|
57
|
+
<div className="h-12 bg-zinc-200 dark:bg-zinc-800 rounded-lg" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
|
|
58
|
+
<div className="h-12 bg-zinc-200 dark:bg-zinc-800 rounded-lg" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
14
62
|
|
|
15
63
|
return (
|
|
16
64
|
<div className="flex-1 overflow-auto max-w-7xl w-full mx-auto px-4 py-4">
|
|
17
|
-
<h1 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100 mb-8">
|
|
18
|
-
Settings
|
|
19
|
-
</h1>
|
|
65
|
+
<h1 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100 mb-8">Settings</h1>
|
|
20
66
|
<SettingsLayout
|
|
21
67
|
tabs={[
|
|
22
68
|
{ id: 'account', label: 'Account', content: <AccountSection /> },
|
|
23
|
-
{ id: 'general', label: 'General', content: <GeneralSection initialMainBranch={mainBranch} /> },
|
|
24
|
-
{ id: 'env-vars', label: 'Environment Variables', content: <EnvVarsSection initialEnvVars={envVars} envFiles={envFiles} selectedFile={selectedFile} /> },
|
|
69
|
+
{ id: 'general', label: 'General', content: <GeneralSection initialMainBranch={{ branch: mainBranch, source: 'detected' }} initialClaudeModel={claudeModel} /> },
|
|
70
|
+
{ id: 'env-vars', label: 'Environment Variables', content: <EnvVarsSection initialEnvVars={envVars.map(v => ({ name: v.key, value: v.value }))} envFiles={envFiles} selectedFile={selectedFile} /> },
|
|
71
|
+
{ id: 'ai-context', label: 'AI Context', content: <AiContextSection initialDesignSystemDir={designSystemDir} initialContextDocuments={contextDocuments} /> },
|
|
72
|
+
{ id: 'project-stack', label: 'Your Project Stack', content: <ProjectStackSection initialConfig={environmentConfig} /> },
|
|
25
73
|
]}
|
|
26
74
|
/>
|
|
27
75
|
</div>
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
1
|
import { useState, useEffect, useRef } from 'react';
|
|
4
|
-
import
|
|
5
|
-
import Link from 'next/link';
|
|
2
|
+
import { Link } from 'react-router-dom';
|
|
6
3
|
import { Button } from '@/components/ui/Button';
|
|
7
4
|
import { Input } from '@/components/ui/Input';
|
|
5
|
+
import { isTauri, auth } from '@/lib/tauri-bridge';
|
|
8
6
|
|
|
9
7
|
const API_BASE = 'https://jettypod-update-server.spangbaryn2.workers.dev';
|
|
10
8
|
|
|
@@ -21,11 +19,11 @@ export default function SignupPage() {
|
|
|
21
19
|
// Redirect already-authenticated users to dashboard
|
|
22
20
|
useEffect(() => {
|
|
23
21
|
async function checkIfAlreadyAuthenticated() {
|
|
24
|
-
if (
|
|
22
|
+
if (isTauri()) {
|
|
25
23
|
try {
|
|
26
|
-
const status = await
|
|
24
|
+
const status = await auth.getStatus();
|
|
27
25
|
if (status.authenticated) {
|
|
28
|
-
const path = await
|
|
26
|
+
const path = await auth.getPostLoginPath() || '/';
|
|
29
27
|
window.location.href = path;
|
|
30
28
|
}
|
|
31
29
|
} catch {
|
|
@@ -43,15 +41,15 @@ export default function SignupPage() {
|
|
|
43
41
|
}, []);
|
|
44
42
|
|
|
45
43
|
const handleGoogleSignUp = () => {
|
|
46
|
-
if (!
|
|
47
|
-
|
|
44
|
+
if (!isTauri()) return;
|
|
45
|
+
auth.loginWithGoogle();
|
|
48
46
|
|
|
49
47
|
pollRef.current = setInterval(async () => {
|
|
50
48
|
try {
|
|
51
|
-
const status = await
|
|
49
|
+
const status = await auth.getStatus();
|
|
52
50
|
if (status.authenticated) {
|
|
53
51
|
if (pollRef.current) clearInterval(pollRef.current);
|
|
54
|
-
const path = await
|
|
52
|
+
const path = await auth.getPostLoginPath() || '/';
|
|
55
53
|
window.location.href = path;
|
|
56
54
|
}
|
|
57
55
|
} catch {
|
|
@@ -119,11 +117,11 @@ export default function SignupPage() {
|
|
|
119
117
|
|
|
120
118
|
const data = await res.json() as { token: string; user: { id: string; email: string; plan: string } };
|
|
121
119
|
|
|
122
|
-
if (
|
|
123
|
-
await
|
|
120
|
+
if (isTauri()) {
|
|
121
|
+
await auth.saveToken(data.token, data.user);
|
|
124
122
|
}
|
|
125
123
|
|
|
126
|
-
const path = await
|
|
124
|
+
const path = await auth.getPostLoginPath() || '/';
|
|
127
125
|
window.location.href = path;
|
|
128
126
|
} catch {
|
|
129
127
|
setError('Failed to verify code. Check your connection.');
|
|
@@ -136,12 +134,11 @@ export default function SignupPage() {
|
|
|
136
134
|
<div className="max-w-md w-full space-y-10">
|
|
137
135
|
{/* Logo */}
|
|
138
136
|
<div className="flex flex-col items-center space-y-6">
|
|
139
|
-
<
|
|
137
|
+
<img
|
|
140
138
|
src="/jettypod_wordmark.png"
|
|
141
139
|
alt="JettyPod"
|
|
142
140
|
width={160}
|
|
143
141
|
height={40}
|
|
144
|
-
priority
|
|
145
142
|
/>
|
|
146
143
|
<h1 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100 text-center">
|
|
147
144
|
Create your account
|
|
@@ -235,7 +232,7 @@ export default function SignupPage() {
|
|
|
235
232
|
{/* Switch to login */}
|
|
236
233
|
<p className="text-center text-base text-zinc-500 dark:text-zinc-400">
|
|
237
234
|
Already have an account?{' '}
|
|
238
|
-
<Link
|
|
235
|
+
<Link to="/login" className="font-medium hover:underline" style={{ color: '#819D9F' }}>
|
|
239
236
|
Sign in
|
|
240
237
|
</Link>
|
|
241
238
|
</p>
|
|
@@ -1,10 +1,43 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
2
|
import { RealTimeTestsWrapper } from '@/components/RealTimeTestsWrapper';
|
|
3
|
-
|
|
4
|
-
export const dynamic = 'force-dynamic';
|
|
3
|
+
import { prefetch } from '@/lib/data-bridge';
|
|
5
4
|
|
|
6
5
|
export default function TestsPage() {
|
|
7
|
-
const initialData =
|
|
6
|
+
const [initialData, setInitialData] = useState<any>(null);
|
|
7
|
+
const [loading, setLoading] = useState(true);
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
async function loadData() {
|
|
11
|
+
try {
|
|
12
|
+
const data = await prefetch.tests();
|
|
13
|
+
setInitialData(data);
|
|
14
|
+
} catch (err) {
|
|
15
|
+
console.error('Failed to load test data:', err);
|
|
16
|
+
setInitialData({ suites: [], summary: { total: 0, passing: 0, failing: 0, pending: 0 } });
|
|
17
|
+
} finally {
|
|
18
|
+
setLoading(false);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
loadData();
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
if (loading || !initialData) return (
|
|
25
|
+
<div className="flex-1 max-w-7xl w-full mx-auto px-4 py-4">
|
|
26
|
+
<div className="flex justify-between items-center mb-6">
|
|
27
|
+
<div className="h-8 w-32 bg-zinc-200 dark:bg-zinc-800 rounded" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
|
|
28
|
+
<div className="flex gap-3">
|
|
29
|
+
{[1, 2, 3, 4].map(i => (
|
|
30
|
+
<div key={i} className="h-10 w-20 bg-zinc-200 dark:bg-zinc-800 rounded-lg" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
|
|
31
|
+
))}
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
<div className="space-y-3">
|
|
35
|
+
{[1, 2, 3, 4, 5].map(i => (
|
|
36
|
+
<div key={i} className="h-16 bg-zinc-200 dark:bg-zinc-800 rounded-xl" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
|
|
37
|
+
))}
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
8
41
|
|
|
9
42
|
return <RealTimeTestsWrapper initialData={initialData} />;
|
|
10
43
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
1
|
import { useState, useEffect } from 'react';
|
|
4
2
|
import { WelcomeScreen } from '@/components/WelcomeScreen';
|
|
5
|
-
import
|
|
3
|
+
import { isTauri, project } from '@/lib/tauri-bridge';
|
|
4
|
+
import type { RecentProject } from '@/lib/tauri-bridge';
|
|
6
5
|
|
|
7
6
|
export default function WelcomePage() {
|
|
8
7
|
const [error, setError] = useState<string | null>(null);
|
|
@@ -11,11 +10,11 @@ export default function WelcomePage() {
|
|
|
11
10
|
// Load recent projects on mount
|
|
12
11
|
useEffect(() => {
|
|
13
12
|
async function loadRecentProjects() {
|
|
14
|
-
if (!
|
|
13
|
+
if (!isTauri()) {
|
|
15
14
|
return;
|
|
16
15
|
}
|
|
17
16
|
|
|
18
|
-
const projects = await
|
|
17
|
+
const projects = await project.getRecent();
|
|
19
18
|
setRecentProjects(projects);
|
|
20
19
|
}
|
|
21
20
|
|
|
@@ -25,12 +24,12 @@ export default function WelcomePage() {
|
|
|
25
24
|
const handleNewProject = async () => {
|
|
26
25
|
setError(null);
|
|
27
26
|
|
|
28
|
-
if (!
|
|
27
|
+
if (!isTauri()) {
|
|
29
28
|
setError('Project creation is only available in the desktop app.');
|
|
30
29
|
return;
|
|
31
30
|
}
|
|
32
31
|
|
|
33
|
-
const result = await
|
|
32
|
+
const result = await project.newProject();
|
|
34
33
|
|
|
35
34
|
if (result.canceled) {
|
|
36
35
|
return;
|
|
@@ -47,13 +46,12 @@ export default function WelcomePage() {
|
|
|
47
46
|
const handleOpenProject = async () => {
|
|
48
47
|
setError(null);
|
|
49
48
|
|
|
50
|
-
|
|
51
|
-
if (!window.electronAPI?.isElectron) {
|
|
49
|
+
if (!isTauri()) {
|
|
52
50
|
setError('Project selection is only available in the desktop app.');
|
|
53
51
|
return;
|
|
54
52
|
}
|
|
55
53
|
|
|
56
|
-
const result = await
|
|
54
|
+
const result = await project.openDialog();
|
|
57
55
|
|
|
58
56
|
if (result.canceled) {
|
|
59
57
|
// User canceled - do nothing
|
|
@@ -69,19 +67,18 @@ export default function WelcomePage() {
|
|
|
69
67
|
window.location.href = '/';
|
|
70
68
|
};
|
|
71
69
|
|
|
72
|
-
const handleSelectRecentProject = async (
|
|
70
|
+
const handleSelectRecentProject = async (p: RecentProject) => {
|
|
73
71
|
setError(null);
|
|
74
72
|
|
|
75
|
-
|
|
76
|
-
if (!window.electronAPI?.isElectron) {
|
|
73
|
+
if (!isTauri()) {
|
|
77
74
|
setError('Project selection is only available in the desktop app.');
|
|
78
75
|
return;
|
|
79
76
|
}
|
|
80
77
|
|
|
81
|
-
const result = await
|
|
78
|
+
const result = await project.openRecent(p.path);
|
|
82
79
|
|
|
83
80
|
if (!result.success) {
|
|
84
|
-
setError(
|
|
81
|
+
setError('Failed to open project');
|
|
85
82
|
return;
|
|
86
83
|
}
|
|
87
84
|
|
|
@@ -1,74 +1,117 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { useParams, Link, useNavigate } from 'react-router-dom';
|
|
2
3
|
import { WorkItemTree } from '@/components/WorkItemTree';
|
|
3
4
|
import { WorkItemHeader } from '@/components/WorkItemHeader';
|
|
4
5
|
import { EditableDetailTitle } from '@/components/EditableDetailTitle';
|
|
5
6
|
import { EditableDetailDescription } from '@/components/EditableDetailDescription';
|
|
6
|
-
import Link from 'next/link';
|
|
7
|
-
import { notFound } from 'next/navigation';
|
|
8
7
|
import { TYPE_LABELS, STATUS_LABELS, MODE_LABELS_FULL } from '@/lib/constants';
|
|
9
8
|
import { TypeIcon } from '@/components/TypeIcon';
|
|
10
9
|
import { DetailReviewActions } from '@/components/DetailReviewActions';
|
|
10
|
+
import { dataBridge, prefetch } from '@/lib/data-bridge';
|
|
11
|
+
import type { WorkItemData, DecisionData } from '@/lib/data-bridge';
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
export default function WorkItemPage() {
|
|
14
|
+
const { id } = useParams<{ id: string }>();
|
|
15
|
+
const navigate = useNavigate();
|
|
16
|
+
const [item, setItem] = useState<WorkItemData | null>(null);
|
|
17
|
+
const [children, setChildren] = useState<WorkItemData[]>([]);
|
|
18
|
+
const [decisions, setDecisions] = useState<DecisionData[]>([]);
|
|
19
|
+
const [parentItem, setParentItem] = useState<WorkItemData | null>(null);
|
|
20
|
+
const [loading, setLoading] = useState(true);
|
|
21
|
+
const [notFound, setNotFound] = useState(false);
|
|
15
22
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
async function loadData() {
|
|
25
|
+
const workItemId = parseInt(id || '', 10);
|
|
26
|
+
if (isNaN(workItemId)) {
|
|
27
|
+
setNotFound(true);
|
|
28
|
+
setLoading(false);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
19
31
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
32
|
+
try {
|
|
33
|
+
// Uses prefetch cache — if the user hovered the kanban card, data is
|
|
34
|
+
// already loaded. Otherwise fetches fresh (same parallel IPC calls).
|
|
35
|
+
const { item: workItem, children: childItems, decisions: decisionItems } =
|
|
36
|
+
await prefetch.workItem(workItemId);
|
|
37
|
+
|
|
38
|
+
if (!workItem) {
|
|
39
|
+
setNotFound(true);
|
|
40
|
+
setLoading(false);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Parent fetch depends on the work item's parent_id, but we can
|
|
45
|
+
// overlap it with setState calls since React batches them.
|
|
46
|
+
const parent = workItem.parent_id
|
|
47
|
+
? await dataBridge.getWorkItem(workItem.parent_id)
|
|
48
|
+
: null;
|
|
49
|
+
|
|
50
|
+
setItem(workItem);
|
|
51
|
+
setChildren(childItems);
|
|
52
|
+
setDecisions(decisionItems);
|
|
53
|
+
setParentItem(parent);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.error('Failed to load work item:', err);
|
|
56
|
+
setNotFound(true);
|
|
57
|
+
} finally {
|
|
58
|
+
setLoading(false);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
loadData();
|
|
62
|
+
}, [id]);
|
|
23
63
|
|
|
24
|
-
const
|
|
64
|
+
const handleTitleChange = useCallback((newTitle: string) => {
|
|
65
|
+
setItem(prev => prev ? { ...prev, title: newTitle } : prev);
|
|
66
|
+
}, []);
|
|
25
67
|
|
|
26
|
-
|
|
27
|
-
|
|
68
|
+
const handleDescriptionChange = useCallback((newDescription: string) => {
|
|
69
|
+
setItem(prev => prev ? { ...prev, description: newDescription } : prev);
|
|
70
|
+
}, []);
|
|
71
|
+
|
|
72
|
+
if (loading) return null;
|
|
73
|
+
|
|
74
|
+
if (notFound) {
|
|
75
|
+
return (
|
|
76
|
+
<div className="flex-1 flex items-center justify-center">
|
|
77
|
+
<div className="text-center">
|
|
78
|
+
<h1 className="text-2xl font-bold text-zinc-900 dark:text-zinc-100 mb-2">Not Found</h1>
|
|
79
|
+
<p className="text-zinc-500 mb-4">Work item not found.</p>
|
|
80
|
+
<Link to="/" viewTransition className="text-[#5a7d7f] hover:underline">← Back to Dashboard</Link>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
28
84
|
}
|
|
29
85
|
|
|
30
|
-
|
|
31
|
-
const decisions = getDecisionsForWorkItem(workItemId);
|
|
32
|
-
const parentItem = item.parent_id ? getWorkItem(item.parent_id) : null;
|
|
86
|
+
if (!item) return null;
|
|
33
87
|
|
|
34
88
|
const typeInfo = TYPE_LABELS[item.type] || { icon: '📄', label: 'Item' };
|
|
35
89
|
const statusInfo = STATUS_LABELS[item.status] || STATUS_LABELS.backlog;
|
|
36
90
|
|
|
37
91
|
return (
|
|
38
|
-
<div className="
|
|
39
|
-
{/* Header */}
|
|
92
|
+
<div className="flex-1 overflow-y-auto bg-zinc-50 dark:bg-zinc-950">
|
|
40
93
|
<header className="border-b border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900">
|
|
41
94
|
<div className="max-w-4xl mx-auto px-6 py-6">
|
|
42
|
-
<Link
|
|
43
|
-
← Back to Dashboard
|
|
44
|
-
</Link>
|
|
95
|
+
<Link to="/" viewTransition className="text-[#5a7d7f] dark:text-[#a3bfc0] hover:underline text-base">← Back to Dashboard</Link>
|
|
45
96
|
</div>
|
|
46
97
|
</header>
|
|
47
98
|
|
|
48
99
|
<main className="max-w-4xl mx-auto px-4 py-6">
|
|
49
|
-
{/* Breadcrumb */}
|
|
50
100
|
{parentItem && (
|
|
51
101
|
<div className="mb-6 text-base text-zinc-500">
|
|
52
|
-
<Link
|
|
102
|
+
<Link to={`/work/${parentItem.id}`} viewTransition className="inline-flex items-center gap-1.5 hover:underline">
|
|
53
103
|
<TypeIcon type={parentItem.type} className="w-6 h-6" /> #{parentItem.id} {parentItem.title}
|
|
54
104
|
</Link>
|
|
55
105
|
<span className="mx-2">→</span>
|
|
56
106
|
</div>
|
|
57
107
|
)}
|
|
58
108
|
|
|
59
|
-
{/* Main card */}
|
|
60
109
|
<div className="bg-white dark:bg-zinc-900 rounded-lg border border-zinc-200 dark:border-zinc-800 overflow-hidden">
|
|
61
|
-
{/* Header */}
|
|
62
110
|
<div className="px-8 py-6 border-b border-zinc-200 dark:border-zinc-800">
|
|
63
111
|
<div className="flex items-start justify-between gap-6">
|
|
64
112
|
<div className="flex-1 min-w-0">
|
|
65
|
-
<WorkItemHeader
|
|
66
|
-
|
|
67
|
-
title={item.title}
|
|
68
|
-
type={item.type}
|
|
69
|
-
typeLabel={typeInfo.label}
|
|
70
|
-
/>
|
|
71
|
-
<EditableDetailTitle title={item.title} itemId={item.id} />
|
|
113
|
+
<WorkItemHeader id={item.id} title={item.title} type={item.type} typeLabel={typeInfo.label} />
|
|
114
|
+
<EditableDetailTitle title={item.title} itemId={item.id} onTitleChange={handleTitleChange} />
|
|
72
115
|
</div>
|
|
73
116
|
<div className="flex items-center gap-2">
|
|
74
117
|
{item.mode && MODE_LABELS_FULL[item.mode] && (
|
|
@@ -76,29 +119,19 @@ export default async function WorkItemPage({ params }: PageProps) {
|
|
|
76
119
|
{MODE_LABELS_FULL[item.mode].label}
|
|
77
120
|
</span>
|
|
78
121
|
)}
|
|
79
|
-
<span className={`text-base px-3 py-1.5 rounded ${statusInfo.color}`}>
|
|
80
|
-
|
|
81
|
-
</span>
|
|
82
|
-
{!!item.ready_for_review && (
|
|
83
|
-
<DetailReviewActions workItemId={item.id} />
|
|
84
|
-
)}
|
|
122
|
+
<span className={`text-base px-3 py-1.5 rounded ${statusInfo.color}`}>{statusInfo.label}</span>
|
|
123
|
+
{!!item.ready_for_review && <DetailReviewActions workItemId={item.id} />}
|
|
85
124
|
</div>
|
|
86
125
|
</div>
|
|
87
126
|
</div>
|
|
88
127
|
|
|
89
|
-
{/* Description */}
|
|
90
128
|
<div className="px-8 py-6 border-b border-zinc-200 dark:border-zinc-800">
|
|
91
|
-
<h2 className="text-base font-semibold text-zinc-500 uppercase tracking-wide mb-3">
|
|
92
|
-
|
|
93
|
-
</h2>
|
|
94
|
-
<EditableDetailDescription description={item.description} itemId={item.id} />
|
|
129
|
+
<h2 className="text-base font-semibold text-zinc-500 uppercase tracking-wide mb-3">Description</h2>
|
|
130
|
+
<EditableDetailDescription description={item.description} itemId={item.id} onDescriptionChange={handleDescriptionChange} />
|
|
95
131
|
</div>
|
|
96
132
|
|
|
97
|
-
{/* Metadata */}
|
|
98
133
|
<div className="px-8 py-6 border-b border-zinc-200 dark:border-zinc-800">
|
|
99
|
-
<h2 className="text-base font-semibold text-zinc-500 uppercase tracking-wide mb-4">
|
|
100
|
-
Details
|
|
101
|
-
</h2>
|
|
134
|
+
<h2 className="text-base font-semibold text-zinc-500 uppercase tracking-wide mb-4">Details</h2>
|
|
102
135
|
<dl className="grid grid-cols-2 gap-6 text-base">
|
|
103
136
|
{item.branch_name && (
|
|
104
137
|
<div>
|
|
@@ -114,59 +147,41 @@ export default async function WorkItemPage({ params }: PageProps) {
|
|
|
114
147
|
)}
|
|
115
148
|
<div>
|
|
116
149
|
<dt className="text-zinc-500">Created</dt>
|
|
117
|
-
<dd className="text-zinc-900 dark:text-zinc-100">
|
|
118
|
-
{new Date(item.created_at).toLocaleDateString()}
|
|
119
|
-
</dd>
|
|
150
|
+
<dd className="text-zinc-900 dark:text-zinc-100">{new Date(item.created_at).toLocaleDateString()}</dd>
|
|
120
151
|
</div>
|
|
121
152
|
{item.completed_at && (
|
|
122
153
|
<div>
|
|
123
154
|
<dt className="text-zinc-500">Completed</dt>
|
|
124
|
-
<dd className="text-zinc-900 dark:text-zinc-100">
|
|
125
|
-
{new Date(item.completed_at).toLocaleDateString()}
|
|
126
|
-
</dd>
|
|
155
|
+
<dd className="text-zinc-900 dark:text-zinc-100">{new Date(item.completed_at).toLocaleDateString()}</dd>
|
|
127
156
|
</div>
|
|
128
157
|
)}
|
|
129
158
|
</dl>
|
|
130
159
|
</div>
|
|
131
160
|
|
|
132
|
-
{/* Decisions */}
|
|
133
161
|
{decisions.length > 0 && (
|
|
134
162
|
<div className="px-8 py-6 border-b border-zinc-200 dark:border-zinc-800">
|
|
135
|
-
<h2 className="text-base font-semibold text-zinc-500 uppercase tracking-wide mb-3">
|
|
136
|
-
Decisions
|
|
137
|
-
</h2>
|
|
163
|
+
<h2 className="text-base font-semibold text-zinc-500 uppercase tracking-wide mb-3">Decisions</h2>
|
|
138
164
|
<div className="space-y-6">
|
|
139
165
|
{decisions.map((decision) => (
|
|
140
166
|
<div key={decision.id} className="bg-zinc-50 dark:bg-zinc-800 rounded-lg p-6">
|
|
141
167
|
<div className="flex items-center gap-3 mb-3">
|
|
142
|
-
<span className="text-base font-medium text-zinc-900 dark:text-zinc-100">
|
|
143
|
-
|
|
144
|
-
</span>
|
|
145
|
-
<span className="text-base text-zinc-500">
|
|
146
|
-
{new Date(decision.created_at).toLocaleDateString()}
|
|
147
|
-
</span>
|
|
168
|
+
<span className="text-base font-medium text-zinc-900 dark:text-zinc-100">{decision.aspect}</span>
|
|
169
|
+
<span className="text-base text-zinc-500">{new Date(decision.created_at).toLocaleDateString()}</span>
|
|
148
170
|
</div>
|
|
149
|
-
<p className="text-zinc-700 dark:text-zinc-300 font-medium">
|
|
150
|
-
|
|
151
|
-
</p>
|
|
152
|
-
{decision.rationale && (
|
|
153
|
-
<p className="text-base text-zinc-500 mt-1.5">
|
|
154
|
-
{decision.rationale}
|
|
155
|
-
</p>
|
|
156
|
-
)}
|
|
171
|
+
<p className="text-zinc-700 dark:text-zinc-300 font-medium">{decision.decision}</p>
|
|
172
|
+
{decision.rationale && <p className="text-base text-zinc-500 mt-1.5">{decision.rationale}</p>}
|
|
157
173
|
</div>
|
|
158
174
|
))}
|
|
159
175
|
</div>
|
|
160
176
|
</div>
|
|
161
177
|
)}
|
|
162
178
|
|
|
163
|
-
{/* Children */}
|
|
164
179
|
{children.length > 0 && (
|
|
165
180
|
<div className="px-8 py-6">
|
|
166
181
|
<h2 className="text-base font-semibold text-zinc-500 uppercase tracking-wide mb-4">
|
|
167
182
|
{item.type === 'epic' ? 'Features & Chores' : 'Child Items'} ({children.length})
|
|
168
183
|
</h2>
|
|
169
|
-
<WorkItemTree items={children.map(c => ({ ...c, children: [] }))} />
|
|
184
|
+
<WorkItemTree items={children.map(c => ({ ...c, type: c.type, children: [], epic_id: c.parent_id, rejection_round: c.rejection_round ?? null, rejection_history: c.rejection_history ?? null }))} />
|
|
170
185
|
</div>
|
|
171
186
|
)}
|
|
172
187
|
</div>
|