jettypod 4.4.116 → 4.4.120
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 +7 -0
- package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +124 -48
- package/apps/dashboard/app/api/claude/[workItemId]/route.ts +171 -58
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +161 -10
- package/apps/dashboard/app/api/tests/run/stream/route.ts +13 -1
- package/apps/dashboard/app/api/usage/route.ts +17 -0
- package/apps/dashboard/app/api/work/[id]/route.ts +35 -0
- package/apps/dashboard/app/api/work/[id]/status/route.ts +43 -1
- package/apps/dashboard/app/connect-claude/page.tsx +24 -0
- package/apps/dashboard/app/decision/[id]/page.tsx +14 -14
- package/apps/dashboard/app/demo/gates/page.tsx +42 -42
- package/apps/dashboard/app/design-system/page.tsx +868 -0
- package/apps/dashboard/app/globals.css +6 -2
- package/apps/dashboard/app/install-claude/page.tsx +9 -7
- package/apps/dashboard/app/layout.tsx +17 -5
- package/apps/dashboard/app/login/page.tsx +250 -0
- package/apps/dashboard/app/page.tsx +11 -9
- package/apps/dashboard/app/settings/page.tsx +4 -2
- package/apps/dashboard/app/signup/page.tsx +245 -0
- package/apps/dashboard/app/subscribe/page.tsx +11 -0
- package/apps/dashboard/app/welcome/page.tsx +24 -1
- package/apps/dashboard/app/work/[id]/page.tsx +34 -50
- package/apps/dashboard/components/AppShell.tsx +95 -55
- package/apps/dashboard/components/CardMenu.tsx +56 -13
- package/apps/dashboard/components/ClaudePanel.tsx +301 -582
- package/apps/dashboard/components/ClaudePanelInput.tsx +23 -14
- package/apps/dashboard/components/ConnectClaudeScreen.tsx +210 -0
- package/apps/dashboard/components/CopyableId.tsx +3 -3
- package/apps/dashboard/components/DetailReviewActions.tsx +109 -0
- package/apps/dashboard/components/DragContext.tsx +75 -65
- package/apps/dashboard/components/DraggableCard.tsx +6 -46
- package/apps/dashboard/components/DropZone.tsx +2 -2
- package/apps/dashboard/components/EditableDetailDescription.tsx +1 -1
- package/apps/dashboard/components/EditableTitle.tsx +26 -6
- package/apps/dashboard/components/ElapsedTimer.tsx +54 -0
- package/apps/dashboard/components/EpicGroup.tsx +329 -0
- package/apps/dashboard/components/GateCard.tsx +100 -16
- package/apps/dashboard/components/GateChoiceCard.tsx +15 -17
- package/apps/dashboard/components/InstallClaudeScreen.tsx +140 -51
- package/apps/dashboard/components/JettyLoader.tsx +38 -0
- package/apps/dashboard/components/KanbanBoard.tsx +147 -766
- package/apps/dashboard/components/KanbanCard.tsx +506 -0
- package/apps/dashboard/components/LazyMarkdown.tsx +12 -0
- package/apps/dashboard/components/MainNav.tsx +20 -54
- package/apps/dashboard/components/MessageBlock.tsx +391 -0
- package/apps/dashboard/components/ModeStartCard.tsx +15 -15
- package/apps/dashboard/components/OnboardingWelcome.tsx +214 -0
- package/apps/dashboard/components/PlaceholderCard.tsx +11 -21
- package/apps/dashboard/components/ProjectSwitcher.tsx +36 -8
- package/apps/dashboard/components/PrototypeTimeline.tsx +25 -25
- package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +265 -301
- package/apps/dashboard/components/RealTimeTestsWrapper.tsx +97 -74
- package/apps/dashboard/components/ReviewFooter.tsx +141 -0
- package/apps/dashboard/components/SessionList.tsx +19 -18
- package/apps/dashboard/components/SubscribeContent.tsx +206 -0
- package/apps/dashboard/components/TestTree.tsx +15 -14
- package/apps/dashboard/components/TipCard.tsx +177 -0
- package/apps/dashboard/components/Toast.tsx +5 -5
- package/apps/dashboard/components/TypeIcon.tsx +56 -0
- package/apps/dashboard/components/UpgradeBanner.tsx +30 -0
- package/apps/dashboard/components/WaveCompletionAnimation.tsx +61 -62
- package/apps/dashboard/components/WelcomeScreen.tsx +25 -27
- package/apps/dashboard/components/WorkItemHeader.tsx +4 -4
- package/apps/dashboard/components/WorkItemTree.tsx +9 -28
- package/apps/dashboard/components/settings/AccountSection.tsx +169 -0
- package/apps/dashboard/components/settings/EnvVarsSection.tsx +54 -79
- package/apps/dashboard/components/settings/GeneralSection.tsx +26 -31
- package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -4
- package/apps/dashboard/components/ui/Button.tsx +104 -0
- package/apps/dashboard/components/ui/Input.tsx +78 -0
- package/apps/dashboard/contexts/ClaudeSessionContext.tsx +408 -105
- package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -4
- package/apps/dashboard/contexts/UsageContext.tsx +155 -0
- package/apps/dashboard/contexts/usageHelpers.js +9 -0
- package/apps/dashboard/electron/ipc-handlers.js +281 -88
- package/apps/dashboard/electron/main.js +691 -131
- package/apps/dashboard/electron/preload.js +25 -4
- package/apps/dashboard/electron/session-manager.js +163 -0
- package/apps/dashboard/electron-builder.config.js +3 -5
- package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
- package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
- package/apps/dashboard/lib/backlog-parser.ts +50 -0
- package/apps/dashboard/lib/claude-process-manager.ts +50 -11
- package/apps/dashboard/lib/constants.ts +43 -0
- package/apps/dashboard/lib/db-bridge.ts +33 -0
- package/apps/dashboard/lib/db.ts +136 -20
- package/apps/dashboard/lib/kanban-utils.ts +70 -0
- package/apps/dashboard/lib/run-migrations.js +27 -2
- package/apps/dashboard/lib/session-state-machine.ts +3 -0
- package/apps/dashboard/lib/session-stream-manager.ts +144 -38
- package/apps/dashboard/lib/shadows.ts +7 -0
- package/apps/dashboard/lib/tests.ts +3 -1
- package/apps/dashboard/lib/utils.ts +6 -0
- package/apps/dashboard/next.config.js +35 -14
- package/apps/dashboard/package.json +6 -3
- package/apps/dashboard/public/bug-icon.svg +9 -0
- package/apps/dashboard/public/buoy-icon.svg +9 -0
- package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
- package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
- package/apps/dashboard/public/in-flight-seagull.svg +9 -0
- package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
- package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
- package/apps/dashboard/public/jettypod_logo.png +0 -0
- package/apps/dashboard/public/pier-icon.svg +14 -0
- package/apps/dashboard/public/star-icon.svg +9 -0
- package/apps/dashboard/public/wrench-icon.svg +9 -0
- package/apps/dashboard/scripts/upload-to-r2.js +89 -0
- package/apps/dashboard/scripts/ws-server.js +191 -0
- package/apps/dashboard/tsconfig.tsbuildinfo +1 -0
- package/apps/update-server/package.json +16 -0
- package/apps/update-server/schema.sql +31 -0
- package/apps/update-server/src/index.ts +1085 -0
- package/apps/update-server/tsconfig.json +16 -0
- package/apps/update-server/wrangler.toml +35 -0
- package/cucumber.js +9 -3
- package/docs/COMMAND_REFERENCE.md +34 -0
- package/hooks/post-checkout +32 -75
- package/hooks/post-merge +111 -10
- package/jest.setup.js +1 -0
- package/jettypod.js +54 -116
- package/lib/chore-taxonomy.js +33 -10
- package/lib/database.js +36 -16
- package/lib/db-watcher.js +1 -1
- package/lib/git-hooks/pre-commit +1 -1
- package/lib/jettypod-backup.js +27 -4
- package/lib/migrations/027-plan-at-creation-column.js +33 -0
- package/lib/migrations/028-ready-for-review-column.js +27 -0
- package/lib/migrations/029-remove-autoincrement.js +307 -0
- package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
- package/lib/migrations/index.js +47 -4
- package/lib/schema.js +13 -6
- package/lib/seed-onboarding.js +101 -69
- package/lib/update-command/index.js +9 -175
- package/lib/work-commands/index.js +129 -16
- package/lib/work-tracking/index.js +86 -46
- package/lib/worktree-diagnostics.js +16 -16
- package/lib/worktree-facade.js +1 -1
- package/lib/worktree-manager.js +8 -8
- package/lib/worktree-reconciler.js +5 -5
- package/package.json +9 -2
- package/scripts/ndjson-to-cucumber-json.js +152 -0
- package/scripts/postinstall.js +25 -0
- package/skills-templates/bug-mode/SKILL.md +39 -28
- package/skills-templates/bug-planning/SKILL.md +25 -29
- package/skills-templates/chore-mode/SKILL.md +131 -68
- package/skills-templates/chore-mode/verification.js +51 -10
- package/skills-templates/chore-planning/SKILL.md +47 -18
- package/skills-templates/epic-planning/SKILL.md +68 -48
- package/skills-templates/external-transition/SKILL.md +47 -47
- package/skills-templates/feature-planning/SKILL.md +83 -73
- package/skills-templates/production-mode/SKILL.md +49 -49
- package/skills-templates/request-routing/SKILL.md +27 -14
- package/skills-templates/simple-improvement/SKILL.md +68 -44
- package/skills-templates/speed-mode/SKILL.md +209 -128
- package/skills-templates/stable-mode/SKILL.md +105 -94
- package/templates/bdd-guidance.md +139 -0
- package/templates/bdd-scaffolding/wait.js +18 -0
- package/templates/bdd-scaffolding/world.js +19 -0
- package/.jettypod-backup/work.db +0 -0
- package/apps/dashboard/app/access-code/page.tsx +0 -110
- package/lib/discovery-checkpoint.js +0 -123
- package/skills-templates/project-discovery/SKILL.md +0 -372
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { SubscribeContent } from '@/components/SubscribeContent';
|
|
4
|
+
|
|
5
|
+
export default function SubscribePage() {
|
|
6
|
+
return (
|
|
7
|
+
<div className="flex flex-col items-center justify-center min-h-screen bg-white dark:bg-zinc-900 p-8">
|
|
8
|
+
<SubscribeContent />
|
|
9
|
+
</div>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
@@ -22,6 +22,28 @@ export default function WelcomePage() {
|
|
|
22
22
|
loadRecentProjects();
|
|
23
23
|
}, []);
|
|
24
24
|
|
|
25
|
+
const handleNewProject = async () => {
|
|
26
|
+
setError(null);
|
|
27
|
+
|
|
28
|
+
if (!window.electronAPI?.isElectron) {
|
|
29
|
+
setError('Project creation is only available in the desktop app.');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const result = await window.electronAPI.project.newProject();
|
|
34
|
+
|
|
35
|
+
if (result.canceled) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!result.success) {
|
|
40
|
+
setError(result.error || 'Failed to create project');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
window.location.href = '/';
|
|
45
|
+
};
|
|
46
|
+
|
|
25
47
|
const handleOpenProject = async () => {
|
|
26
48
|
setError(null);
|
|
27
49
|
|
|
@@ -70,12 +92,13 @@ export default function WelcomePage() {
|
|
|
70
92
|
return (
|
|
71
93
|
<>
|
|
72
94
|
{error && (
|
|
73
|
-
<div className="fixed top-4 left-1/2 transform -translate-x-1/2 bg-red-
|
|
95
|
+
<div className="fixed top-4 left-1/2 transform -translate-x-1/2 bg-red-50 dark:bg-red-900/20 border-2 border-red-200 dark:border-red-800 text-red-700 dark:text-red-400 px-4 py-3 rounded-xl text-base z-50">
|
|
74
96
|
{error}
|
|
75
97
|
</div>
|
|
76
98
|
)}
|
|
77
99
|
<WelcomeScreen
|
|
78
100
|
recentProjects={recentProjects}
|
|
101
|
+
onNewProject={handleNewProject}
|
|
79
102
|
onOpenProject={handleOpenProject}
|
|
80
103
|
onSelectRecentProject={handleSelectRecentProject}
|
|
81
104
|
/>
|
|
@@ -5,27 +5,9 @@ import { EditableDetailTitle } from '@/components/EditableDetailTitle';
|
|
|
5
5
|
import { EditableDetailDescription } from '@/components/EditableDetailDescription';
|
|
6
6
|
import Link from 'next/link';
|
|
7
7
|
import { notFound } from 'next/navigation';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
feature: { icon: '✨', label: 'Feature' },
|
|
12
|
-
chore: { icon: '🔧', label: 'Chore' },
|
|
13
|
-
bug: { icon: '🐛', label: 'Bug' },
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const statusLabels: Record<string, { label: string; color: string }> = {
|
|
17
|
-
backlog: { label: 'Backlog', color: 'bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300' },
|
|
18
|
-
todo: { label: 'Todo', color: 'bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300' },
|
|
19
|
-
in_progress: { label: 'In Progress', color: 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300' },
|
|
20
|
-
done: { label: 'Done', color: 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300' },
|
|
21
|
-
cancelled: { label: 'Cancelled', color: 'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300' },
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const modeLabels: Record<string, { label: string; color: string }> = {
|
|
25
|
-
speed: { label: 'Speed Mode', color: 'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200' },
|
|
26
|
-
stable: { label: 'Stable Mode', color: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' },
|
|
27
|
-
production: { label: 'Production Mode', color: 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200' },
|
|
28
|
-
};
|
|
8
|
+
import { TYPE_LABELS, STATUS_LABELS, MODE_LABELS_FULL } from '@/lib/constants';
|
|
9
|
+
import { TypeIcon } from '@/components/TypeIcon';
|
|
10
|
+
import { DetailReviewActions } from '@/components/DetailReviewActions';
|
|
29
11
|
|
|
30
12
|
interface PageProps {
|
|
31
13
|
params: Promise<{ id: string }>;
|
|
@@ -49,15 +31,15 @@ export default async function WorkItemPage({ params }: PageProps) {
|
|
|
49
31
|
const decisions = getDecisionsForWorkItem(workItemId);
|
|
50
32
|
const parentItem = item.parent_id ? getWorkItem(item.parent_id) : null;
|
|
51
33
|
|
|
52
|
-
const typeInfo =
|
|
53
|
-
const statusInfo =
|
|
34
|
+
const typeInfo = TYPE_LABELS[item.type] || { icon: '📄', label: 'Item' };
|
|
35
|
+
const statusInfo = STATUS_LABELS[item.status] || STATUS_LABELS.backlog;
|
|
54
36
|
|
|
55
37
|
return (
|
|
56
38
|
<div className="min-h-screen bg-zinc-50 dark:bg-zinc-950">
|
|
57
39
|
{/* Header */}
|
|
58
40
|
<header className="border-b border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900">
|
|
59
|
-
<div className="max-w-4xl mx-auto px-
|
|
60
|
-
<Link href="/" className="text-
|
|
41
|
+
<div className="max-w-4xl mx-auto px-6 py-6">
|
|
42
|
+
<Link href="/" className="text-[#5a7d7f] dark:text-[#a3bfc0] hover:underline text-base">
|
|
61
43
|
← Back to Dashboard
|
|
62
44
|
</Link>
|
|
63
45
|
</div>
|
|
@@ -66,9 +48,9 @@ export default async function WorkItemPage({ params }: PageProps) {
|
|
|
66
48
|
<main className="max-w-4xl mx-auto px-4 py-6">
|
|
67
49
|
{/* Breadcrumb */}
|
|
68
50
|
{parentItem && (
|
|
69
|
-
<div className="mb-
|
|
70
|
-
<Link href={`/work/${parentItem.id}`} className="hover:underline">
|
|
71
|
-
{
|
|
51
|
+
<div className="mb-6 text-base text-zinc-500">
|
|
52
|
+
<Link href={`/work/${parentItem.id}`} className="inline-flex items-center gap-1.5 hover:underline">
|
|
53
|
+
<TypeIcon type={parentItem.type} className="w-6 h-6" /> #{parentItem.id} {parentItem.title}
|
|
72
54
|
</Link>
|
|
73
55
|
<span className="mx-2">→</span>
|
|
74
56
|
</div>
|
|
@@ -77,45 +59,47 @@ export default async function WorkItemPage({ params }: PageProps) {
|
|
|
77
59
|
{/* Main card */}
|
|
78
60
|
<div className="bg-white dark:bg-zinc-900 rounded-lg border border-zinc-200 dark:border-zinc-800 overflow-hidden">
|
|
79
61
|
{/* Header */}
|
|
80
|
-
<div className="px-
|
|
81
|
-
<div className="flex items-start justify-between gap-
|
|
62
|
+
<div className="px-8 py-6 border-b border-zinc-200 dark:border-zinc-800">
|
|
63
|
+
<div className="flex items-start justify-between gap-6">
|
|
82
64
|
<div className="flex-1 min-w-0">
|
|
83
65
|
<WorkItemHeader
|
|
84
66
|
id={item.id}
|
|
85
67
|
title={item.title}
|
|
86
68
|
type={item.type}
|
|
87
|
-
typeIcon={typeInfo.icon}
|
|
88
69
|
typeLabel={typeInfo.label}
|
|
89
70
|
/>
|
|
90
71
|
<EditableDetailTitle title={item.title} itemId={item.id} />
|
|
91
72
|
</div>
|
|
92
73
|
<div className="flex items-center gap-2">
|
|
93
|
-
{item.mode &&
|
|
94
|
-
<span className={`text-
|
|
95
|
-
{
|
|
74
|
+
{item.mode && MODE_LABELS_FULL[item.mode] && (
|
|
75
|
+
<span className={`text-base px-3 py-1.5 rounded ${MODE_LABELS_FULL[item.mode].color}`}>
|
|
76
|
+
{MODE_LABELS_FULL[item.mode].label}
|
|
96
77
|
</span>
|
|
97
78
|
)}
|
|
98
|
-
<span className={`text-
|
|
79
|
+
<span className={`text-base px-3 py-1.5 rounded ${statusInfo.color}`}>
|
|
99
80
|
{statusInfo.label}
|
|
100
81
|
</span>
|
|
82
|
+
{!!item.ready_for_review && (
|
|
83
|
+
<DetailReviewActions workItemId={item.id} />
|
|
84
|
+
)}
|
|
101
85
|
</div>
|
|
102
86
|
</div>
|
|
103
87
|
</div>
|
|
104
88
|
|
|
105
89
|
{/* Description */}
|
|
106
|
-
<div className="px-
|
|
107
|
-
<h2 className="text-
|
|
90
|
+
<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">
|
|
108
92
|
Description
|
|
109
93
|
</h2>
|
|
110
94
|
<EditableDetailDescription description={item.description} itemId={item.id} />
|
|
111
95
|
</div>
|
|
112
96
|
|
|
113
97
|
{/* Metadata */}
|
|
114
|
-
<div className="px-
|
|
115
|
-
<h2 className="text-
|
|
98
|
+
<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">
|
|
116
100
|
Details
|
|
117
101
|
</h2>
|
|
118
|
-
<dl className="grid grid-cols-2 gap-
|
|
102
|
+
<dl className="grid grid-cols-2 gap-6 text-base">
|
|
119
103
|
{item.branch_name && (
|
|
120
104
|
<div>
|
|
121
105
|
<dt className="text-zinc-500">Branch</dt>
|
|
@@ -147,18 +131,18 @@ export default async function WorkItemPage({ params }: PageProps) {
|
|
|
147
131
|
|
|
148
132
|
{/* Decisions */}
|
|
149
133
|
{decisions.length > 0 && (
|
|
150
|
-
<div className="px-
|
|
151
|
-
<h2 className="text-
|
|
134
|
+
<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">
|
|
152
136
|
Decisions
|
|
153
137
|
</h2>
|
|
154
|
-
<div className="space-y-
|
|
138
|
+
<div className="space-y-6">
|
|
155
139
|
{decisions.map((decision) => (
|
|
156
|
-
<div key={decision.id} className="bg-zinc-50 dark:bg-zinc-800 rounded-lg p-
|
|
157
|
-
<div className="flex items-center gap-
|
|
158
|
-
<span className="text-
|
|
140
|
+
<div key={decision.id} className="bg-zinc-50 dark:bg-zinc-800 rounded-lg p-6">
|
|
141
|
+
<div className="flex items-center gap-3 mb-3">
|
|
142
|
+
<span className="text-base font-medium text-zinc-900 dark:text-zinc-100">
|
|
159
143
|
{decision.aspect}
|
|
160
144
|
</span>
|
|
161
|
-
<span className="text-
|
|
145
|
+
<span className="text-base text-zinc-500">
|
|
162
146
|
{new Date(decision.created_at).toLocaleDateString()}
|
|
163
147
|
</span>
|
|
164
148
|
</div>
|
|
@@ -166,7 +150,7 @@ export default async function WorkItemPage({ params }: PageProps) {
|
|
|
166
150
|
{decision.decision}
|
|
167
151
|
</p>
|
|
168
152
|
{decision.rationale && (
|
|
169
|
-
<p className="text-
|
|
153
|
+
<p className="text-base text-zinc-500 mt-1.5">
|
|
170
154
|
{decision.rationale}
|
|
171
155
|
</p>
|
|
172
156
|
)}
|
|
@@ -178,8 +162,8 @@ export default async function WorkItemPage({ params }: PageProps) {
|
|
|
178
162
|
|
|
179
163
|
{/* Children */}
|
|
180
164
|
{children.length > 0 && (
|
|
181
|
-
<div className="px-
|
|
182
|
-
<h2 className="text-
|
|
165
|
+
<div className="px-8 py-6">
|
|
166
|
+
<h2 className="text-base font-semibold text-zinc-500 uppercase tracking-wide mb-4">
|
|
183
167
|
{item.type === 'epic' ? 'Features & Chores' : 'Child Items'} ({children.length})
|
|
184
168
|
</h2>
|
|
185
169
|
<WorkItemTree items={children.map(c => ({ ...c, children: [] }))} />
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { usePathname, useRouter } from 'next/navigation';
|
|
5
|
+
import { ClaudeSessionProvider, useSessionState, useSessionActions } from '../contexts/ClaudeSessionContext';
|
|
6
|
+
import { ConnectionStatusProvider, useConnectionStatus } from '../contexts/ConnectionStatusContext';
|
|
7
|
+
import { UsageProvider } from '../contexts/UsageContext';
|
|
6
8
|
import { ToastProvider } from './Toast';
|
|
7
9
|
import { MainNav } from './MainNav';
|
|
8
10
|
import { ClaudePanel } from './ClaudePanel';
|
|
11
|
+
import { JettyLoader } from './JettyLoader';
|
|
12
|
+
import { LazyMotion, domAnimation, m, AnimatePresence } from 'framer-motion';
|
|
9
13
|
import type { ReactNode } from 'react';
|
|
10
14
|
|
|
11
15
|
// Pages that should not show the nav header (pre-project screens)
|
|
12
|
-
const NO_NAV_PATHS = ['/install-claude', '/welcome'];
|
|
16
|
+
const NO_NAV_PATHS = ['/login', '/signup', '/subscribe', '/install-claude', '/connect-claude', '/welcome'];
|
|
17
|
+
|
|
18
|
+
// Pages accessible without authentication
|
|
19
|
+
const PUBLIC_PATHS = ['/login', '/signup', '/subscribe', '/install-claude', '/connect-claude', '/welcome', '/design-system'];
|
|
13
20
|
|
|
14
21
|
interface AppShellProps {
|
|
15
22
|
projectName: string;
|
|
@@ -18,74 +25,107 @@ interface AppShellProps {
|
|
|
18
25
|
|
|
19
26
|
function AppShellContent({ projectName, children }: AppShellProps) {
|
|
20
27
|
const pathname = usePathname();
|
|
28
|
+
const router = useRouter();
|
|
21
29
|
const showNav = !NO_NAV_PATHS.includes(pathname);
|
|
30
|
+
const [authChecked, setAuthChecked] = useState(false);
|
|
31
|
+
const [minLoadingDone, setMinLoadingDone] = useState(false);
|
|
32
|
+
const { showDisconnected, status: connectionStatus } = useConnectionStatus();
|
|
33
|
+
|
|
34
|
+
// Ensure the loading screen shows for at least 3 seconds
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
const timer = setTimeout(() => setMinLoadingDone(true), 3000);
|
|
37
|
+
return () => clearTimeout(timer);
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
// Auth enforcement gate: redirect unauthenticated users to /signup (first-time) or /login (returning)
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (PUBLIC_PATHS.includes(pathname)) {
|
|
43
|
+
setAuthChecked(true);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function checkAuth() {
|
|
48
|
+
if (typeof window !== 'undefined' && window.electronAPI?.isElectron) {
|
|
49
|
+
try {
|
|
50
|
+
const status = await window.electronAPI.auth.getStatus();
|
|
51
|
+
if (!status.authenticated) {
|
|
52
|
+
const hasLoggedIn = await window.electronAPI.auth.hasLoggedInBefore?.();
|
|
53
|
+
router.push(hasLoggedIn ? '/login' : '/signup');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// Corrupted auth file, IPC error, etc. — treat as unauthenticated
|
|
58
|
+
router.push('/login');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
setAuthChecked(true);
|
|
63
|
+
}
|
|
64
|
+
checkAuth();
|
|
65
|
+
}, [pathname, router]);
|
|
22
66
|
|
|
23
|
-
const {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
activeSession,
|
|
28
|
-
sessions,
|
|
29
|
-
standaloneSessions,
|
|
30
|
-
messages,
|
|
31
|
-
status,
|
|
32
|
-
error,
|
|
33
|
-
exitCode,
|
|
34
|
-
canRetry,
|
|
35
|
-
switchSession,
|
|
36
|
-
closeSession,
|
|
37
|
-
createNewSession,
|
|
38
|
-
sendMessage,
|
|
39
|
-
retry,
|
|
40
|
-
stop,
|
|
41
|
-
narratedMode,
|
|
42
|
-
toggleNarratedMode,
|
|
43
|
-
} = useClaudeSession();
|
|
67
|
+
const { claudePanelOpen } = useSessionState();
|
|
68
|
+
const { setClaudePanelOpen } = useSessionActions();
|
|
69
|
+
|
|
70
|
+
const showLoading = !authChecked || !minLoadingDone;
|
|
44
71
|
|
|
45
72
|
return (
|
|
46
|
-
<
|
|
73
|
+
<AnimatePresence mode="wait">
|
|
74
|
+
{showLoading ? (
|
|
75
|
+
<m.div
|
|
76
|
+
key="loader"
|
|
77
|
+
initial={{ opacity: 0 }}
|
|
78
|
+
animate={{ opacity: 1 }}
|
|
79
|
+
exit={{ opacity: 0 }}
|
|
80
|
+
transition={{ duration: 0.3 }}
|
|
81
|
+
className="h-screen flex items-center justify-center bg-background"
|
|
82
|
+
>
|
|
83
|
+
<JettyLoader size={80} />
|
|
84
|
+
</m.div>
|
|
85
|
+
) : (
|
|
86
|
+
<m.div
|
|
87
|
+
key="content"
|
|
88
|
+
initial={{ opacity: 0 }}
|
|
89
|
+
animate={{ opacity: 1 }}
|
|
90
|
+
transition={{ duration: 0.3 }}
|
|
91
|
+
className="h-screen flex flex-col overflow-hidden">
|
|
47
92
|
{showNav && <MainNav projectName={projectName} />}
|
|
48
|
-
|
|
93
|
+
{showNav && showDisconnected && (
|
|
94
|
+
<div className="bg-amber-50 dark:bg-amber-950 border-b border-amber-200 dark:border-amber-800 px-4 py-2 text-center text-base text-amber-800 dark:text-amber-200" data-testid="disconnect-banner">
|
|
95
|
+
{connectionStatus === 'reconnecting'
|
|
96
|
+
? 'Reconnecting to live updates...'
|
|
97
|
+
: 'Live updates disconnected. Changes may not appear automatically.'}
|
|
98
|
+
</div>
|
|
99
|
+
)}
|
|
100
|
+
<main className={`flex-1 flex flex-col min-h-0 overflow-y-auto transition-[margin] duration-200 ease-out ${showNav && claudePanelOpen ? 'mr-[480px]' : ''}`}>
|
|
49
101
|
{children}
|
|
50
102
|
</main>
|
|
51
103
|
{showNav && (
|
|
52
104
|
<ClaudePanel
|
|
53
105
|
isOpen={claudePanelOpen}
|
|
54
|
-
workItemId={activeSessionId || 'sessions'}
|
|
55
|
-
workItemTitle={activeSession?.title || 'Claude Sessions'}
|
|
56
|
-
messages={messages}
|
|
57
|
-
status={status}
|
|
58
|
-
error={error}
|
|
59
|
-
exitCode={exitCode}
|
|
60
|
-
canRetry={canRetry}
|
|
61
106
|
onClose={() => setClaudePanelOpen(false)}
|
|
62
|
-
onRetry={retry}
|
|
63
|
-
onSendMessage={sendMessage}
|
|
64
|
-
onStop={stop}
|
|
65
|
-
sessions={sessions}
|
|
66
|
-
activeSessionId={activeSessionId}
|
|
67
|
-
onSwitchSession={switchSession}
|
|
68
|
-
standaloneSessions={standaloneSessions}
|
|
69
|
-
onNewSession={createNewSession}
|
|
70
|
-
onCloseSession={closeSession}
|
|
71
|
-
narratedMode={narratedMode}
|
|
72
|
-
onToggleNarratedMode={toggleNarratedMode}
|
|
73
107
|
/>
|
|
74
108
|
)}
|
|
75
|
-
</div>
|
|
109
|
+
</m.div>
|
|
110
|
+
)}
|
|
111
|
+
</AnimatePresence>
|
|
76
112
|
);
|
|
77
113
|
}
|
|
78
114
|
|
|
79
115
|
export function AppShell({ projectName, children }: AppShellProps) {
|
|
80
116
|
return (
|
|
81
|
-
<
|
|
82
|
-
<
|
|
83
|
-
<
|
|
84
|
-
<
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
117
|
+
<LazyMotion features={domAnimation} strict>
|
|
118
|
+
<ConnectionStatusProvider>
|
|
119
|
+
<UsageProvider>
|
|
120
|
+
<ToastProvider>
|
|
121
|
+
<ClaudeSessionProvider>
|
|
122
|
+
<AppShellContent projectName={projectName}>
|
|
123
|
+
{children}
|
|
124
|
+
</AppShellContent>
|
|
125
|
+
</ClaudeSessionProvider>
|
|
126
|
+
</ToastProvider>
|
|
127
|
+
</UsageProvider>
|
|
128
|
+
</ConnectionStatusProvider>
|
|
129
|
+
</LazyMotion>
|
|
90
130
|
);
|
|
91
131
|
}
|
|
@@ -7,14 +7,19 @@ interface CardMenuProps {
|
|
|
7
7
|
itemId: number;
|
|
8
8
|
itemTitle?: string;
|
|
9
9
|
itemType?: string;
|
|
10
|
+
itemDescription?: string | null;
|
|
11
|
+
conversational?: boolean;
|
|
10
12
|
currentStatus: string;
|
|
11
13
|
onStatusChange: (id: number, newStatus: string) => Promise<void>;
|
|
12
|
-
onTriggerClaude?: (id: number, title: string, type: string) => void;
|
|
14
|
+
onTriggerClaude?: (id: number, title: string, type: string, conversational?: boolean, description?: string | null) => void;
|
|
13
15
|
hasActiveSession?: boolean;
|
|
14
16
|
onOpenSession?: (id: string) => void;
|
|
17
|
+
usageAllowed?: boolean;
|
|
18
|
+
onEditName?: () => void;
|
|
19
|
+
onUnaccept?: () => void;
|
|
15
20
|
}
|
|
16
21
|
|
|
17
|
-
export function CardMenu({ itemId, itemTitle = '', itemType = 'chore', currentStatus, onStatusChange, onTriggerClaude, hasActiveSession, onOpenSession }: CardMenuProps) {
|
|
22
|
+
export function CardMenu({ itemId, itemTitle = '', itemType = 'chore', itemDescription, conversational = false, currentStatus, onStatusChange, onTriggerClaude, hasActiveSession, onOpenSession, usageAllowed = true, onEditName, onUnaccept }: CardMenuProps) {
|
|
18
23
|
const [isOpen, setIsOpen] = useState(false);
|
|
19
24
|
const [error, setError] = useState<string | null>(null);
|
|
20
25
|
const [dropdownPosition, setDropdownPosition] = useState<{ top: number; left: number } | null>(null);
|
|
@@ -74,7 +79,7 @@ export function CardMenu({ itemId, itemTitle = '', itemType = 'chore', currentSt
|
|
|
74
79
|
await onStatusChange(itemId, 'in_progress');
|
|
75
80
|
// Trigger Claude panel after status change succeeds
|
|
76
81
|
if (onTriggerClaude) {
|
|
77
|
-
onTriggerClaude(itemId, itemTitle, itemType);
|
|
82
|
+
onTriggerClaude(itemId, itemTitle, itemType, conversational, itemDescription);
|
|
78
83
|
}
|
|
79
84
|
} catch (err) {
|
|
80
85
|
setError(err instanceof Error ? err.message : 'Failed to update status');
|
|
@@ -89,40 +94,68 @@ export function CardMenu({ itemId, itemTitle = '', itemType = 'chore', currentSt
|
|
|
89
94
|
}
|
|
90
95
|
};
|
|
91
96
|
|
|
97
|
+
const handleEditName = (e: React.MouseEvent) => {
|
|
98
|
+
e.stopPropagation();
|
|
99
|
+
setIsOpen(false);
|
|
100
|
+
onEditName?.();
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const handleUnaccept = (e: React.MouseEvent) => {
|
|
104
|
+
e.stopPropagation();
|
|
105
|
+
setIsOpen(false);
|
|
106
|
+
onUnaccept?.();
|
|
107
|
+
};
|
|
108
|
+
|
|
92
109
|
const dropdownContent = (
|
|
93
110
|
<div
|
|
94
111
|
ref={dropdownRef}
|
|
95
|
-
className="fixed z-50 bg-white dark:bg-zinc-800 rounded-lg shadow-lg
|
|
112
|
+
className="fixed z-50 bg-white dark:bg-zinc-800 rounded-lg shadow-lg py-1.5 min-w-[140px]"
|
|
96
113
|
style={{
|
|
97
114
|
top: dropdownPosition?.top ?? 0,
|
|
98
115
|
left: dropdownPosition?.left ?? 0,
|
|
99
116
|
}}
|
|
100
117
|
data-testid="status-dropdown"
|
|
101
118
|
>
|
|
119
|
+
{onEditName && (
|
|
120
|
+
<button
|
|
121
|
+
onClick={handleEditName}
|
|
122
|
+
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"
|
|
123
|
+
data-testid="edit-name-button"
|
|
124
|
+
>
|
|
125
|
+
<span className="text-zinc-500">✏️</span>
|
|
126
|
+
Edit name
|
|
127
|
+
</button>
|
|
128
|
+
)}
|
|
102
129
|
{(currentStatus === 'backlog' || currentStatus === 'cancelled') && (
|
|
103
130
|
<button
|
|
104
131
|
onClick={handleStart}
|
|
105
|
-
|
|
132
|
+
disabled={!usageAllowed}
|
|
133
|
+
className={`w-full px-4 py-3 text-left text-base flex items-center gap-3 ${
|
|
134
|
+
usageAllowed
|
|
135
|
+
? 'text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700'
|
|
136
|
+
: 'text-zinc-400 dark:text-zinc-600 cursor-not-allowed'
|
|
137
|
+
}`}
|
|
106
138
|
data-testid="start-button"
|
|
139
|
+
title={!usageAllowed ? 'Weekly usage limit reached' : undefined}
|
|
107
140
|
>
|
|
108
|
-
<span className=
|
|
141
|
+
<span className={usageAllowed ? 'text-[#819D9F]' : 'text-zinc-400 dark:text-zinc-600'}>▶</span>
|
|
109
142
|
Start
|
|
110
143
|
</button>
|
|
111
144
|
)}
|
|
112
145
|
{hasActiveSession && (
|
|
113
146
|
<button
|
|
114
147
|
onClick={handleOpenSession}
|
|
115
|
-
className="w-full px-
|
|
148
|
+
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"
|
|
116
149
|
data-testid="open-session-button"
|
|
117
150
|
>
|
|
118
|
-
<span className="text-
|
|
151
|
+
<span className="text-[#819D9F]">💬</span>
|
|
119
152
|
Open session
|
|
120
153
|
</button>
|
|
121
154
|
)}
|
|
122
155
|
{currentStatus !== 'done' && (
|
|
123
156
|
<button
|
|
124
157
|
onClick={(e) => handleAction(e, 'done')}
|
|
125
|
-
className="w-full px-
|
|
158
|
+
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"
|
|
126
159
|
data-testid="mark-done-button"
|
|
127
160
|
>
|
|
128
161
|
<span className="text-green-500">✓</span>
|
|
@@ -132,17 +165,27 @@ export function CardMenu({ itemId, itemTitle = '', itemType = 'chore', currentSt
|
|
|
132
165
|
{(currentStatus === 'in_progress' || currentStatus === 'done') && (
|
|
133
166
|
<button
|
|
134
167
|
onClick={(e) => handleAction(e, 'backlog')}
|
|
135
|
-
className="w-full px-
|
|
168
|
+
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"
|
|
136
169
|
data-testid="unstart-button"
|
|
137
170
|
>
|
|
138
171
|
<span className="text-amber-500">↩</span>
|
|
139
172
|
Unstart
|
|
140
173
|
</button>
|
|
141
174
|
)}
|
|
175
|
+
{currentStatus === 'done' && onUnaccept && (
|
|
176
|
+
<button
|
|
177
|
+
onClick={handleUnaccept}
|
|
178
|
+
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"
|
|
179
|
+
data-testid="unaccept-button"
|
|
180
|
+
>
|
|
181
|
+
<span className="text-red-500">↩</span>
|
|
182
|
+
Unaccept
|
|
183
|
+
</button>
|
|
184
|
+
)}
|
|
142
185
|
{currentStatus !== 'cancelled' && (
|
|
143
186
|
<button
|
|
144
187
|
onClick={(e) => handleAction(e, 'cancelled')}
|
|
145
|
-
className="w-full px-
|
|
188
|
+
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"
|
|
146
189
|
data-testid="cancel-button"
|
|
147
190
|
>
|
|
148
191
|
<span className="text-red-500">✕</span>
|
|
@@ -157,7 +200,7 @@ export function CardMenu({ itemId, itemTitle = '', itemType = 'chore', currentSt
|
|
|
157
200
|
<button
|
|
158
201
|
ref={buttonRef}
|
|
159
202
|
onClick={handleMenuClick}
|
|
160
|
-
className="p-1 rounded hover:bg-zinc-200 dark:hover:bg-zinc-600 transition-colors"
|
|
203
|
+
className="p-1 rounded hover:bg-zinc-200 dark:hover:bg-zinc-600 transition-colors duration-200 ease-out"
|
|
161
204
|
aria-label="Card menu"
|
|
162
205
|
data-testid="menu-button"
|
|
163
206
|
>
|
|
@@ -172,7 +215,7 @@ export function CardMenu({ itemId, itemTitle = '', itemType = 'chore', currentSt
|
|
|
172
215
|
|
|
173
216
|
{error && (
|
|
174
217
|
<div
|
|
175
|
-
className="absolute right-0 top-full mt-1 z-10 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 text-xs px-
|
|
218
|
+
className="absolute right-0 top-full mt-1 z-10 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 text-xs px-3 py-1.5 rounded border-2 border-red-200 dark:border-red-800"
|
|
176
219
|
data-testid="error-message"
|
|
177
220
|
>
|
|
178
221
|
{error}
|