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,36 +0,0 @@
1
- This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
2
-
3
- ## Getting Started
4
-
5
- First, run the development server:
6
-
7
- ```bash
8
- npm run dev
9
- # or
10
- yarn dev
11
- # or
12
- pnpm dev
13
- # or
14
- bun dev
15
- ```
16
-
17
- Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18
-
19
- You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20
-
21
- This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
22
-
23
- ## Learn More
24
-
25
- To learn more about Next.js, take a look at the following resources:
26
-
27
- - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28
- - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29
-
30
- You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31
-
32
- ## Deploy on Vercel
33
-
34
- The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35
-
36
- Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
@@ -1,446 +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
- /**
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
- /**
33
- * Build a context restoration prefix from stored conversation history.
34
- * Prepended to the user's message when a Claude process was respawned.
35
- */
36
- function buildContextPrefix(history: ConversationTurn[]): string {
37
- if (history.length === 0) return '';
38
-
39
- const relevant = history.filter(t => t.role === 'user' || t.role === 'assistant');
40
- if (relevant.length === 0) return '';
41
-
42
- const lines = relevant.map(t => {
43
- const label = t.role === 'user' ? 'User' : 'Assistant';
44
- return `${label}: ${t.content}`;
45
- });
46
-
47
- 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`;
48
- }
49
-
50
- // Cache the CLI availability check so we only pay the spawnSync cost once per process lifetime
51
- let claudeCliAvailable: boolean | null = null;
52
-
53
- function isClaudeCliAvailable(): boolean {
54
- if (claudeCliAvailable !== null) return claudeCliAvailable;
55
- const result = spawnSync('which', ['claude'], { encoding: 'utf-8' });
56
- claudeCliAvailable = result.status === 0 && result.stdout.trim().length > 0;
57
- return claudeCliAvailable;
58
- }
59
-
60
- function isValidWorkItemId(id: string): boolean {
61
- const parsed = parseInt(id, 10);
62
- return !isNaN(parsed) && parsed > 0 && String(parsed) === id;
63
- }
64
-
65
- export async function POST(
66
- request: NextRequest,
67
- { params }: { params: Promise<{ workItemId: string }> }
68
- ) {
69
- const { workItemId } = await params;
70
-
71
- // Check for debug mode - shows synthetic messages for troubleshooting (#1000104)
72
- const { searchParams } = new URL(request.url);
73
- const showSynthetic = searchParams.get('debug') === 'true';
74
-
75
- // Validate work item ID
76
- if (!isValidWorkItemId(workItemId)) {
77
- return NextResponse.json(
78
- { type: 'error', message: 'Invalid work item' },
79
- { status: 400 }
80
- );
81
- }
82
-
83
- // Check if Claude CLI is available
84
- if (!isClaudeCliAvailable()) {
85
- return NextResponse.json(
86
- { type: 'error', message: 'Claude CLI not found' },
87
- { status: 503 }
88
- );
89
- }
90
-
91
- // Get the message and optional images
92
- const body = await request.json().catch(() => ({}));
93
- const { message, images } = body;
94
-
95
- if (!message || typeof message !== 'string') {
96
- return NextResponse.json(
97
- { type: 'error', message: 'Message is required' },
98
- { status: 400 }
99
- );
100
- }
101
-
102
- // Handle empty/whitespace-only input gracefully
103
- const trimmedMessage = message.trim();
104
- if (!trimmedMessage) {
105
- // Save empty user message to session content
106
- const workItemIdNum = parseInt(workItemId, 10);
107
- appendSessionContentByWorkItem(workItemIdNum, {
108
- role: 'user',
109
- content: message,
110
- timestamp: new Date().toISOString()
111
- });
112
-
113
- // Return a helpful response without invoking Claude
114
- const clarificationMessage = 'I didn\'t catch that. What would you like me to help with?';
115
-
116
- // Save assistant response to session content
117
- appendSessionContentByWorkItem(workItemIdNum, {
118
- role: 'assistant',
119
- content: clarificationMessage,
120
- timestamp: new Date().toISOString()
121
- });
122
-
123
- const encoder = new TextEncoder();
124
- const emptyInputResponse = new ReadableStream({
125
- start(controller) {
126
- const response = {
127
- type: 'assistant',
128
- message: {
129
- content: [{ type: 'text', text: clarificationMessage }]
130
- }
131
- };
132
- controller.enqueue(encoder.encode(`data: ${JSON.stringify(response)}\n\n`));
133
- controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'done', exitCode: 0 })}\n\n`));
134
- controller.close();
135
- }
136
- });
137
-
138
- return new Response(emptyInputResponse, {
139
- headers: {
140
- 'Content-Type': 'text/event-stream',
141
- 'Cache-Control': 'no-cache',
142
- 'Connection': 'keep-alive',
143
- },
144
- });
145
- }
146
-
147
- // Save user message to session content
148
- const workItemIdNum = parseInt(workItemId, 10);
149
- appendSessionContentByWorkItem(workItemIdNum, {
150
- role: 'user',
151
- content: message,
152
- timestamp: new Date().toISOString()
153
- });
154
-
155
- // Check if this is a conversational work item (skip worktree)
156
- const workItemData = getWorkItem(workItemIdNum);
157
- const isConversational = workItemData?.conversational === 1;
158
-
159
- const repoPath = getProjectRoot();
160
- let claudeCwd: string;
161
-
162
- if (isConversational) {
163
- // Conversational chores run from main repo — no worktree needed
164
- claudeCwd = repoPath;
165
- } else {
166
- // Non-conversational: attempt to create worktree via lazy-loaded facade.
167
- // worktree-facade depends on the CLI's sqlite3 module which is not a dashboard
168
- // dependency — lazy-load to avoid crashing the entire route module at import time.
169
- try {
170
- // eslint-disable-next-line @typescript-eslint/no-require-imports
171
- const worktreeFacade = require('../../../../../../../lib/worktree-facade');
172
- const workItem = {
173
- id: parseInt(workItemId, 10),
174
- title: `Work item ${workItemId}`
175
- };
176
- const workResult = await worktreeFacade.startWork(workItem, { repoPath });
177
- claudeCwd = workResult.path;
178
- } catch {
179
- // Worktree creation unavailable or failed — fall back to main repo
180
- claudeCwd = repoPath;
181
- }
182
- }
183
-
184
- const settingsPath = getSettingsPath(repoPath);
185
-
186
- // Use workItemId prefixed with 'wi-' to avoid collision with standalone session IDs
187
- const processSessionId = `wi-${workItemId}`;
188
-
189
- // Get or create persistent Claude process for this work item
190
- const processResult = getOrCreateProcess(processSessionId, claudeCwd, settingsPath);
191
-
192
- // Check if we hit the process limit
193
- if ('error' in processResult) {
194
- return NextResponse.json(
195
- { type: 'error', message: processResult.error },
196
- { status: 503 }
197
- );
198
- }
199
-
200
- const { emitter, isNew } = processResult;
201
-
202
- // If process was just spawned for an existing session, restore conversation context
203
- let messageToSend = trimmedMessage;
204
- if (isNew) {
205
- const history = getSessionContentByWorkItem(workItemIdNum);
206
- // Exclude the message we just appended (last user turn)
207
- const priorHistory = history.slice(0, -1);
208
- const prefix = buildContextPrefix(priorHistory);
209
- if (prefix) {
210
- messageToSend = prefix + trimmedMessage;
211
- }
212
- }
213
-
214
- // Create a readable stream for SSE
215
- const encoder = new TextEncoder();
216
-
217
- // Cleanup function hoisted so cancel() can access it when client disconnects
218
- let cleanupStreamListeners: (() => void) | null = null;
219
-
220
- const stream = new ReadableStream({
221
- start(controller) {
222
- // Collect assistant response text for saving to session content
223
- let assistantResponse = '';
224
- let responseComplete = false;
225
-
226
- // Track if we've saved the response (to avoid duplicate saves)
227
- let responseSaved = false;
228
-
229
- // Track active emitter (may change during retry)
230
- let currentEmitter = emitter;
231
-
232
- // Safe enqueue — if the stream has been closed (client disconnect, error),
233
- // enqueue() throws. Without this wrapper, the error propagates through
234
- // the EventEmitter chain and crashes the process manager's stdout handler,
235
- // silently killing all subsequent events for this session.
236
- const safeEnqueue = (data: Uint8Array): boolean => {
237
- try {
238
- controller.enqueue(data);
239
- return true;
240
- } catch {
241
- // Stream is dead — clean up
242
- if (!responseComplete) {
243
- responseComplete = true;
244
- saveAssistantResponse();
245
- currentEmitter.off('data', onData);
246
- currentEmitter.off('error', onError);
247
- currentEmitter.off('close', onClose);
248
- if (heartbeatInterval) clearInterval(heartbeatInterval);
249
- }
250
- return false;
251
- }
252
- };
253
-
254
- // SSE heartbeat — send a comment every 15s to keep the connection alive.
255
- // During long tool executions (jettypod work start, npm test, etc.) the
256
- // SSE stream can be idle for 30+ seconds. Proxies and HTTP middleware
257
- // may close idle connections, killing the stream mid-session.
258
- const heartbeatInterval = setInterval(() => {
259
- if (responseComplete) {
260
- clearInterval(heartbeatInterval);
261
- return;
262
- }
263
- safeEnqueue(encoder.encode(': heartbeat\n\n'));
264
- }, 15_000);
265
-
266
- // Helper to save assistant response if not already saved
267
- const saveAssistantResponse = () => {
268
- if (!responseSaved && assistantResponse.trim()) {
269
- appendSessionContentByWorkItem(workItemIdNum, {
270
- role: 'assistant',
271
- content: assistantResponse.trim(),
272
- timestamp: new Date().toISOString()
273
- });
274
- responseSaved = true;
275
- }
276
- };
277
-
278
- // Handle data from the persistent process
279
- const onData = (parsed: Record<string, unknown>) => {
280
- if (responseComplete) return;
281
-
282
- // Skip synthetic messages (skill prompt injections) unless in debug mode (#1000104)
283
- // These are internal system prompts that shouldn't normally appear in the conversation
284
- if (parsed.isSynthetic === true && !showSynthetic) {
285
- return;
286
- }
287
-
288
- // Collect text content for session storage
289
- if (parsed.type === 'assistant' && parsed.message) {
290
- const msg = parsed.message as { content?: Array<{ type: string; text?: string; name?: string }> };
291
- if (msg.content) {
292
- for (const block of msg.content) {
293
- if (block.type === 'text' && block.text) {
294
- if (assistantResponse && !assistantResponse.endsWith('\n')) {
295
- assistantResponse += '\n\n';
296
- }
297
- assistantResponse += block.text;
298
- }
299
- // When Claude invokes the Skill tool, save accumulated response immediately
300
- // This prevents losing content when skills are invoked and the turn ends differently
301
- if (block.type === 'tool_use' && block.name === 'Skill') {
302
- saveAssistantResponse();
303
- // Reset for potential post-skill content (Bug #1000097)
304
- // Without this, responseSaved=true blocks saving any content after the skill
305
- assistantResponse = '';
306
- responseSaved = false;
307
- }
308
- }
309
- }
310
- } else if (parsed.type === 'content_block_delta') {
311
- const delta = parsed.delta as { text?: string } | undefined;
312
- if (delta?.text) {
313
- assistantResponse += delta.text;
314
- }
315
- }
316
-
317
- // Check for result message - this indicates Claude is done with this turn
318
- if (parsed.type === 'result') {
319
- responseComplete = true;
320
- clearInterval(heartbeatInterval);
321
-
322
- // Save assistant response to session content (if not already saved)
323
- saveAssistantResponse();
324
-
325
- safeEnqueue(encoder.encode(`data: ${JSON.stringify(parsed)}\n\n`));
326
- safeEnqueue(encoder.encode(`data: ${JSON.stringify({ type: 'done', exitCode: 0 })}\n\n`));
327
-
328
- // Remove listeners and close
329
- currentEmitter.off('data', onData);
330
- currentEmitter.off('error', onError);
331
- currentEmitter.off('close', onClose);
332
- try { controller.close(); } catch { /* already closed */ }
333
- return;
334
- }
335
-
336
- safeEnqueue(encoder.encode(`data: ${JSON.stringify(parsed)}\n\n`));
337
- };
338
-
339
- const onError = (err: { type: string; content: string }) => {
340
- if (responseComplete) return;
341
- // Persist error to database (#1000098)
342
- appendSessionContentByWorkItem(workItemIdNum, {
343
- role: 'error',
344
- content: err.content,
345
- timestamp: new Date().toISOString()
346
- });
347
- safeEnqueue(encoder.encode(`data: ${JSON.stringify({ type: 'error', content: err.content })}\n\n`));
348
- };
349
-
350
- const onClose = (info: { exitCode: number }) => {
351
- if (responseComplete) return;
352
- responseComplete = true;
353
- clearInterval(heartbeatInterval);
354
-
355
- // Save any collected response (if not already saved)
356
- saveAssistantResponse();
357
-
358
- safeEnqueue(encoder.encode(`data: ${JSON.stringify({ type: 'done', exitCode: info.exitCode })}\n\n`));
359
- try { controller.close(); } catch { /* already closed */ }
360
- };
361
-
362
- // Expose cleanup so cancel() can call it when client disconnects
363
- cleanupStreamListeners = () => {
364
- if (responseComplete) return;
365
- responseComplete = true;
366
- clearInterval(heartbeatInterval);
367
- saveAssistantResponse();
368
- currentEmitter.off('data', onData);
369
- currentEmitter.off('error', onError);
370
- currentEmitter.off('close', onClose);
371
- };
372
-
373
- // Attach listeners
374
- currentEmitter.on('data', onData);
375
- currentEmitter.on('error', onError);
376
- currentEmitter.on('close', onClose);
377
-
378
- // Send the message to the persistent process
379
- let sent = sendProcessMessage(processSessionId, messageToSend, images);
380
-
381
- // If send failed, the process may have died after getOrCreateProcess
382
- // Try to create a fresh process and retry once
383
- if (!sent) {
384
- // Kill any zombie process and create fresh one
385
- killProcess(processSessionId);
386
-
387
- const retryResult = getOrCreateProcess(processSessionId, claudeCwd, settingsPath);
388
- if ('error' in retryResult) {
389
- // Hit process limit on retry - return error
390
- clearInterval(heartbeatInterval);
391
- safeEnqueue(encoder.encode(`data: ${JSON.stringify({
392
- type: 'error',
393
- content: retryResult.error
394
- })}\n\n`));
395
- safeEnqueue(encoder.encode(`data: ${JSON.stringify({ type: 'done', exitCode: 1 })}\n\n`));
396
- try { controller.close(); } catch { /* already closed */ }
397
- return;
398
- }
399
- const { emitter: newEmitter } = retryResult;
400
-
401
- // Re-attach listeners to new emitter
402
- currentEmitter.off('data', onData);
403
- currentEmitter.off('error', onError);
404
- currentEmitter.off('close', onClose);
405
- currentEmitter = newEmitter;
406
- currentEmitter.on('data', onData);
407
- currentEmitter.on('error', onError);
408
- currentEmitter.on('close', onClose);
409
-
410
- // Retry send
411
- sent = sendProcessMessage(processSessionId, messageToSend, images);
412
- }
413
-
414
- if (!sent) {
415
- // Still failed after retry - give clear error
416
- clearInterval(heartbeatInterval);
417
- const errorContent = 'Claude process is unavailable. The process may have crashed or failed to start. Please try again.';
418
- // Persist error to database (#1000098)
419
- appendSessionContentByWorkItem(workItemIdNum, {
420
- role: 'error',
421
- content: errorContent,
422
- timestamp: new Date().toISOString()
423
- });
424
- safeEnqueue(encoder.encode(`data: ${JSON.stringify({
425
- type: 'error',
426
- content: errorContent
427
- })}\n\n`));
428
- safeEnqueue(encoder.encode(`data: ${JSON.stringify({ type: 'done', exitCode: 1 })}\n\n`));
429
- try { controller.close(); } catch { /* already closed */ }
430
- }
431
- },
432
- cancel() {
433
- // Called when client disconnects — clean up listeners to prevent
434
- // "Controller is already closed" errors from orphaned event handlers
435
- cleanupStreamListeners?.();
436
- },
437
- });
438
-
439
- return new Response(stream, {
440
- headers: {
441
- 'Content-Type': 'text/event-stream',
442
- 'Cache-Control': 'no-cache',
443
- 'Connection': 'keep-alive',
444
- },
445
- });
446
- }
@@ -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
- }