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.
Files changed (208) hide show
  1. package/.env +2 -1
  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 +54 -49
  8. package/apps/dashboard/app/demo/gates/page.tsx +3 -5
  9. package/apps/dashboard/app/design-system/page.tsx +1 -1
  10. package/apps/dashboard/app/globals.css +74 -2
  11. package/apps/dashboard/app/install-claude/page.tsx +3 -5
  12. package/apps/dashboard/app/login/page.tsx +17 -20
  13. package/apps/dashboard/app/page.tsx +101 -48
  14. package/apps/dashboard/app/settings/page.tsx +60 -12
  15. package/apps/dashboard/app/signup/page.tsx +14 -17
  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 +12 -15
  19. package/apps/dashboard/app/work/[id]/page.tsx +90 -75
  20. package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
  21. package/apps/dashboard/components/AppShell.tsx +70 -61
  22. package/apps/dashboard/components/CardMenu.tsx +0 -1
  23. package/apps/dashboard/components/ClaudePanel.tsx +541 -283
  24. package/apps/dashboard/components/ClaudePanelInput.tsx +23 -4
  25. package/apps/dashboard/components/ConnectClaudeScreen.tsx +1 -5
  26. package/apps/dashboard/components/CopyableId.tsx +1 -2
  27. package/apps/dashboard/components/DetailReviewActions.tsx +11 -20
  28. package/apps/dashboard/components/DragContext.tsx +132 -62
  29. package/apps/dashboard/components/DraggableCard.tsx +3 -5
  30. package/apps/dashboard/components/DropZone.tsx +5 -6
  31. package/apps/dashboard/components/EditableDetailDescription.tsx +6 -12
  32. package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
  33. package/apps/dashboard/components/EditableTitle.tsx +0 -1
  34. package/apps/dashboard/components/ElapsedTimer.tsx +15 -3
  35. package/apps/dashboard/components/EpicGroup.tsx +100 -70
  36. package/apps/dashboard/components/GateCard.tsx +0 -1
  37. package/apps/dashboard/components/GateChoiceCard.tsx +1 -2
  38. package/apps/dashboard/components/InstallClaudeScreen.tsx +1 -5
  39. package/apps/dashboard/components/JettyLoader.tsx +0 -1
  40. package/apps/dashboard/components/KanbanBoard.tsx +319 -173
  41. package/apps/dashboard/components/KanbanCard.tsx +341 -107
  42. package/apps/dashboard/components/LazyCard.tsx +62 -0
  43. package/apps/dashboard/components/LazyMarkdown.tsx +0 -1
  44. package/apps/dashboard/components/MainNav.tsx +24 -25
  45. package/apps/dashboard/components/MessageBlock.tsx +93 -16
  46. package/apps/dashboard/components/ModeStartCard.tsx +0 -1
  47. package/apps/dashboard/components/OnboardingWelcome.tsx +0 -1
  48. package/apps/dashboard/components/PlaceholderCard.tsx +0 -1
  49. package/apps/dashboard/components/ProjectSwitcher.tsx +20 -20
  50. package/apps/dashboard/components/PrototypeTimeline.tsx +47 -26
  51. package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +308 -223
  52. package/apps/dashboard/components/RealTimeTestsWrapper.tsx +303 -160
  53. package/apps/dashboard/components/ReviewFooter.tsx +12 -14
  54. package/apps/dashboard/components/SessionList.tsx +0 -1
  55. package/apps/dashboard/components/SubscribeContent.tsx +40 -11
  56. package/apps/dashboard/components/TestTree.tsx +1 -2
  57. package/apps/dashboard/components/TipCard.tsx +2 -4
  58. package/apps/dashboard/components/Toast.tsx +0 -1
  59. package/apps/dashboard/components/TypeIcon.tsx +7 -8
  60. package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
  61. package/apps/dashboard/components/WaveCompletionAnimation.tsx +5 -17
  62. package/apps/dashboard/components/WelcomeScreen.tsx +2 -6
  63. package/apps/dashboard/components/WorkItemHeader.tsx +0 -1
  64. package/apps/dashboard/components/WorkItemTree.tsx +2 -4
  65. package/apps/dashboard/components/settings/AccountSection.tsx +27 -13
  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 +20 -73
  69. package/apps/dashboard/components/settings/GeneralSection.tsx +137 -26
  70. package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
  71. package/apps/dashboard/components/settings/SettingsLayout.tsx +0 -1
  72. package/apps/dashboard/components/ui/Button.tsx +1 -1
  73. package/apps/dashboard/components/ui/Input.tsx +1 -1
  74. package/apps/dashboard/components.json +1 -1
  75. package/apps/dashboard/contexts/ClaudeSessionContext.tsx +611 -358
  76. package/apps/dashboard/contexts/ConnectionStatusContext.tsx +0 -1
  77. package/apps/dashboard/contexts/UsageContext.tsx +62 -31
  78. package/apps/dashboard/dev.sh +35 -0
  79. package/apps/dashboard/eslint.config.mjs +9 -9
  80. package/apps/dashboard/hooks/useWebSocket.ts +138 -83
  81. package/apps/dashboard/index.html +73 -0
  82. package/apps/dashboard/lib/data-bridge.ts +722 -0
  83. package/apps/dashboard/lib/db.ts +69 -1302
  84. package/apps/dashboard/lib/environment-config.ts +173 -0
  85. package/apps/dashboard/lib/environment-verification.ts +119 -0
  86. package/apps/dashboard/lib/kanban-utils.ts +226 -26
  87. package/apps/dashboard/lib/proof-run.ts +495 -0
  88. package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
  89. package/apps/dashboard/lib/service-recovery.ts +326 -0
  90. package/apps/dashboard/lib/session-state-machine.ts +1 -0
  91. package/apps/dashboard/lib/session-state-utils.ts +0 -164
  92. package/apps/dashboard/lib/session-stream-manager.ts +253 -122
  93. package/apps/dashboard/lib/stream-manager-registry.ts +46 -6
  94. package/apps/dashboard/lib/tauri-bridge.ts +102 -0
  95. package/apps/dashboard/lib/tauri.ts +106 -0
  96. package/apps/dashboard/lib/utils.ts +3 -3
  97. package/apps/dashboard/next-env.d.ts +1 -1
  98. package/apps/dashboard/package.json +21 -33
  99. package/apps/dashboard/public/bug-icon.png +0 -0
  100. package/apps/dashboard/public/buoy-icon.png +0 -0
  101. package/apps/dashboard/public/in-flight-seagull.png +0 -0
  102. package/apps/dashboard/public/pier-icon.png +0 -0
  103. package/apps/dashboard/public/star-icon.png +0 -0
  104. package/apps/dashboard/public/wrench-icon.png +0 -0
  105. package/apps/dashboard/scripts/tauri-build.js +228 -0
  106. package/apps/dashboard/scripts/upload-tauri-to-r2.js +125 -0
  107. package/apps/dashboard/src/main.tsx +12 -0
  108. package/apps/dashboard/src/router.tsx +107 -0
  109. package/apps/dashboard/src/vite-env.d.ts +1 -0
  110. package/apps/dashboard/tsconfig.json +7 -12
  111. package/apps/dashboard/tsconfig.tsbuildinfo +1 -1
  112. package/apps/dashboard/vite.config.ts +33 -0
  113. package/apps/update-server/src/index.ts +167 -30
  114. package/claude-hooks/global-guardrails.js +14 -13
  115. package/crates/jettypod-cli/Cargo.toml +19 -0
  116. package/crates/jettypod-cli/src/commands.rs +1249 -0
  117. package/crates/jettypod-cli/src/main.rs +595 -0
  118. package/crates/jettypod-core/Cargo.toml +26 -0
  119. package/crates/jettypod-core/build.rs +98 -0
  120. package/crates/jettypod-core/migrations/V1__baseline.sql +197 -0
  121. package/crates/jettypod-core/migrations/V2__work_items_indexes.sql +6 -0
  122. package/crates/jettypod-core/migrations/V3__qa_steps.sql +2 -0
  123. package/crates/jettypod-core/src/auth.rs +294 -0
  124. package/crates/jettypod-core/src/config.rs +397 -0
  125. package/crates/jettypod-core/src/db/mod.rs +507 -0
  126. package/crates/jettypod-core/src/db/recovery.rs +114 -0
  127. package/crates/jettypod-core/src/db/startup.rs +101 -0
  128. package/crates/jettypod-core/src/db/validate.rs +149 -0
  129. package/crates/jettypod-core/src/error.rs +76 -0
  130. package/crates/jettypod-core/src/git.rs +458 -0
  131. package/crates/jettypod-core/src/lib.rs +20 -0
  132. package/crates/jettypod-core/src/sessions.rs +625 -0
  133. package/crates/jettypod-core/src/skills.rs +556 -0
  134. package/crates/jettypod-core/src/work.rs +1086 -0
  135. package/crates/jettypod-core/src/worktree.rs +628 -0
  136. package/crates/jettypod-core/src/ws.rs +767 -0
  137. package/cucumber-test.cjs +6 -0
  138. package/jettypod.js +96 -4
  139. package/lib/bdd-preflight.js +96 -0
  140. package/lib/merge-lock.js +111 -253
  141. package/lib/migrations/030-rejection-round-columns.js +54 -0
  142. package/lib/migrations/031-session-isolation-index.js +17 -0
  143. package/lib/work-commands/index.js +58 -16
  144. package/lib/work-tracking/index.js +108 -8
  145. package/package.json +1 -1
  146. package/skills-templates/bug-mode/SKILL.md +43 -1
  147. package/skills-templates/chore-mode/SKILL.md +40 -1
  148. package/skills-templates/design-system-selection/SKILL.md +273 -0
  149. package/skills-templates/epic-planning/SKILL.md +14 -0
  150. package/skills-templates/feature-planning/SKILL.md +90 -1
  151. package/skills-templates/production-mode/SKILL.md +20 -0
  152. package/skills-templates/simple-improvement/SKILL.md +39 -2
  153. package/skills-templates/speed-mode/SKILL.md +10 -15
  154. package/skills-templates/stable-mode/SKILL.md +47 -0
  155. package/apps/dashboard/README.md +0 -36
  156. package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -446
  157. package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
  158. package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -280
  159. package/apps/dashboard/app/api/claude/sessions/[sessionId]/content/route.ts +0 -52
  160. package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +0 -525
  161. package/apps/dashboard/app/api/claude/sessions/[sessionId]/pin/route.ts +0 -24
  162. package/apps/dashboard/app/api/claude/sessions/cleanup/route.ts +0 -34
  163. package/apps/dashboard/app/api/claude/sessions/route.ts +0 -184
  164. package/apps/dashboard/app/api/decisions/[id]/route.ts +0 -25
  165. package/apps/dashboard/app/api/internal/set-project/route.ts +0 -17
  166. package/apps/dashboard/app/api/kanban/route.ts +0 -15
  167. package/apps/dashboard/app/api/settings/env-vars/route.ts +0 -125
  168. package/apps/dashboard/app/api/settings/general/route.ts +0 -21
  169. package/apps/dashboard/app/api/tests/route.ts +0 -9
  170. package/apps/dashboard/app/api/tests/run/route.ts +0 -82
  171. package/apps/dashboard/app/api/tests/run/stream/route.ts +0 -71
  172. package/apps/dashboard/app/api/tests/undefined/route.ts +0 -9
  173. package/apps/dashboard/app/api/usage/route.ts +0 -17
  174. package/apps/dashboard/app/api/work/[id]/description/route.ts +0 -21
  175. package/apps/dashboard/app/api/work/[id]/epic/route.ts +0 -21
  176. package/apps/dashboard/app/api/work/[id]/order/route.ts +0 -21
  177. package/apps/dashboard/app/api/work/[id]/route.ts +0 -35
  178. package/apps/dashboard/app/api/work/[id]/status/route.ts +0 -63
  179. package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
  180. package/apps/dashboard/app/layout.tsx +0 -55
  181. package/apps/dashboard/components/UpgradeBanner.tsx +0 -30
  182. package/apps/dashboard/electron/ipc-handlers.js +0 -1026
  183. package/apps/dashboard/electron/main.js +0 -2306
  184. package/apps/dashboard/electron/preload.js +0 -125
  185. package/apps/dashboard/electron/session-manager.js +0 -163
  186. package/apps/dashboard/electron-builder.config.js +0 -357
  187. package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
  188. package/apps/dashboard/lib/backlog-parser.ts +0 -50
  189. package/apps/dashboard/lib/claude-process-manager.ts +0 -529
  190. package/apps/dashboard/lib/db-bridge.ts +0 -283
  191. package/apps/dashboard/lib/prototypes.ts +0 -202
  192. package/apps/dashboard/lib/test-results-db.ts +0 -307
  193. package/apps/dashboard/lib/tests.ts +0 -282
  194. package/apps/dashboard/next.config.js +0 -66
  195. package/apps/dashboard/postcss.config.mjs +0 -7
  196. package/apps/dashboard/public/bug-icon.svg +0 -9
  197. package/apps/dashboard/public/buoy-icon.svg +0 -9
  198. package/apps/dashboard/public/file.svg +0 -1
  199. package/apps/dashboard/public/globe.svg +0 -1
  200. package/apps/dashboard/public/in-flight-seagull.svg +0 -9
  201. package/apps/dashboard/public/next.svg +0 -1
  202. package/apps/dashboard/public/pier-icon.svg +0 -14
  203. package/apps/dashboard/public/star-icon.svg +0 -9
  204. package/apps/dashboard/public/vercel.svg +0 -1
  205. package/apps/dashboard/public/window.svg +0 -1
  206. package/apps/dashboard/public/wrench-icon.svg +0 -9
  207. package/apps/dashboard/scripts/download-node.js +0 -104
  208. package/apps/dashboard/scripts/upload-to-r2.js +0 -89
