jettypod 4.4.118 → 4.4.121

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/.env +4 -3
  2. package/Cargo.lock +6450 -0
  3. package/Cargo.toml +35 -0
  4. package/README.md +5 -1
  5. package/TAURI-MIGRATION-PLAN.md +840 -0
  6. package/apps/dashboard/app/connect-claude/page.tsx +5 -6
  7. package/apps/dashboard/app/decision/[id]/page.tsx +63 -58
  8. package/apps/dashboard/app/demo/gates/page.tsx +43 -45
  9. package/apps/dashboard/app/design-system/page.tsx +868 -0
  10. package/apps/dashboard/app/globals.css +80 -4
  11. package/apps/dashboard/app/install-claude/page.tsx +4 -6
  12. package/apps/dashboard/app/login/page.tsx +72 -54
  13. package/apps/dashboard/app/page.tsx +101 -48
  14. package/apps/dashboard/app/settings/page.tsx +61 -13
  15. package/apps/dashboard/app/signup/page.tsx +242 -0
  16. package/apps/dashboard/app/subscribe/page.tsx +0 -2
  17. package/apps/dashboard/app/tests/page.tsx +37 -4
  18. package/apps/dashboard/app/welcome/page.tsx +13 -16
  19. package/apps/dashboard/app/work/[id]/page.tsx +117 -118
  20. package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
  21. package/apps/dashboard/components/AppShell.tsx +92 -85
  22. package/apps/dashboard/components/CardMenu.tsx +45 -12
  23. package/apps/dashboard/components/ClaudePanel.tsx +771 -850
  24. package/apps/dashboard/components/ClaudePanelInput.tsx +43 -15
  25. package/apps/dashboard/components/ConnectClaudeScreen.tsx +17 -34
  26. package/apps/dashboard/components/CopyableId.tsx +3 -4
  27. package/apps/dashboard/components/DetailReviewActions.tsx +100 -0
  28. package/apps/dashboard/components/DragContext.tsx +134 -63
  29. package/apps/dashboard/components/DraggableCard.tsx +3 -5
  30. package/apps/dashboard/components/DropZone.tsx +6 -7
  31. package/apps/dashboard/components/EditableDetailDescription.tsx +7 -13
  32. package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
  33. package/apps/dashboard/components/EditableTitle.tsx +26 -7
  34. package/apps/dashboard/components/ElapsedTimer.tsx +66 -0
  35. package/apps/dashboard/components/EpicGroup.tsx +359 -0
  36. package/apps/dashboard/components/GateCard.tsx +79 -17
  37. package/apps/dashboard/components/GateChoiceCard.tsx +15 -18
  38. package/apps/dashboard/components/InstallClaudeScreen.tsx +15 -32
  39. package/apps/dashboard/components/JettyLoader.tsx +37 -0
  40. package/apps/dashboard/components/KanbanBoard.tsx +368 -958
  41. package/apps/dashboard/components/KanbanCard.tsx +740 -0
  42. package/apps/dashboard/components/LazyCard.tsx +62 -0
  43. package/apps/dashboard/components/LazyMarkdown.tsx +11 -0
  44. package/apps/dashboard/components/MainNav.tsx +38 -73
  45. package/apps/dashboard/components/MessageBlock.tsx +468 -0
  46. package/apps/dashboard/components/ModeStartCard.tsx +15 -16
  47. package/apps/dashboard/components/OnboardingWelcome.tsx +213 -0
  48. package/apps/dashboard/components/PlaceholderCard.tsx +3 -4
  49. package/apps/dashboard/components/ProjectSwitcher.tsx +30 -30
  50. package/apps/dashboard/components/PrototypeTimeline.tsx +72 -51
  51. package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +406 -388
  52. package/apps/dashboard/components/RealTimeTestsWrapper.tsx +373 -235
  53. package/apps/dashboard/components/ReviewFooter.tsx +139 -0
  54. package/apps/dashboard/components/SessionList.tsx +19 -19
  55. package/apps/dashboard/components/SubscribeContent.tsx +91 -47
  56. package/apps/dashboard/components/TestTree.tsx +16 -16
  57. package/apps/dashboard/components/TipCard.tsx +16 -17
  58. package/apps/dashboard/components/Toast.tsx +5 -6
  59. package/apps/dashboard/components/TypeIcon.tsx +55 -0
  60. package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
  61. package/apps/dashboard/components/WaveCompletionAnimation.tsx +52 -65
  62. package/apps/dashboard/components/WelcomeScreen.tsx +19 -35
  63. package/apps/dashboard/components/WorkItemHeader.tsx +4 -5
  64. package/apps/dashboard/components/WorkItemTree.tsx +11 -32
  65. package/apps/dashboard/components/settings/AccountSection.tsx +55 -35
  66. package/apps/dashboard/components/settings/AiContextSection.tsx +89 -0
  67. package/apps/dashboard/components/settings/ContextDocumentsSection.tsx +317 -0
  68. package/apps/dashboard/components/settings/EnvVarsSection.tsx +74 -152
  69. package/apps/dashboard/components/settings/GeneralSection.tsx +162 -56
  70. package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
  71. package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -5
  72. package/apps/dashboard/components/ui/Button.tsx +104 -0
  73. package/apps/dashboard/components/ui/Input.tsx +78 -0
  74. package/apps/dashboard/components.json +1 -1
  75. package/apps/dashboard/contexts/ClaudeSessionContext.tsx +711 -418
  76. package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -5
  77. package/apps/dashboard/contexts/UsageContext.tsx +87 -32
  78. package/apps/dashboard/dev.sh +35 -0
  79. package/apps/dashboard/eslint.config.mjs +9 -9
  80. package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
  81. package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
  82. package/apps/dashboard/hooks/useWebSocket.ts +138 -83
  83. package/apps/dashboard/index.html +73 -0
  84. package/apps/dashboard/lib/constants.ts +43 -0
  85. package/apps/dashboard/lib/data-bridge.ts +722 -0
  86. package/apps/dashboard/lib/db.ts +69 -1265
  87. package/apps/dashboard/lib/environment-config.ts +173 -0
  88. package/apps/dashboard/lib/environment-verification.ts +119 -0
  89. package/apps/dashboard/lib/kanban-utils.ts +270 -0
  90. package/apps/dashboard/lib/proof-run.ts +495 -0
  91. package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
  92. package/apps/dashboard/lib/run-migrations.js +27 -2
  93. package/apps/dashboard/lib/service-recovery.ts +326 -0
  94. package/apps/dashboard/lib/session-state-machine.ts +1 -0
  95. package/apps/dashboard/lib/session-state-utils.ts +0 -164
  96. package/apps/dashboard/lib/session-stream-manager.ts +308 -134
  97. package/apps/dashboard/lib/shadows.ts +7 -0
  98. package/apps/dashboard/lib/stream-manager-registry.ts +46 -6
  99. package/apps/dashboard/lib/tauri-bridge.ts +102 -0
  100. package/apps/dashboard/lib/tauri.ts +106 -0
  101. package/apps/dashboard/lib/utils.ts +6 -0
  102. package/apps/dashboard/next-env.d.ts +1 -1
  103. package/apps/dashboard/package.json +21 -32
  104. package/apps/dashboard/public/bug-icon.png +0 -0
  105. package/apps/dashboard/public/buoy-icon.png +0 -0
  106. package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
  107. package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
  108. package/apps/dashboard/public/in-flight-seagull.png +0 -0
  109. package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
  110. package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
  111. package/apps/dashboard/public/jettypod_logo.png +0 -0
  112. package/apps/dashboard/public/pier-icon.png +0 -0
  113. package/apps/dashboard/public/star-icon.png +0 -0
  114. package/apps/dashboard/public/wrench-icon.png +0 -0
  115. package/apps/dashboard/scripts/tauri-build.js +228 -0
  116. package/apps/dashboard/scripts/upload-tauri-to-r2.js +125 -0
  117. package/apps/dashboard/scripts/ws-server.js +191 -0
  118. package/apps/dashboard/src/main.tsx +12 -0
  119. package/apps/dashboard/src/router.tsx +107 -0
  120. package/apps/dashboard/src/vite-env.d.ts +1 -0
  121. package/apps/dashboard/tsconfig.json +7 -12
  122. package/apps/dashboard/tsconfig.tsbuildinfo +1 -1
  123. package/apps/dashboard/vite.config.ts +33 -0
  124. package/apps/update-server/src/index.ts +228 -80
  125. package/claude-hooks/global-guardrails.js +14 -13
  126. package/crates/jettypod-cli/Cargo.toml +19 -0
  127. package/crates/jettypod-cli/src/commands.rs +1249 -0
  128. package/crates/jettypod-cli/src/main.rs +595 -0
  129. package/crates/jettypod-core/Cargo.toml +26 -0
  130. package/crates/jettypod-core/build.rs +98 -0
  131. package/crates/jettypod-core/migrations/V1__baseline.sql +197 -0
  132. package/crates/jettypod-core/migrations/V2__work_items_indexes.sql +6 -0
  133. package/crates/jettypod-core/migrations/V3__qa_steps.sql +2 -0
  134. package/crates/jettypod-core/src/auth.rs +294 -0
  135. package/crates/jettypod-core/src/config.rs +397 -0
  136. package/crates/jettypod-core/src/db/mod.rs +507 -0
  137. package/crates/jettypod-core/src/db/recovery.rs +114 -0
  138. package/crates/jettypod-core/src/db/startup.rs +101 -0
  139. package/crates/jettypod-core/src/db/validate.rs +149 -0
  140. package/crates/jettypod-core/src/error.rs +76 -0
  141. package/crates/jettypod-core/src/git.rs +458 -0
  142. package/crates/jettypod-core/src/lib.rs +20 -0
  143. package/crates/jettypod-core/src/sessions.rs +625 -0
  144. package/crates/jettypod-core/src/skills.rs +556 -0
  145. package/crates/jettypod-core/src/work.rs +1086 -0
  146. package/crates/jettypod-core/src/worktree.rs +628 -0
  147. package/crates/jettypod-core/src/ws.rs +767 -0
  148. package/cucumber-test.cjs +6 -0
  149. package/cucumber.js +9 -3
  150. package/docs/COMMAND_REFERENCE.md +34 -0
  151. package/hooks/post-checkout +32 -75
  152. package/hooks/post-merge +111 -10
  153. package/jest.setup.js +1 -0
  154. package/jettypod.js +145 -116
  155. package/lib/bdd-preflight.js +96 -0
  156. package/lib/chore-taxonomy.js +33 -10
  157. package/lib/database.js +36 -16
  158. package/lib/db-watcher.js +1 -1
  159. package/lib/git-hooks/pre-commit +1 -1
  160. package/lib/jettypod-backup.js +27 -4
  161. package/lib/merge-lock.js +111 -253
  162. package/lib/migrations/027-plan-at-creation-column.js +3 -1
  163. package/lib/migrations/029-remove-autoincrement.js +307 -0
  164. package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
  165. package/lib/migrations/030-rejection-round-columns.js +54 -0
  166. package/lib/migrations/031-session-isolation-index.js +17 -0
  167. package/lib/migrations/index.js +47 -4
  168. package/lib/schema.js +10 -5
  169. package/lib/seed-onboarding.js +1 -1
  170. package/lib/update-command/index.js +9 -175
  171. package/lib/work-commands/index.js +144 -19
  172. package/lib/work-tracking/index.js +148 -27
  173. package/lib/worktree-diagnostics.js +16 -16
  174. package/lib/worktree-facade.js +1 -1
  175. package/lib/worktree-manager.js +8 -8
  176. package/lib/worktree-reconciler.js +5 -5
  177. package/package.json +9 -2
  178. package/scripts/ndjson-to-cucumber-json.js +152 -0
  179. package/scripts/postinstall.js +25 -0
  180. package/skills-templates/bug-mode/SKILL.md +79 -20
  181. package/skills-templates/bug-planning/SKILL.md +25 -29
  182. package/skills-templates/chore-mode/SKILL.md +171 -69
  183. package/skills-templates/chore-mode/verification.js +51 -10
  184. package/skills-templates/chore-planning/SKILL.md +47 -18
  185. package/skills-templates/design-system-selection/SKILL.md +273 -0
  186. package/skills-templates/epic-planning/SKILL.md +82 -48
  187. package/skills-templates/external-transition/SKILL.md +47 -47
  188. package/skills-templates/feature-planning/SKILL.md +173 -74
  189. package/skills-templates/production-mode/SKILL.md +69 -49
  190. package/skills-templates/request-routing/SKILL.md +4 -4
  191. package/skills-templates/simple-improvement/SKILL.md +74 -29
  192. package/skills-templates/speed-mode/SKILL.md +217 -141
  193. package/skills-templates/stable-mode/SKILL.md +148 -89
  194. package/apps/dashboard/README.md +0 -36
  195. package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -386
  196. package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
  197. package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -167
  198. package/apps/dashboard/app/api/claude/sessions/[sessionId]/content/route.ts +0 -52
  199. package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +0 -378
  200. package/apps/dashboard/app/api/claude/sessions/[sessionId]/pin/route.ts +0 -24
  201. package/apps/dashboard/app/api/claude/sessions/cleanup/route.ts +0 -34
  202. package/apps/dashboard/app/api/claude/sessions/route.ts +0 -184
  203. package/apps/dashboard/app/api/decisions/[id]/route.ts +0 -25
  204. package/apps/dashboard/app/api/internal/set-project/route.ts +0 -17
  205. package/apps/dashboard/app/api/kanban/route.ts +0 -15
  206. package/apps/dashboard/app/api/settings/env-vars/route.ts +0 -125
  207. package/apps/dashboard/app/api/settings/general/route.ts +0 -21
  208. package/apps/dashboard/app/api/tests/route.ts +0 -9
  209. package/apps/dashboard/app/api/tests/run/route.ts +0 -82
  210. package/apps/dashboard/app/api/tests/run/stream/route.ts +0 -71
  211. package/apps/dashboard/app/api/tests/undefined/route.ts +0 -9
  212. package/apps/dashboard/app/api/usage/route.ts +0 -17
  213. package/apps/dashboard/app/api/work/[id]/description/route.ts +0 -21
  214. package/apps/dashboard/app/api/work/[id]/epic/route.ts +0 -21
  215. package/apps/dashboard/app/api/work/[id]/order/route.ts +0 -21
  216. package/apps/dashboard/app/api/work/[id]/status/route.ts +0 -21
  217. package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
  218. package/apps/dashboard/app/layout.tsx +0 -43
  219. package/apps/dashboard/components/UpgradeBanner.tsx +0 -29
  220. package/apps/dashboard/electron/ipc-handlers.js +0 -1028
  221. package/apps/dashboard/electron/main.js +0 -2124
  222. package/apps/dashboard/electron/preload.js +0 -123
  223. package/apps/dashboard/electron/session-manager.js +0 -141
  224. package/apps/dashboard/electron-builder.config.js +0 -357
  225. package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
  226. package/apps/dashboard/lib/claude-process-manager.ts +0 -492
  227. package/apps/dashboard/lib/db-bridge.ts +0 -282
  228. package/apps/dashboard/lib/prototypes.ts +0 -202
  229. package/apps/dashboard/lib/test-results-db.ts +0 -307
  230. package/apps/dashboard/lib/tests.ts +0 -282
  231. package/apps/dashboard/next.config.js +0 -50
  232. package/apps/dashboard/postcss.config.mjs +0 -7
  233. package/apps/dashboard/public/file.svg +0 -1
  234. package/apps/dashboard/public/globe.svg +0 -1
  235. package/apps/dashboard/public/next.svg +0 -1
  236. package/apps/dashboard/public/vercel.svg +0 -1
  237. package/apps/dashboard/public/window.svg +0 -1
  238. package/apps/dashboard/scripts/download-node.js +0 -104
  239. package/apps/dashboard/scripts/upload-to-r2.js +0 -89
  240. package/docs/bdd-guidance.md +0 -390
