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.
- package/.env +2 -1
- package/Cargo.lock +6450 -0
- package/Cargo.toml +35 -0
- package/README.md +5 -1
- package/TAURI-MIGRATION-PLAN.md +840 -0
- package/apps/dashboard/app/connect-claude/page.tsx +5 -6
- package/apps/dashboard/app/decision/[id]/page.tsx +54 -49
- package/apps/dashboard/app/demo/gates/page.tsx +3 -5
- package/apps/dashboard/app/design-system/page.tsx +1 -1
- package/apps/dashboard/app/globals.css +74 -2
- package/apps/dashboard/app/install-claude/page.tsx +3 -5
- package/apps/dashboard/app/login/page.tsx +17 -20
- package/apps/dashboard/app/page.tsx +101 -48
- package/apps/dashboard/app/settings/page.tsx +60 -12
- package/apps/dashboard/app/signup/page.tsx +14 -17
- package/apps/dashboard/app/subscribe/page.tsx +0 -2
- package/apps/dashboard/app/tests/page.tsx +37 -4
- package/apps/dashboard/app/welcome/page.tsx +12 -15
- package/apps/dashboard/app/work/[id]/page.tsx +90 -75
- package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
- package/apps/dashboard/components/AppShell.tsx +70 -61
- package/apps/dashboard/components/CardMenu.tsx +0 -1
- package/apps/dashboard/components/ClaudePanel.tsx +541 -283
- package/apps/dashboard/components/ClaudePanelInput.tsx +23 -4
- package/apps/dashboard/components/ConnectClaudeScreen.tsx +1 -5
- package/apps/dashboard/components/CopyableId.tsx +1 -2
- package/apps/dashboard/components/DetailReviewActions.tsx +11 -20
- package/apps/dashboard/components/DragContext.tsx +132 -62
- package/apps/dashboard/components/DraggableCard.tsx +3 -5
- package/apps/dashboard/components/DropZone.tsx +5 -6
- package/apps/dashboard/components/EditableDetailDescription.tsx +6 -12
- package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
- package/apps/dashboard/components/EditableTitle.tsx +0 -1
- package/apps/dashboard/components/ElapsedTimer.tsx +15 -3
- package/apps/dashboard/components/EpicGroup.tsx +100 -70
- package/apps/dashboard/components/GateCard.tsx +0 -1
- package/apps/dashboard/components/GateChoiceCard.tsx +1 -2
- package/apps/dashboard/components/InstallClaudeScreen.tsx +1 -5
- package/apps/dashboard/components/JettyLoader.tsx +0 -1
- package/apps/dashboard/components/KanbanBoard.tsx +319 -173
- package/apps/dashboard/components/KanbanCard.tsx +341 -107
- package/apps/dashboard/components/LazyCard.tsx +62 -0
- package/apps/dashboard/components/LazyMarkdown.tsx +0 -1
- package/apps/dashboard/components/MainNav.tsx +24 -25
- package/apps/dashboard/components/MessageBlock.tsx +93 -16
- package/apps/dashboard/components/ModeStartCard.tsx +0 -1
- package/apps/dashboard/components/OnboardingWelcome.tsx +0 -1
- package/apps/dashboard/components/PlaceholderCard.tsx +0 -1
- package/apps/dashboard/components/ProjectSwitcher.tsx +20 -20
- package/apps/dashboard/components/PrototypeTimeline.tsx +47 -26
- package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +308 -223
- package/apps/dashboard/components/RealTimeTestsWrapper.tsx +303 -160
- package/apps/dashboard/components/ReviewFooter.tsx +12 -14
- package/apps/dashboard/components/SessionList.tsx +0 -1
- package/apps/dashboard/components/SubscribeContent.tsx +40 -11
- package/apps/dashboard/components/TestTree.tsx +1 -2
- package/apps/dashboard/components/TipCard.tsx +2 -4
- package/apps/dashboard/components/Toast.tsx +0 -1
- package/apps/dashboard/components/TypeIcon.tsx +7 -8
- package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
- package/apps/dashboard/components/WaveCompletionAnimation.tsx +5 -17
- package/apps/dashboard/components/WelcomeScreen.tsx +2 -6
- package/apps/dashboard/components/WorkItemHeader.tsx +0 -1
- package/apps/dashboard/components/WorkItemTree.tsx +2 -4
- package/apps/dashboard/components/settings/AccountSection.tsx +27 -13
- package/apps/dashboard/components/settings/AiContextSection.tsx +89 -0
- package/apps/dashboard/components/settings/ContextDocumentsSection.tsx +317 -0
- package/apps/dashboard/components/settings/EnvVarsSection.tsx +20 -73
- package/apps/dashboard/components/settings/GeneralSection.tsx +137 -26
- package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
- package/apps/dashboard/components/settings/SettingsLayout.tsx +0 -1
- package/apps/dashboard/components/ui/Button.tsx +1 -1
- package/apps/dashboard/components/ui/Input.tsx +1 -1
- package/apps/dashboard/components.json +1 -1
- package/apps/dashboard/contexts/ClaudeSessionContext.tsx +611 -358
- package/apps/dashboard/contexts/ConnectionStatusContext.tsx +0 -1
- package/apps/dashboard/contexts/UsageContext.tsx +62 -31
- package/apps/dashboard/dev.sh +35 -0
- package/apps/dashboard/eslint.config.mjs +9 -9
- package/apps/dashboard/hooks/useWebSocket.ts +138 -83
- package/apps/dashboard/index.html +73 -0
- package/apps/dashboard/lib/data-bridge.ts +722 -0
- package/apps/dashboard/lib/db.ts +69 -1302
- package/apps/dashboard/lib/environment-config.ts +173 -0
- package/apps/dashboard/lib/environment-verification.ts +119 -0
- package/apps/dashboard/lib/kanban-utils.ts +226 -26
- package/apps/dashboard/lib/proof-run.ts +495 -0
- package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
- package/apps/dashboard/lib/service-recovery.ts +326 -0
- package/apps/dashboard/lib/session-state-machine.ts +1 -0
- package/apps/dashboard/lib/session-state-utils.ts +0 -164
- package/apps/dashboard/lib/session-stream-manager.ts +253 -122
- package/apps/dashboard/lib/stream-manager-registry.ts +46 -6
- package/apps/dashboard/lib/tauri-bridge.ts +102 -0
- package/apps/dashboard/lib/tauri.ts +106 -0
- package/apps/dashboard/lib/utils.ts +3 -3
- package/apps/dashboard/next-env.d.ts +1 -1
- package/apps/dashboard/package.json +21 -33
- package/apps/dashboard/public/bug-icon.png +0 -0
- package/apps/dashboard/public/buoy-icon.png +0 -0
- package/apps/dashboard/public/in-flight-seagull.png +0 -0
- package/apps/dashboard/public/pier-icon.png +0 -0
- package/apps/dashboard/public/star-icon.png +0 -0
- package/apps/dashboard/public/wrench-icon.png +0 -0
- package/apps/dashboard/scripts/tauri-build.js +228 -0
- package/apps/dashboard/scripts/upload-tauri-to-r2.js +125 -0
- package/apps/dashboard/src/main.tsx +12 -0
- package/apps/dashboard/src/router.tsx +107 -0
- package/apps/dashboard/src/vite-env.d.ts +1 -0
- package/apps/dashboard/tsconfig.json +7 -12
- package/apps/dashboard/tsconfig.tsbuildinfo +1 -1
- package/apps/dashboard/vite.config.ts +33 -0
- package/apps/update-server/src/index.ts +167 -30
- package/claude-hooks/global-guardrails.js +14 -13
- package/crates/jettypod-cli/Cargo.toml +19 -0
- package/crates/jettypod-cli/src/commands.rs +1249 -0
- package/crates/jettypod-cli/src/main.rs +595 -0
- package/crates/jettypod-core/Cargo.toml +26 -0
- package/crates/jettypod-core/build.rs +98 -0
- package/crates/jettypod-core/migrations/V1__baseline.sql +197 -0
- package/crates/jettypod-core/migrations/V2__work_items_indexes.sql +6 -0
- package/crates/jettypod-core/migrations/V3__qa_steps.sql +2 -0
- package/crates/jettypod-core/src/auth.rs +294 -0
- package/crates/jettypod-core/src/config.rs +397 -0
- package/crates/jettypod-core/src/db/mod.rs +507 -0
- package/crates/jettypod-core/src/db/recovery.rs +114 -0
- package/crates/jettypod-core/src/db/startup.rs +101 -0
- package/crates/jettypod-core/src/db/validate.rs +149 -0
- package/crates/jettypod-core/src/error.rs +76 -0
- package/crates/jettypod-core/src/git.rs +458 -0
- package/crates/jettypod-core/src/lib.rs +20 -0
- package/crates/jettypod-core/src/sessions.rs +625 -0
- package/crates/jettypod-core/src/skills.rs +556 -0
- package/crates/jettypod-core/src/work.rs +1086 -0
- package/crates/jettypod-core/src/worktree.rs +628 -0
- package/crates/jettypod-core/src/ws.rs +767 -0
- package/cucumber-test.cjs +6 -0
- package/jettypod.js +96 -4
- package/lib/bdd-preflight.js +96 -0
- package/lib/merge-lock.js +111 -253
- package/lib/migrations/030-rejection-round-columns.js +54 -0
- package/lib/migrations/031-session-isolation-index.js +17 -0
- package/lib/work-commands/index.js +58 -16
- package/lib/work-tracking/index.js +108 -8
- package/package.json +1 -1
- package/skills-templates/bug-mode/SKILL.md +43 -1
- package/skills-templates/chore-mode/SKILL.md +40 -1
- package/skills-templates/design-system-selection/SKILL.md +273 -0
- package/skills-templates/epic-planning/SKILL.md +14 -0
- package/skills-templates/feature-planning/SKILL.md +90 -1
- package/skills-templates/production-mode/SKILL.md +20 -0
- package/skills-templates/simple-improvement/SKILL.md +39 -2
- package/skills-templates/speed-mode/SKILL.md +10 -15
- package/skills-templates/stable-mode/SKILL.md +47 -0
- package/apps/dashboard/README.md +0 -36
- package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -446
- package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
- package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -280
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/content/route.ts +0 -52
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +0 -525
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/pin/route.ts +0 -24
- package/apps/dashboard/app/api/claude/sessions/cleanup/route.ts +0 -34
- package/apps/dashboard/app/api/claude/sessions/route.ts +0 -184
- package/apps/dashboard/app/api/decisions/[id]/route.ts +0 -25
- package/apps/dashboard/app/api/internal/set-project/route.ts +0 -17
- package/apps/dashboard/app/api/kanban/route.ts +0 -15
- package/apps/dashboard/app/api/settings/env-vars/route.ts +0 -125
- package/apps/dashboard/app/api/settings/general/route.ts +0 -21
- package/apps/dashboard/app/api/tests/route.ts +0 -9
- package/apps/dashboard/app/api/tests/run/route.ts +0 -82
- package/apps/dashboard/app/api/tests/run/stream/route.ts +0 -71
- package/apps/dashboard/app/api/tests/undefined/route.ts +0 -9
- package/apps/dashboard/app/api/usage/route.ts +0 -17
- package/apps/dashboard/app/api/work/[id]/description/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/epic/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/order/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/route.ts +0 -35
- package/apps/dashboard/app/api/work/[id]/status/route.ts +0 -63
- package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
- package/apps/dashboard/app/layout.tsx +0 -55
- package/apps/dashboard/components/UpgradeBanner.tsx +0 -30
- package/apps/dashboard/electron/ipc-handlers.js +0 -1026
- package/apps/dashboard/electron/main.js +0 -2306
- package/apps/dashboard/electron/preload.js +0 -125
- package/apps/dashboard/electron/session-manager.js +0 -163
- package/apps/dashboard/electron-builder.config.js +0 -357
- package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
- package/apps/dashboard/lib/backlog-parser.ts +0 -50
- package/apps/dashboard/lib/claude-process-manager.ts +0 -529
- package/apps/dashboard/lib/db-bridge.ts +0 -283
- package/apps/dashboard/lib/prototypes.ts +0 -202
- package/apps/dashboard/lib/test-results-db.ts +0 -307
- package/apps/dashboard/lib/tests.ts +0 -282
- package/apps/dashboard/next.config.js +0 -66
- package/apps/dashboard/postcss.config.mjs +0 -7
- package/apps/dashboard/public/bug-icon.svg +0 -9
- package/apps/dashboard/public/buoy-icon.svg +0 -9
- package/apps/dashboard/public/file.svg +0 -1
- package/apps/dashboard/public/globe.svg +0 -1
- package/apps/dashboard/public/in-flight-seagull.svg +0 -9
- package/apps/dashboard/public/next.svg +0 -1
- package/apps/dashboard/public/pier-icon.svg +0 -14
- package/apps/dashboard/public/star-icon.svg +0 -9
- package/apps/dashboard/public/vercel.svg +0 -1
- package/apps/dashboard/public/window.svg +0 -1
- package/apps/dashboard/public/wrench-icon.svg +0 -9
- package/apps/dashboard/scripts/download-node.js +0 -104
- package/apps/dashboard/scripts/upload-to-r2.js +0 -89
package/apps/dashboard/README.md
DELETED
|
@@ -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
|
-
}
|