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,386 +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 { appendSessionContentByWorkItem, getSessionContentByWorkItem, getWorkItem, ConversationTurn } from '@/lib/db';
6
- import {
7
- getOrCreateProcess,
8
- sendMessage as sendProcessMessage,
9
- killProcess,
10
- } from '@/lib/claude-process-manager';
11
-
12
- // Import worktree facade for worktree management
13
- // eslint-disable-next-line @typescript-eslint/no-require-imports
14
- const worktreeFacade = require('../../../../../../../lib/worktree-facade');
15
-
16
- /**
17
- * Get the project root path for Claude CLI operations.
18
- * In packaged Electron apps, process.cwd() returns the app bundle's Resources directory,
19
- * so we use JETTYPOD_PROJECT_PATH env var which is set correctly by the Electron main process.
20
- */
21
- function getProjectRoot(): string {
22
- return process.env.JETTYPOD_PROJECT_PATH || path.resolve(process.cwd());
23
- }
24
-
25
- /**
26
- * Get the settings path if it exists, otherwise return undefined.
27
- * Claude CLI can run without explicit settings, so this is optional.
28
- */
29
- function getSettingsPath(projectRoot: string): string | undefined {
30
- const settingsPath = path.join(projectRoot, '.claude/settings.json');
31
- return fs.existsSync(settingsPath) ? settingsPath : undefined;
32
- }
33
-
34
- export const dynamic = 'force-dynamic';
35
-
36
- /**
37
- * Build a context restoration prefix from stored conversation history.
38
- * Prepended to the user's message when a Claude process was respawned.
39
- */
40
- function buildContextPrefix(history: ConversationTurn[]): string {
41
- if (history.length === 0) return '';
42
-
43
- const relevant = history.filter(t => t.role === 'user' || t.role === 'assistant');
44
- if (relevant.length === 0) return '';
45
-
46
- const lines = relevant.map(t => {
47
- const label = t.role === 'user' ? 'User' : 'Assistant';
48
- return `${label}: ${t.content}`;
49
- });
50
-
51
- 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`;
52
- }
53
-
54
- // Cache the CLI availability check so we only pay the spawnSync cost once per process lifetime
55
- let claudeCliAvailable: boolean | null = null;
56
-
57
- function isClaudeCliAvailable(): boolean {
58
- if (claudeCliAvailable !== null) return claudeCliAvailable;
59
- const result = spawnSync('which', ['claude'], { encoding: 'utf-8' });
60
- claudeCliAvailable = result.status === 0 && result.stdout.trim().length > 0;
61
- return claudeCliAvailable;
62
- }
63
-
64
- function isValidWorkItemId(id: string): boolean {
65
- const parsed = parseInt(id, 10);
66
- return !isNaN(parsed) && parsed > 0 && String(parsed) === id;
67
- }
68
-
69
- export async function POST(
70
- request: NextRequest,
71
- { params }: { params: Promise<{ workItemId: string }> }
72
- ) {
73
- const { workItemId } = await params;
74
-
75
- // Check for debug mode - shows synthetic messages for troubleshooting (#1000104)
76
- const { searchParams } = new URL(request.url);
77
- const showSynthetic = searchParams.get('debug') === 'true';
78
-
79
- // Validate work item ID
80
- if (!isValidWorkItemId(workItemId)) {
81
- return NextResponse.json(
82
- { type: 'error', message: 'Invalid work item' },
83
- { status: 400 }
84
- );
85
- }
86
-
87
- // Check if Claude CLI is available
88
- if (!isClaudeCliAvailable()) {
89
- return NextResponse.json(
90
- { type: 'error', message: 'Claude CLI not found' },
91
- { status: 503 }
92
- );
93
- }
94
-
95
- // Get the message (conversationHistory no longer needed - persistent process maintains context)
96
- const body = await request.json().catch(() => ({}));
97
- const { message } = body;
98
-
99
- if (!message || typeof message !== 'string') {
100
- return NextResponse.json(
101
- { type: 'error', message: 'Message is required' },
102
- { status: 400 }
103
- );
104
- }
105
-
106
- // Handle empty/whitespace-only input gracefully
107
- const trimmedMessage = message.trim();
108
- if (!trimmedMessage) {
109
- // Save empty user message to session content
110
- const workItemIdNum = parseInt(workItemId, 10);
111
- appendSessionContentByWorkItem(workItemIdNum, {
112
- role: 'user',
113
- content: message,
114
- timestamp: new Date().toISOString()
115
- });
116
-
117
- // Return a helpful response without invoking Claude
118
- const clarificationMessage = 'I didn\'t catch that. What would you like me to help with?';
119
-
120
- // Save assistant response to session content
121
- appendSessionContentByWorkItem(workItemIdNum, {
122
- role: 'assistant',
123
- content: clarificationMessage,
124
- timestamp: new Date().toISOString()
125
- });
126
-
127
- const encoder = new TextEncoder();
128
- const emptyInputResponse = new ReadableStream({
129
- start(controller) {
130
- const response = {
131
- type: 'assistant',
132
- message: {
133
- content: [{ type: 'text', text: clarificationMessage }]
134
- }
135
- };
136
- controller.enqueue(encoder.encode(`data: ${JSON.stringify(response)}\n\n`));
137
- controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'done', exitCode: 0 })}\n\n`));
138
- controller.close();
139
- }
140
- });
141
-
142
- return new Response(emptyInputResponse, {
143
- headers: {
144
- 'Content-Type': 'text/event-stream',
145
- 'Cache-Control': 'no-cache',
146
- 'Connection': 'keep-alive',
147
- },
148
- });
149
- }
150
-
151
- // Save user message to session content
152
- const workItemIdNum = parseInt(workItemId, 10);
153
- appendSessionContentByWorkItem(workItemIdNum, {
154
- role: 'user',
155
- content: message,
156
- timestamp: new Date().toISOString()
157
- });
158
-
159
- // Check if this is a conversational work item (skip worktree)
160
- const workItemData = getWorkItem(workItemIdNum);
161
- const isConversational = workItemData?.conversational === 1;
162
-
163
- const repoPath = getProjectRoot();
164
- let claudeCwd: string;
165
-
166
- if (isConversational) {
167
- // Conversational chores run from main repo — no worktree needed
168
- claudeCwd = repoPath;
169
- } else {
170
- // Get or create worktree for this work item
171
- const workItem = {
172
- id: parseInt(workItemId, 10),
173
- title: `Work item ${workItemId}`
174
- };
175
- const workResult = await worktreeFacade.startWork(workItem, { repoPath });
176
- claudeCwd = workResult.path;
177
- }
178
-
179
- const settingsPath = getSettingsPath(repoPath);
180
-
181
- // Use workItemId prefixed with 'wi-' to avoid collision with standalone session IDs
182
- const processSessionId = `wi-${workItemId}`;
183
-
184
- // Get or create persistent Claude process for this work item
185
- const processResult = getOrCreateProcess(processSessionId, claudeCwd, settingsPath);
186
-
187
- // Check if we hit the process limit
188
- if ('error' in processResult) {
189
- return NextResponse.json(
190
- { type: 'error', message: processResult.error },
191
- { status: 503 }
192
- );
193
- }
194
-
195
- const { emitter, isNew } = processResult;
196
-
197
- // If process was just spawned for an existing session, restore conversation context
198
- let messageToSend = trimmedMessage;
199
- if (isNew) {
200
- const history = getSessionContentByWorkItem(workItemIdNum);
201
- // Exclude the message we just appended (last user turn)
202
- const priorHistory = history.slice(0, -1);
203
- const prefix = buildContextPrefix(priorHistory);
204
- if (prefix) {
205
- messageToSend = prefix + trimmedMessage;
206
- }
207
- }
208
-
209
- // Create a readable stream for SSE
210
- const encoder = new TextEncoder();
211
-
212
- const stream = new ReadableStream({
213
- start(controller) {
214
- // Collect assistant response text for saving to session content
215
- let assistantResponse = '';
216
- let responseComplete = false;
217
-
218
- // Track if we've saved the response (to avoid duplicate saves)
219
- let responseSaved = false;
220
-
221
- // Helper to save assistant response if not already saved
222
- const saveAssistantResponse = () => {
223
- if (!responseSaved && assistantResponse.trim()) {
224
- appendSessionContentByWorkItem(workItemIdNum, {
225
- role: 'assistant',
226
- content: assistantResponse.trim(),
227
- timestamp: new Date().toISOString()
228
- });
229
- responseSaved = true;
230
- }
231
- };
232
-
233
- // Handle data from the persistent process
234
- const onData = (parsed: Record<string, unknown>) => {
235
- if (responseComplete) return;
236
-
237
- // Skip synthetic messages (skill prompt injections) unless in debug mode (#1000104)
238
- // These are internal system prompts that shouldn't normally appear in the conversation
239
- if (parsed.isSynthetic === true && !showSynthetic) {
240
- return;
241
- }
242
-
243
- // Collect text content for session storage
244
- if (parsed.type === 'assistant' && parsed.message) {
245
- const msg = parsed.message as { content?: Array<{ type: string; text?: string; name?: string }> };
246
- if (msg.content) {
247
- for (const block of msg.content) {
248
- if (block.type === 'text' && block.text) {
249
- if (assistantResponse && !assistantResponse.endsWith('\n')) {
250
- assistantResponse += '\n\n';
251
- }
252
- assistantResponse += block.text;
253
- }
254
- // When Claude invokes the Skill tool, save accumulated response immediately
255
- // This prevents losing content when skills are invoked and the turn ends differently
256
- if (block.type === 'tool_use' && block.name === 'Skill') {
257
- saveAssistantResponse();
258
- // Reset for potential post-skill content (Bug #1000097)
259
- // Without this, responseSaved=true blocks saving any content after the skill
260
- assistantResponse = '';
261
- responseSaved = false;
262
- }
263
- }
264
- }
265
- } else if (parsed.type === 'content_block_delta') {
266
- const delta = parsed.delta as { text?: string } | undefined;
267
- if (delta?.text) {
268
- assistantResponse += delta.text;
269
- }
270
- }
271
-
272
- // Check for result message - this indicates Claude is done with this turn
273
- if (parsed.type === 'result') {
274
- responseComplete = true;
275
-
276
- // Save assistant response to session content (if not already saved)
277
- saveAssistantResponse();
278
-
279
- const sseData = `data: ${JSON.stringify(parsed)}\n\n`;
280
- controller.enqueue(encoder.encode(sseData));
281
-
282
- // Send done event and close stream
283
- controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'done', exitCode: 0 })}\n\n`));
284
-
285
- // Remove listeners and close
286
- emitter.off('data', onData);
287
- emitter.off('error', onError);
288
- emitter.off('close', onClose);
289
- controller.close();
290
- return;
291
- }
292
-
293
- const sseData = `data: ${JSON.stringify(parsed)}\n\n`;
294
- controller.enqueue(encoder.encode(sseData));
295
- };
296
-
297
- const onError = (err: { type: string; content: string }) => {
298
- if (responseComplete) return;
299
- // Persist error to database (#1000098)
300
- appendSessionContentByWorkItem(workItemIdNum, {
301
- role: 'error',
302
- content: err.content,
303
- timestamp: new Date().toISOString()
304
- });
305
- const sseData = `data: ${JSON.stringify({ type: 'error', content: err.content })}\n\n`;
306
- controller.enqueue(encoder.encode(sseData));
307
- };
308
-
309
- const onClose = (info: { exitCode: number }) => {
310
- if (responseComplete) return;
311
- responseComplete = true;
312
-
313
- // Save any collected response (if not already saved)
314
- saveAssistantResponse();
315
-
316
- const sseData = `data: ${JSON.stringify({ type: 'done', exitCode: info.exitCode })}\n\n`;
317
- controller.enqueue(encoder.encode(sseData));
318
- controller.close();
319
- };
320
-
321
- // Attach listeners
322
- emitter.on('data', onData);
323
- emitter.on('error', onError);
324
- emitter.on('close', onClose);
325
-
326
- // Send the message to the persistent process
327
- let sent = sendProcessMessage(processSessionId, messageToSend);
328
-
329
- // If send failed, the process may have died after getOrCreateProcess
330
- // Try to create a fresh process and retry once
331
- if (!sent) {
332
- // Kill any zombie process and create fresh one
333
- killProcess(processSessionId);
334
-
335
- const retryResult = getOrCreateProcess(processSessionId, claudeCwd, settingsPath);
336
- if ('error' in retryResult) {
337
- // Hit process limit on retry - return error
338
- controller.enqueue(encoder.encode(`data: ${JSON.stringify({
339
- type: 'error',
340
- content: retryResult.error
341
- })}\n\n`));
342
- controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'done', exitCode: 1 })}\n\n`));
343
- controller.close();
344
- return;
345
- }
346
- const { emitter: newEmitter } = retryResult;
347
-
348
- // Re-attach listeners to new emitter
349
- emitter.off('data', onData);
350
- emitter.off('error', onError);
351
- emitter.off('close', onClose);
352
- newEmitter.on('data', onData);
353
- newEmitter.on('error', onError);
354
- newEmitter.on('close', onClose);
355
-
356
- // Retry send
357
- sent = sendProcessMessage(processSessionId, messageToSend);
358
- }
359
-
360
- if (!sent) {
361
- // Still failed after retry - give clear error
362
- const errorContent = 'Claude process is unavailable. The process may have crashed or failed to start. Please try again.';
363
- // Persist error to database (#1000098)
364
- appendSessionContentByWorkItem(workItemIdNum, {
365
- role: 'error',
366
- content: errorContent,
367
- timestamp: new Date().toISOString()
368
- });
369
- controller.enqueue(encoder.encode(`data: ${JSON.stringify({
370
- type: 'error',
371
- content: errorContent
372
- })}\n\n`));
373
- controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'done', exitCode: 1 })}\n\n`));
374
- controller.close();
375
- }
376
- },
377
- });
378
-
379
- return new Response(stream, {
380
- headers: {
381
- 'Content-Type': 'text/event-stream',
382
- 'Cache-Control': 'no-cache',
383
- 'Connection': 'keep-alive',
384
- },
385
- });
386
- }
@@ -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/[workItemId]/pin - Pin work item session to prevent idle cleanup
7
- export async function POST(
8
- _request: NextRequest,
9
- { params }: { params: Promise<{ workItemId: string }> }
10
- ) {
11
- const { workItemId } = await params;
12
- pinSession(`wi-${workItemId}`);
13
- return NextResponse.json({ pinned: true });
14
- }
15
-
16
- // DELETE /api/claude/[workItemId]/pin - Unpin work item session to allow idle cleanup
17
- export async function DELETE(
18
- _request: NextRequest,
19
- { params }: { params: Promise<{ workItemId: string }> }
20
- ) {
21
- const { workItemId } = await params;
22
- unpinSession(`wi-${workItemId}`);
23
- return NextResponse.json({ pinned: false });
24
- }
@@ -1,167 +0,0 @@
1
- import { spawn, spawnSync } from 'child_process';
2
- import { NextRequest, NextResponse } from 'next/server';
3
- import path from 'path';
4
- import fs from 'fs';
5
- import { registerSession, updateSessionStatus } from '@/lib/db';
6
-
7
- // Import worktree facade for worktree management
8
- // eslint-disable-next-line @typescript-eslint/no-require-imports
9
- const worktreeFacade = require('../../../../../../lib/worktree-facade');
10
-
11
- /**
12
- * Get the project root path for Claude CLI operations.
13
- * In packaged Electron apps, process.cwd() returns the app bundle's Resources directory,
14
- * so we use JETTYPOD_PROJECT_PATH env var which is set correctly by the Electron main process.
15
- */
16
- function getProjectRoot(): string {
17
- return process.env.JETTYPOD_PROJECT_PATH || path.resolve(process.cwd());
18
- }
19
-
20
- /**
21
- * Get the settings path if it exists, otherwise return undefined.
22
- * Claude CLI can run without explicit settings, so this is optional.
23
- */
24
- function getSettingsPath(projectRoot: string): string | undefined {
25
- const settingsPath = path.join(projectRoot, '.claude/settings.json');
26
- return fs.existsSync(settingsPath) ? settingsPath : undefined;
27
- }
28
-
29
- export const dynamic = 'force-dynamic';
30
-
31
- function isClaudeCliAvailable(): boolean {
32
- const result = spawnSync('which', ['claude'], { encoding: 'utf-8' });
33
- return result.status === 0 && result.stdout.trim().length > 0;
34
- }
35
-
36
- function isValidWorkItemId(id: string): boolean {
37
- const parsed = parseInt(id, 10);
38
- return !isNaN(parsed) && parsed > 0 && String(parsed) === id;
39
- }
40
-
41
- export async function POST(
42
- request: NextRequest,
43
- { params }: { params: Promise<{ workItemId: string }> }
44
- ) {
45
- const { workItemId } = await params;
46
-
47
- // Validate work item ID
48
- if (!isValidWorkItemId(workItemId)) {
49
- return NextResponse.json(
50
- { type: 'error', message: 'Invalid work item' },
51
- { status: 400 }
52
- );
53
- }
54
-
55
- // Check if Claude CLI is available
56
- if (!isClaudeCliAvailable()) {
57
- return NextResponse.json(
58
- { type: 'error', message: 'Claude CLI not found' },
59
- { status: 503 }
60
- );
61
- }
62
-
63
- // Get the work item context to build the prompt
64
- const body = await request.json().catch(() => ({}));
65
- const { title, description, type } = body;
66
-
67
- // Determine work item type label for the prompt
68
- const workItemType = type || 'chore';
69
-
70
- // Get or create worktree for this work item
71
- const workItem = {
72
- id: parseInt(workItemId, 10),
73
- title: title || `Work item ${workItemId}`
74
- };
75
-
76
- // Determine the repo path - use env var in packaged apps, fallback to cwd
77
- const repoPath = getProjectRoot();
78
-
79
- // Check for existing worktree or create one
80
- const workResult = await worktreeFacade.startWork(workItem, { repoPath });
81
-
82
- // Use worktree path if available, otherwise fall back to main repo
83
- const claudeCwd = workResult.path;
84
-
85
- // Build the prompt for Claude based on work item type
86
- const prompt = `You are working on ${workItemType} #${workItemId}: ${title || 'Unknown task'}
87
- ${description ? `\nDescription: ${description}` : ''}
88
-
89
- Please start working on this ${workItemType}. Use the appropriate tools to implement the required changes.`;
90
-
91
- // Register session in database before spawning Claude
92
- const sessionTitle = title || `Work item ${workItemId}`;
93
- registerSession(parseInt(workItemId, 10), sessionTitle);
94
-
95
- // Create a readable stream that we'll pipe Claude's output to
96
- const encoder = new TextEncoder();
97
-
98
- const stream = new ReadableStream({
99
- start(controller) {
100
- // Spawn Claude CLI with streaming JSON output in the worktree directory
101
- // Use bypassPermissions mode + explicit settings path (if exists) to enable hooks while avoiding prompts
102
- const settingsPath = getSettingsPath(repoPath);
103
- const claudeArgs = [
104
- '-p', prompt,
105
- '--output-format', 'stream-json',
106
- '--verbose',
107
- '--permission-mode', 'bypassPermissions',
108
- ];
109
- if (settingsPath) {
110
- claudeArgs.push('--settings', settingsPath);
111
- }
112
- const claude = spawn('claude', claudeArgs, {
113
- cwd: claudeCwd,
114
- env: { ...process.env },
115
- stdio: ['ignore', 'pipe', 'pipe'],
116
- });
117
-
118
- claude.stdout.on('data', (data: Buffer) => {
119
- // Parse each line of streaming JSON and send as SSE
120
- const lines = data.toString().split('\n').filter(line => line.trim());
121
- for (const line of lines) {
122
- try {
123
- const parsed = JSON.parse(line);
124
- const sseData = `data: ${JSON.stringify(parsed)}\n\n`;
125
- controller.enqueue(encoder.encode(sseData));
126
- } catch {
127
- // If not valid JSON, send as raw text
128
- const sseData = `data: ${JSON.stringify({ type: 'text', content: line })}\n\n`;
129
- controller.enqueue(encoder.encode(sseData));
130
- }
131
- }
132
- });
133
-
134
- claude.stderr.on('data', (data: Buffer) => {
135
- const sseData = `data: ${JSON.stringify({ type: 'error', content: data.toString() })}\n\n`;
136
- controller.enqueue(encoder.encode(sseData));
137
- });
138
-
139
- claude.on('close', (code) => {
140
- // Update session status based on exit code
141
- const status = code === 0 ? 'completed' : 'error';
142
- updateSessionStatus(parseInt(workItemId, 10), status);
143
-
144
- const sseData = `data: ${JSON.stringify({ type: 'done', exitCode: code })}\n\n`;
145
- controller.enqueue(encoder.encode(sseData));
146
- controller.close();
147
- });
148
-
149
- claude.on('error', (err) => {
150
- // Mark session as error on spawn failure
151
- updateSessionStatus(parseInt(workItemId, 10), 'error');
152
-
153
- const sseData = `data: ${JSON.stringify({ type: 'error', content: err.message })}\n\n`;
154
- controller.enqueue(encoder.encode(sseData));
155
- controller.close();
156
- });
157
- },
158
- });
159
-
160
- return new Response(stream, {
161
- headers: {
162
- 'Content-Type': 'text/event-stream',
163
- 'Cache-Control': 'no-cache',
164
- 'Connection': 'keep-alive',
165
- },
166
- });
167
- }
@@ -1,52 +0,0 @@
1
- import { NextRequest, NextResponse } from 'next/server';
2
- import { getSessionContent, getSessionContentByWorkItem } from '@/lib/db';
3
-
4
- export const dynamic = 'force-dynamic';
5
-
6
- function isValidSessionId(id: string): boolean {
7
- const parsed = parseInt(id, 10);
8
- return !isNaN(parsed) && parsed > 0 && String(parsed) === id;
9
- }
10
-
11
- // GET /api/claude/sessions/[sessionId]/content - Get session conversation history
12
- // Use ?by=workitem to look up by work_item_id instead of session id
13
- export async function GET(
14
- request: NextRequest,
15
- { params }: { params: Promise<{ sessionId: string }> }
16
- ) {
17
- const { sessionId } = await params;
18
- const { searchParams } = new URL(request.url);
19
- const lookupBy = searchParams.get('by');
20
-
21
- if (!isValidSessionId(sessionId)) {
22
- return NextResponse.json(
23
- { error: 'Invalid session ID' },
24
- { status: 400 }
25
- );
26
- }
27
-
28
- try {
29
- const id = parseInt(sessionId, 10);
30
- const dbContent = lookupBy === 'workitem'
31
- ? getSessionContentByWorkItem(id)
32
- : getSessionContent(id);
33
-
34
- // Transform DB format (role) to frontend format (type)
35
- // DB stores: { role: 'user'|'assistant'|'error', content, timestamp }
36
- // Frontend expects: { type: 'user'|'assistant'|'error', content, timestamp }
37
- // Includes error messages persisted during conversation (#1000098, #1000099)
38
- const content = dbContent.map(msg => ({
39
- type: msg.role,
40
- content: msg.content,
41
- timestamp: msg.timestamp,
42
- }));
43
-
44
- return NextResponse.json({ content });
45
- } catch (error) {
46
- console.error('Failed to get session content:', error);
47
- return NextResponse.json(
48
- { error: 'Failed to get session content' },
49
- { status: 500 }
50
- );
51
- }
52
- }