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.
- package/.env +4 -3
- 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 +63 -58
- package/apps/dashboard/app/demo/gates/page.tsx +43 -45
- package/apps/dashboard/app/design-system/page.tsx +868 -0
- package/apps/dashboard/app/globals.css +80 -4
- package/apps/dashboard/app/install-claude/page.tsx +4 -6
- package/apps/dashboard/app/login/page.tsx +72 -54
- package/apps/dashboard/app/page.tsx +101 -48
- package/apps/dashboard/app/settings/page.tsx +61 -13
- package/apps/dashboard/app/signup/page.tsx +242 -0
- 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 +13 -16
- package/apps/dashboard/app/work/[id]/page.tsx +117 -118
- package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
- package/apps/dashboard/components/AppShell.tsx +92 -85
- package/apps/dashboard/components/CardMenu.tsx +45 -12
- package/apps/dashboard/components/ClaudePanel.tsx +771 -850
- package/apps/dashboard/components/ClaudePanelInput.tsx +43 -15
- package/apps/dashboard/components/ConnectClaudeScreen.tsx +17 -34
- package/apps/dashboard/components/CopyableId.tsx +3 -4
- package/apps/dashboard/components/DetailReviewActions.tsx +100 -0
- package/apps/dashboard/components/DragContext.tsx +134 -63
- package/apps/dashboard/components/DraggableCard.tsx +3 -5
- package/apps/dashboard/components/DropZone.tsx +6 -7
- package/apps/dashboard/components/EditableDetailDescription.tsx +7 -13
- package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
- package/apps/dashboard/components/EditableTitle.tsx +26 -7
- package/apps/dashboard/components/ElapsedTimer.tsx +66 -0
- package/apps/dashboard/components/EpicGroup.tsx +359 -0
- package/apps/dashboard/components/GateCard.tsx +79 -17
- package/apps/dashboard/components/GateChoiceCard.tsx +15 -18
- package/apps/dashboard/components/InstallClaudeScreen.tsx +15 -32
- package/apps/dashboard/components/JettyLoader.tsx +37 -0
- package/apps/dashboard/components/KanbanBoard.tsx +368 -958
- package/apps/dashboard/components/KanbanCard.tsx +740 -0
- package/apps/dashboard/components/LazyCard.tsx +62 -0
- package/apps/dashboard/components/LazyMarkdown.tsx +11 -0
- package/apps/dashboard/components/MainNav.tsx +38 -73
- package/apps/dashboard/components/MessageBlock.tsx +468 -0
- package/apps/dashboard/components/ModeStartCard.tsx +15 -16
- package/apps/dashboard/components/OnboardingWelcome.tsx +213 -0
- package/apps/dashboard/components/PlaceholderCard.tsx +3 -4
- package/apps/dashboard/components/ProjectSwitcher.tsx +30 -30
- package/apps/dashboard/components/PrototypeTimeline.tsx +72 -51
- package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +406 -388
- package/apps/dashboard/components/RealTimeTestsWrapper.tsx +373 -235
- package/apps/dashboard/components/ReviewFooter.tsx +139 -0
- package/apps/dashboard/components/SessionList.tsx +19 -19
- package/apps/dashboard/components/SubscribeContent.tsx +91 -47
- package/apps/dashboard/components/TestTree.tsx +16 -16
- package/apps/dashboard/components/TipCard.tsx +16 -17
- package/apps/dashboard/components/Toast.tsx +5 -6
- package/apps/dashboard/components/TypeIcon.tsx +55 -0
- package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
- package/apps/dashboard/components/WaveCompletionAnimation.tsx +52 -65
- package/apps/dashboard/components/WelcomeScreen.tsx +19 -35
- package/apps/dashboard/components/WorkItemHeader.tsx +4 -5
- package/apps/dashboard/components/WorkItemTree.tsx +11 -32
- package/apps/dashboard/components/settings/AccountSection.tsx +55 -35
- 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 +74 -152
- package/apps/dashboard/components/settings/GeneralSection.tsx +162 -56
- package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
- package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -5
- package/apps/dashboard/components/ui/Button.tsx +104 -0
- package/apps/dashboard/components/ui/Input.tsx +78 -0
- package/apps/dashboard/components.json +1 -1
- package/apps/dashboard/contexts/ClaudeSessionContext.tsx +711 -418
- package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -5
- package/apps/dashboard/contexts/UsageContext.tsx +87 -32
- package/apps/dashboard/dev.sh +35 -0
- package/apps/dashboard/eslint.config.mjs +9 -9
- package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
- package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
- package/apps/dashboard/hooks/useWebSocket.ts +138 -83
- package/apps/dashboard/index.html +73 -0
- package/apps/dashboard/lib/constants.ts +43 -0
- package/apps/dashboard/lib/data-bridge.ts +722 -0
- package/apps/dashboard/lib/db.ts +69 -1265
- 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 +270 -0
- package/apps/dashboard/lib/proof-run.ts +495 -0
- package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
- package/apps/dashboard/lib/run-migrations.js +27 -2
- 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 +308 -134
- package/apps/dashboard/lib/shadows.ts +7 -0
- 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 +6 -0
- package/apps/dashboard/next-env.d.ts +1 -1
- package/apps/dashboard/package.json +21 -32
- package/apps/dashboard/public/bug-icon.png +0 -0
- package/apps/dashboard/public/buoy-icon.png +0 -0
- package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
- package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
- package/apps/dashboard/public/in-flight-seagull.png +0 -0
- package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
- package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
- package/apps/dashboard/public/jettypod_logo.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/scripts/ws-server.js +191 -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 +228 -80
- 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/cucumber.js +9 -3
- package/docs/COMMAND_REFERENCE.md +34 -0
- package/hooks/post-checkout +32 -75
- package/hooks/post-merge +111 -10
- package/jest.setup.js +1 -0
- package/jettypod.js +145 -116
- package/lib/bdd-preflight.js +96 -0
- package/lib/chore-taxonomy.js +33 -10
- package/lib/database.js +36 -16
- package/lib/db-watcher.js +1 -1
- package/lib/git-hooks/pre-commit +1 -1
- package/lib/jettypod-backup.js +27 -4
- package/lib/merge-lock.js +111 -253
- package/lib/migrations/027-plan-at-creation-column.js +3 -1
- package/lib/migrations/029-remove-autoincrement.js +307 -0
- package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
- package/lib/migrations/030-rejection-round-columns.js +54 -0
- package/lib/migrations/031-session-isolation-index.js +17 -0
- package/lib/migrations/index.js +47 -4
- package/lib/schema.js +10 -5
- package/lib/seed-onboarding.js +1 -1
- package/lib/update-command/index.js +9 -175
- package/lib/work-commands/index.js +144 -19
- package/lib/work-tracking/index.js +148 -27
- package/lib/worktree-diagnostics.js +16 -16
- package/lib/worktree-facade.js +1 -1
- package/lib/worktree-manager.js +8 -8
- package/lib/worktree-reconciler.js +5 -5
- package/package.json +9 -2
- package/scripts/ndjson-to-cucumber-json.js +152 -0
- package/scripts/postinstall.js +25 -0
- package/skills-templates/bug-mode/SKILL.md +79 -20
- package/skills-templates/bug-planning/SKILL.md +25 -29
- package/skills-templates/chore-mode/SKILL.md +171 -69
- package/skills-templates/chore-mode/verification.js +51 -10
- package/skills-templates/chore-planning/SKILL.md +47 -18
- package/skills-templates/design-system-selection/SKILL.md +273 -0
- package/skills-templates/epic-planning/SKILL.md +82 -48
- package/skills-templates/external-transition/SKILL.md +47 -47
- package/skills-templates/feature-planning/SKILL.md +173 -74
- package/skills-templates/production-mode/SKILL.md +69 -49
- package/skills-templates/request-routing/SKILL.md +4 -4
- package/skills-templates/simple-improvement/SKILL.md +74 -29
- package/skills-templates/speed-mode/SKILL.md +217 -141
- package/skills-templates/stable-mode/SKILL.md +148 -89
- package/apps/dashboard/README.md +0 -36
- package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -386
- package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
- package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -167
- 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 -378
- 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]/status/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
- package/apps/dashboard/app/layout.tsx +0 -43
- package/apps/dashboard/components/UpgradeBanner.tsx +0 -29
- package/apps/dashboard/electron/ipc-handlers.js +0 -1028
- package/apps/dashboard/electron/main.js +0 -2124
- package/apps/dashboard/electron/preload.js +0 -123
- package/apps/dashboard/electron/session-manager.js +0 -141
- package/apps/dashboard/electron-builder.config.js +0 -357
- package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
- package/apps/dashboard/lib/claude-process-manager.ts +0 -492
- package/apps/dashboard/lib/db-bridge.ts +0 -282
- 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 -50
- package/apps/dashboard/postcss.config.mjs +0 -7
- package/apps/dashboard/public/file.svg +0 -1
- package/apps/dashboard/public/globe.svg +0 -1
- package/apps/dashboard/public/next.svg +0 -1
- package/apps/dashboard/public/vercel.svg +0 -1
- package/apps/dashboard/public/window.svg +0 -1
- package/apps/dashboard/scripts/download-node.js +0 -104
- package/apps/dashboard/scripts/upload-to-r2.js +0 -89
- 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
|
-
}
|