@@ -1,378 +0,0 @@
1
- import { spawnSync } from 'child_process';
2
- import { NextRequest, NextResponse } from 'next/server';
3
- import path from 'path';
4
- import fs from 'fs';
5
- import { appendSessionContent, getSessionContent, ConversationTurn } from '@/lib/db';
6
- import {
7
- getOrCreateProcess,
8
- sendMessage as sendProcessMessage,
9
- killProcess,
10
- } from '@/lib/claude-process-manager';
11
-
12
- /**
13
- * Get the project root path for Claude CLI operations.
14
- * In packaged Electron apps, process.cwd() returns the app bundle's Resources directory,
15
- * so we use JETTYPOD_PROJECT_PATH env var which is set correctly by the Electron main process.
16
- */
17
- function getProjectRoot(): string {
18
- return process.env.JETTYPOD_PROJECT_PATH || path.resolve(process.cwd());
19
- }
20
-
21
- /**
22
- * Get the settings path if it exists, otherwise return undefined.
23
- * Claude CLI can run without explicit settings, so this is optional.
24
- */
25
- function getSettingsPath(projectRoot: string): string | undefined {
26
- const settingsPath = path.join(projectRoot, '.claude/settings.json');
27
- return fs.existsSync(settingsPath) ? settingsPath : undefined;
28
- }
29
-
30
- export const dynamic = 'force-dynamic';
31
-
32
- // Cache the CLI availability check so we only pay the spawnSync cost once per process lifetime
33
- let claudeCliAvailable: boolean | null = null;
34
-
35
- function isClaudeCliAvailable(): boolean {
36
- if (claudeCliAvailable !== null) return claudeCliAvailable;
37
- const result = spawnSync('which', ['claude'], { encoding: 'utf-8' });
38
- claudeCliAvailable = result.status === 0 && result.stdout.trim().length > 0;
39
- return claudeCliAvailable;
40
- }
41
-
42
- /**
43
- * Build a context restoration prefix from stored conversation history.
44
- * Prepended to the user's message when a Claude process was respawned.
45
- */
46
- function buildContextPrefix(history: ConversationTurn[]): string {
47
- if (history.length === 0) return '';
48
-
49
- // Only include user and assistant messages (skip errors)
50
- const relevant = history.filter(t => t.role === 'user' || t.role === 'assistant');
51
- if (relevant.length === 0) return '';
52
-
53
- const lines = relevant.map(t => {
54
- const label = t.role === 'user' ? 'User' : 'Assistant';
55
- return `${label}: ${t.content}`;
56
- });
57
-
58
- return `[Session context restored — your process was restarted. Previous conversation:]\n\n${lines.join('\n\n')}\n\n[End of restored context. New message from user:]\n\n`;
59
- }
60
-
61
- function isValidSessionId(id: string): boolean {
62
- const parsed = parseInt(id, 10);
63
- return !isNaN(parsed) && parsed > 0 && String(parsed) === id;
64
- }
65
-
66
- export async function POST(
67
- request: NextRequest,
68
- { params }: { params: Promise<{ sessionId: string }> }
69
- ) {
70
- const { sessionId } = await params;
71
-
72
- // Check for debug mode - shows synthetic messages for troubleshooting (#1000104)
73
- const { searchParams } = new URL(request.url);
74
- const showSynthetic = searchParams.get('debug') === 'true';
75
-
76
- // Validate session ID
77
- if (!isValidSessionId(sessionId)) {
78
- return NextResponse.json(
79
- { type: 'error', message: 'Invalid session' },
80
- { status: 400 }
81
- );
82
- }
83
-
84
- // Check if Claude CLI is available
85
- if (!isClaudeCliAvailable()) {
86
- return NextResponse.json(
87
- { type: 'error', message: 'Claude CLI not found' },
88
- { status: 503 }
89
- );
90
- }
91
-
92
- // Get the message
93
- const body = await request.json().catch(() => ({}));
94
- const { message } = body;
95
-
96
- if (!message || typeof message !== 'string') {
97
- return NextResponse.json(
98
- { type: 'error', message: 'Message is required' },
99
- { status: 400 }
100
- );
101
- }
102
-
103
- // Handle empty/whitespace-only input gracefully
104
- const trimmedMessage = message.trim();
105
- if (!trimmedMessage) {
106
- // Save empty user message to session
107
- const sessionIdNum = parseInt(sessionId, 10);
108
- appendSessionContent(sessionIdNum, {
109
- role: 'user',
110
- content: message,
111
- timestamp: new Date().toISOString()
112
- });
113
-
114
- // Return a helpful response without invoking Claude
115
- const clarificationMessage = 'I didn\'t catch that. What would you like to add to the backlog?';
116
-
117
- // Save assistant response to session
118
- appendSessionContent(sessionIdNum, {
119
- role: 'assistant',
120
- content: clarificationMessage,
121
- timestamp: new Date().toISOString()
122
- });
123
-
124
- const encoder = new TextEncoder();
125
- const emptyInputResponse = new ReadableStream({
126
- start(controller) {
127
- const response = {
128
- type: 'assistant',
129
- message: {
130
- content: [{ type: 'text', text: clarificationMessage }]
131
- }
132
- };
133
- controller.enqueue(encoder.encode(`data: ${JSON.stringify(response)}\n\n`));
134
- controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'done', exitCode: 0 })}\n\n`));
135
- controller.close();
136
- }
137
- });
138
-
139
- return new Response(emptyInputResponse, {
140
- headers: {
141
- 'Content-Type': 'text/event-stream',
142
- 'Cache-Control': 'no-cache',
143
- 'Connection': 'keep-alive',
144
- },
145
- });
146
- }
147
-
148
- // Save user message to session content
149
- const sessionIdNum = parseInt(sessionId, 10);
150
- appendSessionContent(sessionIdNum, {
151
- role: 'user',
152
- content: message,
153
- timestamp: new Date().toISOString()
154
- });
155
-
156
- // Standalone sessions run in main repo (no worktree)
157
- const claudeCwd = getProjectRoot();
158
- const settingsPath = getSettingsPath(claudeCwd);
159
-
160
- // Get or create persistent Claude process for this session
161
- const processResult = getOrCreateProcess(sessionId, claudeCwd, settingsPath);
162
-
163
- // Check if we hit the process limit
164
- if ('error' in processResult) {
165
- return NextResponse.json(
166
- { type: 'error', message: processResult.error },
167
- { status: 503 }
168
- );
169
- }
170
-
171
- const { emitter, isNew } = processResult;
172
-
173
- // If process was just spawned for an existing session, restore conversation context
174
- // by prepending stored history to the user's message so Claude has full context
175
- let messageToSend = trimmedMessage;
176
- if (isNew) {
177
- // Load history saved BEFORE this message (the current message was already appended above)
178
- const history = getSessionContent(sessionIdNum);
179
- // Exclude the message we just appended (last user turn)
180
- const priorHistory = history.slice(0, -1);
181
- const prefix = buildContextPrefix(priorHistory);
182
- if (prefix) {
183
- messageToSend = prefix + trimmedMessage;
184
- }
185
- }
186
-
187
- // Create a readable stream for SSE
188
- const encoder = new TextEncoder();
189
-
190
- // Track cleanup state outside the stream so cancel() can access it
191
- let activeEmitter = emitter;
192
- let responseComplete = false;
193
- let onData: ((parsed: Record<string, unknown>) => void) | null = null;
194
- let onError: ((err: { type: string; content: string }) => void) | null = null;
195
- let onClose: ((info: { exitCode: number }) => void) | null = null;
196
-
197
- const removeListeners = () => {
198
- if (onData) activeEmitter.off('data', onData);
199
- if (onError) activeEmitter.off('error', onError);
200
- if (onClose) activeEmitter.off('close', onClose);
201
- };
202
-
203
- const stream = new ReadableStream({
204
- start(controller) {
205
- // Collect assistant response text for saving to session content
206
- let assistantResponse = '';
207
-
208
- // Track if we've saved the response (to avoid duplicate saves)
209
- let responseSaved = false;
210
-
211
- // Helper to save assistant response if not already saved
212
- const saveAssistantResponse = () => {
213
- if (!responseSaved && assistantResponse.trim()) {
214
- appendSessionContent(sessionIdNum, {
215
- role: 'assistant',
216
- content: assistantResponse.trim(),
217
- timestamp: new Date().toISOString()
218
- });
219
- responseSaved = true;
220
- }
221
- };
222
-
223
- // Handle data from the persistent process
224
- onData = (parsed: Record<string, unknown>) => {
225
- if (responseComplete) return;
226
-
227
- // Skip synthetic messages (skill prompt injections) unless in debug mode (#1000104)
228
- // These are internal system prompts that shouldn't normally appear in the conversation
229
- if (parsed.isSynthetic === true && !showSynthetic) {
230
- return;
231
- }
232
-
233
- // Collect text content for session storage
234
- if (parsed.type === 'assistant' && parsed.message) {
235
- const msg = parsed.message as { content?: Array<{ type: string; text?: string; name?: string }> };
236
- if (msg.content) {
237
- for (const block of msg.content) {
238
- if (block.type === 'text' && block.text) {
239
- if (assistantResponse && !assistantResponse.endsWith('\n')) {
240
- assistantResponse += '\n\n';
241
- }
242
- assistantResponse += block.text;
243
- }
244
- // When Claude invokes the Skill tool, save accumulated response immediately
245
- // This prevents losing content when skills are invoked and the turn ends differently
246
- if (block.type === 'tool_use' && block.name === 'Skill') {
247
- saveAssistantResponse();
248
- // Reset for potential post-skill content (Bug #1000097)
249
- // Without this, responseSaved=true blocks saving any content after the skill
250
- assistantResponse = '';
251
- responseSaved = false;
252
- }
253
- }
254
- }
255
- } else if (parsed.type === 'content_block_delta') {
256
- const delta = parsed.delta as { text?: string } | undefined;
257
- if (delta?.text) {
258
- assistantResponse += delta.text;
259
- }
260
- }
261
-
262
- // Check for result message - this indicates Claude is done with this turn
263
- if (parsed.type === 'result') {
264
- responseComplete = true;
265
-
266
- // Save assistant response to session content (if not already saved)
267
- saveAssistantResponse();
268
-
269
- const sseData = `data: ${JSON.stringify(parsed)}\n\n`;
270
- controller.enqueue(encoder.encode(sseData));
271
-
272
- // Send done event and close stream
273
- controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'done', exitCode: 0 })}\n\n`));
274
-
275
- // Remove listeners and close
276
- removeListeners();
277
- controller.close();
278
- return;
279
- }
280
-
281
- const sseData = `data: ${JSON.stringify(parsed)}\n\n`;
282
- controller.enqueue(encoder.encode(sseData));
283
- };
284
-
285
- onError = (err: { type: string; content: string }) => {
286
- if (responseComplete) return;
287
- // Persist error to database (#1000098)
288
- appendSessionContent(sessionIdNum, {
289
- role: 'error',
290
- content: err.content,
291
- timestamp: new Date().toISOString()
292
- });
293
- const sseData = `data: ${JSON.stringify({ type: 'error', content: err.content })}\n\n`;
294
- controller.enqueue(encoder.encode(sseData));
295
- };
296
-
297
- onClose = (info: { exitCode: number }) => {
298
- if (responseComplete) return;
299
- responseComplete = true;
300
-
301
- // Save any collected response (if not already saved)
302
- saveAssistantResponse();
303
-
304
- const sseData = `data: ${JSON.stringify({ type: 'done', exitCode: info.exitCode })}\n\n`;
305
- controller.enqueue(encoder.encode(sseData));
306
- controller.close();
307
- };
308
-
309
- // Attach listeners
310
- emitter.on('data', onData);
311
- emitter.on('error', onError);
312
- emitter.on('close', onClose);
313
-
314
- // Send the message to the persistent process (with context prefix if respawned)
315
- let sent = sendProcessMessage(sessionId, messageToSend);
316
-
317
- // Bug #1000096: If send failed, the process may have died after getOrCreateProcess
318
- // Try to create a fresh process and retry once
319
- if (!sent) {
320
- // Kill any zombie process and create fresh one
321
- killProcess(sessionId);
322
-
323
- const retryResult = getOrCreateProcess(sessionId, claudeCwd, settingsPath);
324
- if ('error' in retryResult) {
325
- // Hit process limit on retry - return error
326
- controller.enqueue(encoder.encode(`data: ${JSON.stringify({
327
- type: 'error',
328
- content: retryResult.error
329
- })}\n\n`));
330
- controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'done', exitCode: 1 })}\n\n`));
331
- controller.close();
332
- return;
333
- }
334
- const { emitter: newEmitter } = retryResult;
335
-
336
- // Re-attach listeners to new emitter
337
- removeListeners();
338
- activeEmitter = newEmitter;
339
- newEmitter.on('data', onData!);
340
- newEmitter.on('error', onError!);
341
- newEmitter.on('close', onClose!);
342
-
343
- // Retry send (with context prefix since this is also a fresh process)
344
- sent = sendProcessMessage(sessionId, messageToSend);
345
- }
346
-
347
- if (!sent) {
348
- // Still failed after retry - give clear error
349
- const errorContent = 'Claude process is unavailable. The process may have crashed or failed to start. Please try again.';
350
- // Persist error to database (#1000098)
351
- appendSessionContent(sessionIdNum, {
352
- role: 'error',
353
- content: errorContent,
354
- timestamp: new Date().toISOString()
355
- });
356
- controller.enqueue(encoder.encode(`data: ${JSON.stringify({
357
- type: 'error',
358
- content: errorContent
359
- })}\n\n`));
360
- controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'done', exitCode: 1 })}\n\n`));
361
- controller.close();
362
- }
363
- },
364
- cancel() {
365
- // Client disconnected - clean up listeners to prevent writes to closed controller
366
- responseComplete = true;
367
- removeListeners();
368
- },
369
- });
370
-
371
- return new Response(stream, {
372
- headers: {
373
- 'Content-Type': 'text/event-stream',
374
- 'Cache-Control': 'no-cache',
375
- 'Connection': 'keep-alive',
376
- },
377
- });
378
- }
@@ -1,24 +0,0 @@
1
- import { NextRequest, NextResponse } from 'next/server';
2
- import { pinSession, unpinSession } from '@/lib/claude-process-manager';
3
-
4
- export const dynamic = 'force-dynamic';
5
-
6
- // POST /api/claude/sessions/[sessionId]/pin - Pin session to prevent idle cleanup
7
- export async function POST(
8
- _request: NextRequest,
9
- { params }: { params: Promise<{ sessionId: string }> }
10
- ) {
11
- const { sessionId } = await params;
12
- pinSession(sessionId);
13
- return NextResponse.json({ pinned: true });
14
- }
15
-
16
- // DELETE /api/claude/sessions/[sessionId]/pin - Unpin session to allow idle cleanup
17
- export async function DELETE(
18
- _request: NextRequest,
19
- { params }: { params: Promise<{ sessionId: string }> }
20
- ) {
21
- const { sessionId } = await params;
22
- unpinSession(sessionId);
23
- return NextResponse.json({ pinned: false });
24
- }
@@ -1,34 +0,0 @@
1
- import { NextResponse } from 'next/server';
2
- import { cleanupStaleSessions } from '@/lib/db';
3
-
4
- export const dynamic = 'force-dynamic';
5
-
6
- // Default retention period in days for completed sessions
7
- const DEFAULT_RETENTION_DAYS = 30;
8
-
9
- // POST /api/claude/sessions/cleanup - Prune orphaned and old completed sessions
10
- // Query params:
11
- // - retentionDays: number of days to keep completed sessions (default: 30)
12
- export async function POST(request: Request) {
13
- try {
14
- const { searchParams } = new URL(request.url);
15
- const retentionDays = parseInt(searchParams.get('retentionDays') || String(DEFAULT_RETENTION_DAYS), 10);
16
-
17
- // Validate retention days
18
- if (isNaN(retentionDays) || retentionDays < 0) {
19
- return NextResponse.json({ error: 'Invalid retentionDays parameter' }, { status: 400 });
20
- }
21
-
22
- const deletedCount = cleanupStaleSessions(retentionDays);
23
-
24
- return NextResponse.json({
25
- success: true,
26
- deletedCount,
27
- retentionDays,
28
- message: `Cleaned up ${deletedCount} stale session(s)`
29
- });
30
- } catch (error) {
31
- console.error('Failed to cleanup sessions:', error);
32
- return NextResponse.json({ error: 'Failed to cleanup sessions' }, { status: 500 });
33
- }
34
- }
@@ -1,184 +0,0 @@
1
- import { NextResponse } from 'next/server';
2
- import { listSessions, createSession, linkSession, isLinkableWorkItem, getActiveSessionByWorkItem, getOrCreateSessionForWorkItem, closeSession, closeSessionByWorkItem, countActiveSessions, cleanupStaleSessions } from '@/lib/db';
3
- import { killProcess } from '@/lib/claude-process-manager';
4
-
5
- export const dynamic = 'force-dynamic';
6
-
7
- // Maximum number of concurrent active sessions
8
- const MAX_SESSIONS = 10;
9
-
10
- // Session title constraints
11
- const MAX_TITLE_LENGTH = 100;
12
-
13
- /**
14
- * Sanitize and validate session title
15
- * - Trims whitespace
16
- * - Removes control characters
17
- * - Enforces max length
18
- */
19
- function sanitizeTitle(title: string | undefined): string {
20
- if (!title) return 'New Session';
21
-
22
- // Remove control characters and trim whitespace
23
- const sanitized = title
24
- .replace(/[\x00-\x1F\x7F]/g, '') // Remove control characters
25
- .trim();
26
-
27
- // Return default if empty after sanitization
28
- if (!sanitized) return 'New Session';
29
-
30
- // Enforce max length
31
- return sanitized.slice(0, MAX_TITLE_LENGTH);
32
- }
33
-
34
- // GET /api/claude/sessions - List all sessions with feature info
35
- export async function GET() {
36
- try {
37
- const sessions = listSessions();
38
- // Convert numeric IDs to strings for frontend consistency
39
- const normalizedSessions = sessions.map(session => ({
40
- ...session,
41
- id: String(session.id),
42
- featureId: session.featureId ? String(session.featureId) : null,
43
- }));
44
- return NextResponse.json(normalizedSessions);
45
- } catch (error) {
46
- // Log error for debugging but return empty array to client
47
- // This ensures dashboard renders without crashing when DB is unavailable
48
- console.error('Failed to list sessions:', error);
49
- return NextResponse.json([]);
50
- }
51
- }
52
-
53
- // POST /api/claude/sessions - Create new unlinked session or get/create for work item
54
- export async function POST(request: Request) {
55
- try {
56
- const body = await request.json();
57
- const { title, workItemId } = body;
58
-
59
- // Opportunistically clean up stale sessions before checking limits
60
- // This ensures old orphaned/completed sessions don't block new session creation
61
- cleanupStaleSessions();
62
-
63
- // Check session limit before creating new sessions
64
- const currentCount = countActiveSessions();
65
- if (currentCount >= MAX_SESSIONS) {
66
- return NextResponse.json({
67
- error: `Session limit reached (${MAX_SESSIONS}). Close existing sessions to create new ones.`,
68
- code: 'SESSION_LIMIT_REACHED',
69
- currentCount,
70
- maxSessions: MAX_SESSIONS
71
- }, { status: 429 });
72
- }
73
-
74
- // If workItemId provided, use get-or-create pattern
75
- if (workItemId) {
76
- const result = getOrCreateSessionForWorkItem(workItemId);
77
- if (!result.session) {
78
- return NextResponse.json({
79
- error: result.error,
80
- redirectToId: result.redirectToId ? String(result.redirectToId) : undefined
81
- }, { status: 400 });
82
- }
83
- return NextResponse.json({
84
- id: String(result.session.id),
85
- title: result.session.title,
86
- created: result.created,
87
- workItemId: result.session.work_item_id ? String(result.session.work_item_id) : null
88
- });
89
- }
90
-
91
- // Otherwise create unlinked session with sanitized title
92
- const sessionTitle = sanitizeTitle(title);
93
- const sessionId = createSession(sessionTitle);
94
- return NextResponse.json({ id: String(sessionId), title: sessionTitle, created: true });
95
- } catch (error) {
96
- console.error('Failed to create session:', error);
97
- return NextResponse.json({ error: 'Failed to create session' }, { status: 500 });
98
- }
99
- }
100
-
101
- // PATCH /api/claude/sessions - Link session to work item
102
- // Only links to: features, standalone chores, bugs
103
- // Rejects: epics, chores under features (redirects to parent feature)
104
- export async function PATCH(request: Request) {
105
- try {
106
- const body = await request.json();
107
- const { sessionId, workItemId } = body;
108
-
109
- if (!sessionId || !workItemId) {
110
- return NextResponse.json({ error: 'sessionId and workItemId required' }, { status: 400 });
111
- }
112
-
113
- // Check if this work item type can have sessions
114
- const linkCheck = isLinkableWorkItem(workItemId);
115
- if (!linkCheck.linkable) {
116
- return NextResponse.json({
117
- error: linkCheck.reason,
118
- redirectToId: linkCheck.redirectToId ? String(linkCheck.redirectToId) : undefined
119
- }, { status: 400 });
120
- }
121
-
122
- // Check if work item already has an ACTIVE session
123
- const existingSession = getActiveSessionByWorkItem(workItemId);
124
- if (existingSession) {
125
- return NextResponse.json({
126
- error: 'Work item already has an active session',
127
- existingSessionId: String(existingSession.id)
128
- }, { status: 409 });
129
- }
130
-
131
- const result = linkSession(sessionId, workItemId);
132
- if (!result.success) {
133
- return NextResponse.json({
134
- error: result.error,
135
- redirectToId: result.redirectToId ? String(result.redirectToId) : undefined
136
- }, { status: 400 });
137
- }
138
-
139
- return NextResponse.json({ success: true });
140
- } catch (error) {
141
- console.error('Failed to link session:', error);
142
- return NextResponse.json({ error: 'Failed to link session' }, { status: 500 });
143
- }
144
- }
145
-
146
- // DELETE /api/claude/sessions - Close a session
147
- // Use ?sessionId=X&type=standalone for standalone sessions (closes by claude_sessions.id)
148
- // Use ?sessionId=X&type=workitem for work-item sessions (closes by work_item_id)
149
- export async function DELETE(request: Request) {
150
- try {
151
- const { searchParams } = new URL(request.url);
152
- const sessionId = searchParams.get('sessionId');
153
- const sessionType = searchParams.get('type') || 'standalone'; // default to standalone for backwards compat
154
-
155
- if (!sessionId) {
156
- return NextResponse.json({ error: 'sessionId required' }, { status: 400 });
157
- }
158
-
159
- const id = parseInt(sessionId, 10);
160
- let success: boolean;
161
-
162
- // Kill any active Claude process for this session before closing
163
- // For standalone sessions, the sessionId IS the process key
164
- // For work-item sessions, we'd need to look up the session ID first
165
- killProcess(sessionId);
166
-
167
- if (sessionType === 'workitem') {
168
- // Work-item sessions: sessionId is the work_item_id
169
- success = closeSessionByWorkItem(id);
170
- } else {
171
- // Standalone sessions: sessionId is the claude_sessions.id
172
- success = closeSession(id);
173
- }
174
-
175
- if (!success) {
176
- return NextResponse.json({ error: 'Session not found' }, { status: 404 });
177
- }
178
-
179
- return NextResponse.json({ success: true });
180
- } catch (error) {
181
- console.error('Failed to close session:', error);
182
- return NextResponse.json({ error: 'Failed to close session' }, { status: 500 });
183
- }
184
- }
@@ -1,25 +0,0 @@
1
- import { NextResponse } from 'next/server';
2
- import { getDecision } from '@/lib/db';
3
-
4
- export const dynamic = 'force-dynamic';
5
-
6
- interface RouteContext {
7
- params: Promise<{ id: string }>;
8
- }
9
-
10
- export async function GET(request: Request, context: RouteContext) {
11
- const { id } = await context.params;
12
- const decisionId = parseInt(id, 10);
13
-
14
- if (isNaN(decisionId)) {
15
- return NextResponse.json({ error: 'Invalid decision ID' }, { status: 400 });
16
- }
17
-
18
- const decision = getDecision(decisionId);
19
-
20
- if (!decision) {
21
- return NextResponse.json({ error: 'Decision not found' }, { status: 404 });
22
- }
23
-
24
- return NextResponse.json(decision);
25
- }
@@ -1,17 +0,0 @@
1
- import { NextResponse } from 'next/server';
2
- import { closeDb } from '@/lib/db';
3
-
4
- // Internal endpoint called by the Electron main process to update the project path
5
- // in the Next.js child process after a project switch. In dev mode, Next.js runs as
6
- // a separate process and doesn't share process.env with Electron.
7
- export async function POST(request: Request) {
8
- const { projectPath } = await request.json();
9
-
10
- // Update env var so all subsequent db.ts / test-results-db.ts calls use the new path
11
- process.env.JETTYPOD_PROJECT_PATH = projectPath || '';
12
-
13
- // Close the cached DB connection so it reconnects to the new project on next access
14
- closeDb();
15
-
16
- return NextResponse.json({ success: true });
17
- }
@@ -1,15 +0,0 @@
1
- import { NextResponse } from 'next/server';
2
- import { getKanbanData } from '@/lib/db';
3
-
4
- export const dynamic = 'force-dynamic';
5
-
6
- export async function GET() {
7
- const data = getKanbanData();
8
-
9
- // Convert Maps to arrays for JSON serialization
10
- return NextResponse.json({
11
- inFlight: data.inFlight,
12
- backlog: Array.from(data.backlog.entries()),
13
- done: Array.from(data.done.entries()),
14
- });
15
- }