@@ -1,525 +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, getSession, createWorkItem, updateWorkItemDescription, ConversationTurn } from '@/lib/db';
6
- import {
7
- getOrCreateProcess,
8
- sendMessage as sendProcessMessage,
9
- killProcess,
10
- } from '@/lib/claude-process-manager';
11
- import { parseBacklogInput } from '@/lib/backlog-parser';
12
-
13
- /**
14
- * Get the project root path for Claude CLI operations.
15
- * In packaged Electron apps, process.cwd() returns the app bundle's Resources directory,
16
- * so we use JETTYPOD_PROJECT_PATH env var which is set correctly by the Electron main process.
17
- */
18
- function getProjectRoot(): string {
19
- return process.env.JETTYPOD_PROJECT_PATH || path.resolve(process.cwd());
20
- }
21
-
22
- /**
23
- * Get the settings path if it exists, otherwise return undefined.
24
- * Claude CLI can run without explicit settings, so this is optional.
25
- */
26
- function getSettingsPath(projectRoot: string): string | undefined {
27
- const settingsPath = path.join(projectRoot, '.claude/settings.json');
28
- return fs.existsSync(settingsPath) ? settingsPath : undefined;
29
- }
30
-
31
- export const dynamic = 'force-dynamic';
32
-
33
- // Cache the CLI availability check so we only pay the spawnSync cost once per process lifetime
34
- let claudeCliAvailable: boolean | null = null;
35
-
36
- function isClaudeCliAvailable(): boolean {
37
- if (claudeCliAvailable !== null) return claudeCliAvailable;
38
- const result = spawnSync('which', ['claude'], { encoding: 'utf-8' });
39
- claudeCliAvailable = result.status === 0 && result.stdout.trim().length > 0;
40
- return claudeCliAvailable;
41
- }
42
-
43
- /**
44
- * Build a context restoration prefix from stored conversation history.
45
- * Prepended to the user's message when a Claude process was respawned.
46
- */
47
- function buildContextPrefix(history: ConversationTurn[]): string {
48
- if (history.length === 0) return '';
49
-
50
- // Only include user and assistant messages (skip errors)
51
- const relevant = history.filter(t => t.role === 'user' || t.role === 'assistant');
52
- if (relevant.length === 0) return '';
53
-
54
- const lines = relevant.map(t => {
55
- const label = t.role === 'user' ? 'User' : 'Assistant';
56
- return `${label}: ${t.content}`;
57
- });
58
-
59
- 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`;
60
- }
61
-
62
- function isValidSessionId(id: string): boolean {
63
- const parsed = parseInt(id, 10);
64
- return !isNaN(parsed) && parsed > 0 && String(parsed) === id;
65
- }
66
-
67
- // Backlog question gate data
68
- const BACKLOG_QUESTION_GATE = JSON.stringify({
69
- question: 'Done. What now?',
70
- options: [
71
- { id: 'finished', label: 'Finished adding to the backlog', description: 'Close this tab' },
72
- { id: 'add-details', label: 'Add details', description: 'Describe what this work item involves' },
73
- { id: 'create-another', label: 'Create another work item', description: 'Add another item to the backlog' },
74
- ],
75
- });
76
-
77
- /**
78
- * Build a synthetic SSE response from an array of text chunks.
79
- * Each chunk becomes a separate assistant message event.
80
- */
81
- function buildSyntheticSSE(chunks: string[]): Response {
82
- const encoder = new TextEncoder();
83
- const stream = new ReadableStream({
84
- start(controller) {
85
- for (const text of chunks) {
86
- controller.enqueue(encoder.encode(`data: ${JSON.stringify({
87
- type: 'assistant',
88
- message: { content: [{ type: 'text', text }] }
89
- })}\n\n`));
90
- }
91
- controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'done', exitCode: 0 })}\n\n`));
92
- controller.close();
93
- }
94
- });
95
- return new Response(stream, {
96
- headers: {
97
- 'Content-Type': 'text/event-stream',
98
- 'Cache-Control': 'no-cache',
99
- 'Connection': 'keep-alive',
100
- },
101
- });
102
- }
103
-
104
- /**
105
- * Handle the full "Add to Backlog" conversation flow without spawning Claude CLI.
106
- * Returns a Response for handled cases, or null to fall through to Claude CLI.
107
- */
108
- function handleBacklogMessage(sessionIdNum: number, message: string): Response | null {
109
- // Check conversation history to determine state
110
- const history = getSessionContent(sessionIdNum);
111
- const lastAssistant = [...history].reverse().find(t => t.role === 'assistant')?.content || '';
112
-
113
- // Option: "Finished adding to the backlog" — handled client-side (tab closes)
114
- // This shouldn't normally reach the server, but handle gracefully if it does
115
- if (message === 'Finished adding to the backlog') {
116
- appendSessionContent(sessionIdNum, {
117
- role: 'assistant',
118
- content: 'Done!',
119
- timestamp: new Date().toISOString()
120
- });
121
- return buildSyntheticSSE(['Done!']);
122
- }
123
-
124
- // Option: "Add details" — prompt for details
125
- if (message === 'Add details') {
126
- const promptText = 'Tell me more about this work item.';
127
- appendSessionContent(sessionIdNum, {
128
- role: 'assistant',
129
- content: promptText,
130
- timestamp: new Date().toISOString()
131
- });
132
- return buildSyntheticSSE([promptText]);
133
- }
134
-
135
- // Option: "Create another work item" — prompt for name
136
- if (message === 'Create another work item') {
137
- const promptText = 'What should we call this next one?';
138
- appendSessionContent(sessionIdNum, {
139
- role: 'assistant',
140
- content: promptText,
141
- timestamp: new Date().toISOString()
142
- });
143
- return buildSyntheticSSE([promptText]);
144
- }
145
-
146
- // State: user is adding details (previous message was "Tell me more...")
147
- if (lastAssistant.includes('Tell me more about this work item')) {
148
- // Find the last created work item ID from history
149
- const workItemMatch = [...history].reverse()
150
- .find(t => t.role === 'assistant' && /Created \w+ #(\d+):/.test(t.content))
151
- ?.content.match(/Created \w+ #(\d+):/);
152
-
153
- if (workItemMatch) {
154
- const workItemId = parseInt(workItemMatch[1], 10);
155
- try {
156
- updateWorkItemDescription(workItemId, message);
157
- const confirmText = `Updated description for #${workItemId}.`;
158
- appendSessionContent(sessionIdNum, {
159
- role: 'assistant',
160
- content: confirmText + `\n[GATE:question]${BACKLOG_QUESTION_GATE}[/GATE]`,
161
- timestamp: new Date().toISOString()
162
- });
163
- return buildSyntheticSSE([
164
- confirmText,
165
- `[GATE:question]${BACKLOG_QUESTION_GATE}[/GATE]`,
166
- ]);
167
- } catch (err) {
168
- console.error('[backlog] Failed to update description:', err);
169
- }
170
- }
171
- // Fall through to Claude CLI if we can't find the work item
172
- return null;
173
- }
174
-
175
- // Default state: user is providing a work item name — try local parsing
176
- const parsed = parseBacklogInput(message);
177
- if (parsed) {
178
- try {
179
- const workItem = createWorkItem(parsed.type, parsed.title);
180
- const confirmText = `Created ${parsed.type} #${workItem.id}: ${parsed.title}`;
181
- const cardGate = `[GATE:work-item-card]${JSON.stringify({ id: workItem.id, type: workItem.type, title: workItem.title, status: 'backlog' })}[/GATE]`;
182
- const questionGate = `[GATE:question]${BACKLOG_QUESTION_GATE}[/GATE]`;
183
-
184
- appendSessionContent(sessionIdNum, {
185
- role: 'assistant',
186
- content: confirmText + '\n' + cardGate + '\n' + questionGate,
187
- timestamp: new Date().toISOString()
188
- });
189
-
190
- // Send as separate SSE events so stream manager processes each gate independently
191
- return buildSyntheticSSE([confirmText, cardGate, questionGate]);
192
- } catch (err) {
193
- console.error('[backlog-parser] Failed to create work item:', err);
194
- }
195
- }
196
-
197
- // Can't parse locally — fall through to Claude CLI with haiku
198
- return null;
199
- }
200
-
201
- export async function POST(
202
- request: NextRequest,
203
- { params }: { params: Promise<{ sessionId: string }> }
204
- ) {
205
- const { sessionId } = await params;
206
-
207
- // Check for debug mode - shows synthetic messages for troubleshooting (#1000104)
208
- const { searchParams } = new URL(request.url);
209
- const showSynthetic = searchParams.get('debug') === 'true';
210
-
211
- // Validate session ID
212
- if (!isValidSessionId(sessionId)) {
213
- return NextResponse.json(
214
- { type: 'error', message: 'Invalid session' },
215
- { status: 400 }
216
- );
217
- }
218
-
219
- // Check if Claude CLI is available
220
- if (!isClaudeCliAvailable()) {
221
- return NextResponse.json(
222
- { type: 'error', message: 'Claude CLI not found' },
223
- { status: 503 }
224
- );
225
- }
226
-
227
- // Get the message and optional images
228
- const body = await request.json().catch(() => ({}));
229
- const { message, images } = body;
230
-
231
- if (!message || typeof message !== 'string') {
232
- return NextResponse.json(
233
- { type: 'error', message: 'Message is required' },
234
- { status: 400 }
235
- );
236
- }
237
-
238
- // Handle empty/whitespace-only input gracefully
239
- const trimmedMessage = message.trim();
240
- if (!trimmedMessage) {
241
- // Save empty user message to session
242
- const sessionIdNum = parseInt(sessionId, 10);
243
- appendSessionContent(sessionIdNum, {
244
- role: 'user',
245
- content: message,
246
- timestamp: new Date().toISOString()
247
- });
248
-
249
- // Return a helpful response without invoking Claude
250
- const clarificationMessage = 'I didn\'t catch that. What should we call this work item?';
251
-
252
- // Save assistant response to session
253
- appendSessionContent(sessionIdNum, {
254
- role: 'assistant',
255
- content: clarificationMessage,
256
- timestamp: new Date().toISOString()
257
- });
258
-
259
- const encoder = new TextEncoder();
260
- const emptyInputResponse = new ReadableStream({
261
- start(controller) {
262
- const response = {
263
- type: 'assistant',
264
- message: {
265
- content: [{ type: 'text', text: clarificationMessage }]
266
- }
267
- };
268
- controller.enqueue(encoder.encode(`data: ${JSON.stringify(response)}\n\n`));
269
- controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'done', exitCode: 0 })}\n\n`));
270
- controller.close();
271
- }
272
- });
273
-
274
- return new Response(emptyInputResponse, {
275
- headers: {
276
- 'Content-Type': 'text/event-stream',
277
- 'Cache-Control': 'no-cache',
278
- 'Connection': 'keep-alive',
279
- },
280
- });
281
- }
282
-
283
- // Save user message to session content
284
- const sessionIdNum = parseInt(sessionId, 10);
285
- appendSessionContent(sessionIdNum, {
286
- role: 'user',
287
- content: message,
288
- timestamp: new Date().toISOString()
289
- });
290
-
291
- // Fast backlog creation: handle the full conversation flow locally
292
- const session = getSession(sessionIdNum);
293
- if (session?.title === 'Add to Backlog') {
294
- const backlogResponse = handleBacklogMessage(sessionIdNum, trimmedMessage);
295
- if (backlogResponse) return backlogResponse;
296
- // If null, fall through to Claude CLI
297
- }
298
-
299
- // Determine model for CLI: backlog sessions use haiku for faster responses
300
- const isBacklogSession = session?.title === 'Add to Backlog';
301
- const processOptions = isBacklogSession ? { model: 'haiku' } : undefined;
302
-
303
- // Standalone sessions run in main repo (no worktree)
304
- const claudeCwd = getProjectRoot();
305
- const settingsPath = getSettingsPath(claudeCwd);
306
-
307
- // Get or create persistent Claude process for this session
308
- const processResult = getOrCreateProcess(sessionId, claudeCwd, settingsPath, processOptions);
309
-
310
- // Check if we hit the process limit
311
- if ('error' in processResult) {
312
- return NextResponse.json(
313
- { type: 'error', message: processResult.error },
314
- { status: 503 }
315
- );
316
- }
317
-
318
- const { emitter, isNew } = processResult;
319
-
320
- // If process was just spawned for an existing session, restore conversation context
321
- // by prepending stored history to the user's message so Claude has full context
322
- let messageToSend = trimmedMessage;
323
- if (isNew) {
324
- // Load history saved BEFORE this message (the current message was already appended above)
325
- const history = getSessionContent(sessionIdNum);
326
- // Exclude the message we just appended (last user turn)
327
- const priorHistory = history.slice(0, -1);
328
- const prefix = buildContextPrefix(priorHistory);
329
- if (prefix) {
330
- messageToSend = prefix + trimmedMessage;
331
- }
332
- }
333
-
334
- // Create a readable stream for SSE
335
- const encoder = new TextEncoder();
336
-
337
- // Track cleanup state outside the stream so cancel() can access it
338
- let activeEmitter = emitter;
339
- let responseComplete = false;
340
- let onData: ((parsed: Record<string, unknown>) => void) | null = null;
341
- let onError: ((err: { type: string; content: string }) => void) | null = null;
342
- let onClose: ((info: { exitCode: number }) => void) | null = null;
343
-
344
- const removeListeners = () => {
345
- if (onData) activeEmitter.off('data', onData);
346
- if (onError) activeEmitter.off('error', onError);
347
- if (onClose) activeEmitter.off('close', onClose);
348
- };
349
-
350
- const stream = new ReadableStream({
351
- start(controller) {
352
- // Collect assistant response text for saving to session content
353
- let assistantResponse = '';
354
-
355
- // Track if we've saved the response (to avoid duplicate saves)
356
- let responseSaved = false;
357
-
358
- // Helper to save assistant response if not already saved
359
- const saveAssistantResponse = () => {
360
- if (!responseSaved && assistantResponse.trim()) {
361
- appendSessionContent(sessionIdNum, {
362
- role: 'assistant',
363
- content: assistantResponse.trim(),
364
- timestamp: new Date().toISOString()
365
- });
366
- responseSaved = true;
367
- }
368
- };
369
-
370
- // Handle data from the persistent process
371
- onData = (parsed: Record<string, unknown>) => {
372
- if (responseComplete) return;
373
-
374
- // Skip synthetic messages (skill prompt injections) unless in debug mode (#1000104)
375
- // These are internal system prompts that shouldn't normally appear in the conversation
376
- if (parsed.isSynthetic === true && !showSynthetic) {
377
- return;
378
- }
379
-
380
- // Collect text content for session storage
381
- if (parsed.type === 'assistant' && parsed.message) {
382
- const msg = parsed.message as { content?: Array<{ type: string; text?: string; name?: string }> };
383
- if (msg.content) {
384
- for (const block of msg.content) {
385
- if (block.type === 'text' && block.text) {
386
- if (assistantResponse && !assistantResponse.endsWith('\n')) {
387
- assistantResponse += '\n\n';
388
- }
389
- assistantResponse += block.text;
390
- }
391
- // When Claude invokes the Skill tool, save accumulated response immediately
392
- // This prevents losing content when skills are invoked and the turn ends differently
393
- if (block.type === 'tool_use' && block.name === 'Skill') {
394
- saveAssistantResponse();
395
- // Reset for potential post-skill content (Bug #1000097)
396
- // Without this, responseSaved=true blocks saving any content after the skill
397
- assistantResponse = '';
398
- responseSaved = false;
399
- }
400
- }
401
- }
402
- } else if (parsed.type === 'content_block_delta') {
403
- const delta = parsed.delta as { text?: string } | undefined;
404
- if (delta?.text) {
405
- assistantResponse += delta.text;
406
- }
407
- }
408
-
409
- // Check for result message - this indicates Claude is done with this turn
410
- if (parsed.type === 'result') {
411
- responseComplete = true;
412
-
413
- // Save assistant response to session content (if not already saved)
414
- saveAssistantResponse();
415
-
416
- const sseData = `data: ${JSON.stringify(parsed)}\n\n`;
417
- controller.enqueue(encoder.encode(sseData));
418
-
419
- // Send done event and close stream
420
- controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'done', exitCode: 0 })}\n\n`));
421
-
422
- // Remove listeners and close
423
- removeListeners();
424
- controller.close();
425
- return;
426
- }
427
-
428
- const sseData = `data: ${JSON.stringify(parsed)}\n\n`;
429
- controller.enqueue(encoder.encode(sseData));
430
- };
431
-
432
- onError = (err: { type: string; content: string }) => {
433
- if (responseComplete) return;
434
- // Persist error to database (#1000098)
435
- appendSessionContent(sessionIdNum, {
436
- role: 'error',
437
- content: err.content,
438
- timestamp: new Date().toISOString()
439
- });
440
- const sseData = `data: ${JSON.stringify({ type: 'error', content: err.content })}\n\n`;
441
- controller.enqueue(encoder.encode(sseData));
442
- };
443
-
444
- onClose = (info: { exitCode: number }) => {
445
- if (responseComplete) return;
446
- responseComplete = true;
447
-
448
- // Save any collected response (if not already saved)
449
- saveAssistantResponse();
450
-
451
- const sseData = `data: ${JSON.stringify({ type: 'done', exitCode: info.exitCode })}\n\n`;
452
- controller.enqueue(encoder.encode(sseData));
453
- controller.close();
454
- };
455
-
456
- // Attach listeners
457
- emitter.on('data', onData);
458
- emitter.on('error', onError);
459
- emitter.on('close', onClose);
460
-
461
- // Send the message to the persistent process (with context prefix if respawned)
462
- let sent = sendProcessMessage(sessionId, messageToSend, images);
463
-
464
- // Bug #1000096: If send failed, the process may have died after getOrCreateProcess
465
- // Try to create a fresh process and retry once
466
- if (!sent) {
467
- // Kill any zombie process and create fresh one
468
- killProcess(sessionId);
469
-
470
- const retryResult = getOrCreateProcess(sessionId, claudeCwd, settingsPath, processOptions);
471
- if ('error' in retryResult) {
472
- // Hit process limit on retry - return error
473
- controller.enqueue(encoder.encode(`data: ${JSON.stringify({
474
- type: 'error',
475
- content: retryResult.error
476
- })}\n\n`));
477
- controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'done', exitCode: 1 })}\n\n`));
478
- controller.close();
479
- return;
480
- }
481
- const { emitter: newEmitter } = retryResult;
482
-
483
- // Re-attach listeners to new emitter
484
- removeListeners();
485
- activeEmitter = newEmitter;
486
- newEmitter.on('data', onData!);
487
- newEmitter.on('error', onError!);
488
- newEmitter.on('close', onClose!);
489
-
490
- // Retry send (with context prefix since this is also a fresh process)
491
- sent = sendProcessMessage(sessionId, messageToSend, images);
492
- }
493
-
494
- if (!sent) {
495
- // Still failed after retry - give clear error
496
- const errorContent = 'Claude process is unavailable. The process may have crashed or failed to start. Please try again.';
497
- // Persist error to database (#1000098)
498
- appendSessionContent(sessionIdNum, {
499
- role: 'error',
500
- content: errorContent,
501
- timestamp: new Date().toISOString()
502
- });
503
- controller.enqueue(encoder.encode(`data: ${JSON.stringify({
504
- type: 'error',
505
- content: errorContent
506
- })}\n\n`));
507
- controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'done', exitCode: 1 })}\n\n`));
508
- controller.close();
509
- }
510
- },
511
- cancel() {
512
- // Client disconnected - clean up listeners to prevent writes to closed controller
513
- responseComplete = true;
514
- removeListeners();
515
- },
516
- });
517
-
518
- return new Response(stream, {
519
- headers: {
520
- 'Content-Type': 'text/event-stream',
521
- 'Cache-Control': 'no-cache',
522
- 'Connection': 'keep-alive',
523
- },
524
- });
525
- }
@@ -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
- }