@siteboon/claude-code-ui 1.22.0 → 1.23.1
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/README.md +63 -214
- package/dist/assets/index-BFyod1Qa.css +32 -0
- package/dist/assets/index-CDeSbU74.js +1204 -0
- package/dist/assets/{vendor-codemirror-BMLq5tLB.js → vendor-codemirror-C8f1vU1x.js} +9 -9
- package/dist/assets/{vendor-react-DIN4KjD2.js → vendor-react-CdSTmIF1.js} +1 -1
- package/dist/index.html +6 -6
- package/package.json +22 -2
- package/server/claude-sdk.js +59 -8
- package/server/database/db.js +68 -0
- package/server/database/init.sql +14 -1
- package/server/index.js +53 -3
- package/server/projects.js +53 -14
- package/server/routes/cli-auth.js +23 -4
- package/server/routes/codex.js +3 -0
- package/server/routes/cursor.js +5 -2
- package/server/routes/gemini.js +2 -0
- package/shared/modelConstants.js +6 -2
- package/dist/assets/index-B6iL1dXV.css +0 -32
- package/dist/assets/index-Br2fwqOq.js +0 -1397
|
@@ -56,4 +56,4 @@ Error generating stack: `+u.message+`
|
|
|
56
56
|
* LICENSE.md file in the root directory of this source tree.
|
|
57
57
|
*
|
|
58
58
|
* @license MIT
|
|
59
|
-
*/const up="6";try{window.__reactRouterVersion=up}catch{}const ip="startTransition",Ha=od[ip];function dp(o){let{basename:f,children:a,future:y,window:g}=o,C=j.useRef();C.current==null&&(C.current=pd({window:g,v5Compat:!0}));let _=C.current,[T,P]=j.useState({action:_.action,location:_.location}),{v7_startTransition:U}=y||{},$=j.useCallback(N=>{U&&Ha?Ha(()=>P(N)):P(N)},[P,U]);return j.useLayoutEffect(()=>_.listen($),[_,$]),j.useEffect(()=>np(y),[y]),j.createElement(lp,{basename:f,children:a,location:T.location,navigationType:T.action,navigator:_,future:y})}var Qa;(function(o){o.UseScrollRestoration="useScrollRestoration",o.UseSubmit="useSubmit",o.UseSubmitFetcher="useSubmitFetcher",o.UseFetcher="useFetcher",o.useViewTransitionState="useViewTransitionState"})(Qa||(Qa={}));var Ka;(function(o){o.UseFetcher="useFetcher",o.UseFetchers="useFetchers",o.UseScrollRestoration="useScrollRestoration"})(Ka||(Ka={}));export{dp as B,
|
|
59
|
+
*/const up="6";try{window.__reactRouterVersion=up}catch{}const ip="startTransition",Ha=od[ip];function dp(o){let{basename:f,children:a,future:y,window:g}=o,C=j.useRef();C.current==null&&(C.current=pd({window:g,v5Compat:!0}));let _=C.current,[T,P]=j.useState({action:_.action,location:_.location}),{v7_startTransition:U}=y||{},$=j.useCallback(N=>{U&&Ha?Ha(()=>P(N)):P(N)},[P,U]);return j.useLayoutEffect(()=>_.listen($),[_,$]),j.useEffect(()=>np(y),[y]),j.createElement(lp,{basename:f,children:a,location:T.location,navigationType:T.action,navigator:_,future:y})}var Qa;(function(o){o.UseScrollRestoration="useScrollRestoration",o.UseSubmit="useSubmit",o.UseSubmitFetcher="useSubmitFetcher",o.UseFetcher="useFetcher",o.useViewTransitionState="useViewTransitionState"})(Qa||(Qa={}));var Ka;(function(o){o.UseFetcher="useFetcher",o.UseFetchers="useFetchers",o.UseScrollRestoration="useScrollRestoration"})(Ka||(Ka={}));export{dp as B,sp as R,j as a,fd as b,id as c,op as d,cp as e,fp as f,Ya as g,rp as h,Xa as r,ap as u};
|
package/dist/index.html
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<title>CloudCLI UI</title>
|
|
9
9
|
|
|
10
10
|
<!-- PWA Manifest -->
|
|
11
|
-
<link rel="manifest" href="/manifest.json" />
|
|
11
|
+
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
|
|
12
12
|
|
|
13
13
|
<!-- iOS Safari PWA Meta Tags -->
|
|
14
14
|
<meta name="mobile-web-app-capable" content="yes" />
|
|
@@ -25,11 +25,11 @@
|
|
|
25
25
|
|
|
26
26
|
<!-- Prevent zoom on iOS -->
|
|
27
27
|
<meta name="format-detection" content="telephone=no" />
|
|
28
|
-
<script type="module" crossorigin src="/assets/index-
|
|
29
|
-
<link rel="modulepreload" crossorigin href="/assets/vendor-react-
|
|
30
|
-
<link rel="modulepreload" crossorigin href="/assets/vendor-codemirror-
|
|
28
|
+
<script type="module" crossorigin src="/assets/index-CDeSbU74.js"></script>
|
|
29
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-react-CdSTmIF1.js">
|
|
30
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-codemirror-C8f1vU1x.js">
|
|
31
31
|
<link rel="modulepreload" crossorigin href="/assets/vendor-xterm-CJZjLICi.js">
|
|
32
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
32
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BFyod1Qa.css">
|
|
33
33
|
</head>
|
|
34
34
|
<body>
|
|
35
35
|
<div id="root"></div>
|
|
@@ -49,4 +49,4 @@
|
|
|
49
49
|
}
|
|
50
50
|
</script>
|
|
51
51
|
</body>
|
|
52
|
-
</html>
|
|
52
|
+
</html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@siteboon/claude-code-ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.23.1",
|
|
4
4
|
"description": "A web-based UI for Claude Code CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server/index.js",
|
|
@@ -30,10 +30,13 @@
|
|
|
30
30
|
"build": "vite build",
|
|
31
31
|
"preview": "vite preview",
|
|
32
32
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
33
|
+
"lint": "eslint src/",
|
|
34
|
+
"lint:fix": "eslint src/ --fix",
|
|
33
35
|
"start": "npm run build && npm run server",
|
|
34
36
|
"release": "./release.sh",
|
|
35
37
|
"prepublishOnly": "npm run build",
|
|
36
|
-
"postinstall": "node scripts/fix-node-pty.js"
|
|
38
|
+
"postinstall": "node scripts/fix-node-pty.js",
|
|
39
|
+
"prepare": "husky"
|
|
37
40
|
},
|
|
38
41
|
"keywords": [
|
|
39
42
|
"claude code",
|
|
@@ -103,6 +106,9 @@
|
|
|
103
106
|
"ws": "^8.14.2"
|
|
104
107
|
},
|
|
105
108
|
"devDependencies": {
|
|
109
|
+
"@commitlint/cli": "^20.4.3",
|
|
110
|
+
"@commitlint/config-conventional": "^20.4.3",
|
|
111
|
+
"@eslint/js": "^9.39.3",
|
|
106
112
|
"@release-it/conventional-changelog": "^10.0.5",
|
|
107
113
|
"@types/node": "^22.19.7",
|
|
108
114
|
"@types/react": "^18.2.43",
|
|
@@ -111,12 +117,26 @@
|
|
|
111
117
|
"auto-changelog": "^2.5.0",
|
|
112
118
|
"autoprefixer": "^10.4.16",
|
|
113
119
|
"concurrently": "^8.2.2",
|
|
120
|
+
"eslint": "^9.39.3",
|
|
121
|
+
"eslint-plugin-import-x": "^4.16.1",
|
|
122
|
+
"eslint-plugin-react": "^7.37.5",
|
|
123
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
124
|
+
"eslint-plugin-react-refresh": "^0.5.2",
|
|
125
|
+
"eslint-plugin-tailwindcss": "^3.18.2",
|
|
126
|
+
"eslint-plugin-unused-imports": "^4.4.1",
|
|
127
|
+
"globals": "^17.4.0",
|
|
128
|
+
"husky": "^9.1.7",
|
|
129
|
+
"lint-staged": "^16.3.2",
|
|
114
130
|
"node-gyp": "^10.0.0",
|
|
115
131
|
"postcss": "^8.4.32",
|
|
116
132
|
"release-it": "^19.0.5",
|
|
117
133
|
"sharp": "^0.34.2",
|
|
118
134
|
"tailwindcss": "^3.4.0",
|
|
119
135
|
"typescript": "^5.9.3",
|
|
136
|
+
"typescript-eslint": "^8.56.1",
|
|
120
137
|
"vite": "^7.0.4"
|
|
138
|
+
},
|
|
139
|
+
"lint-staged": {
|
|
140
|
+
"src/**/*.{ts,tsx,js,jsx}": "eslint"
|
|
121
141
|
}
|
|
122
142
|
}
|
package/server/claude-sdk.js
CHANGED
|
@@ -34,7 +34,7 @@ function createRequestId() {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
function waitForToolApproval(requestId, options = {}) {
|
|
37
|
-
const { timeoutMs = TOOL_APPROVAL_TIMEOUT_MS, signal, onCancel } = options;
|
|
37
|
+
const { timeoutMs = TOOL_APPROVAL_TIMEOUT_MS, signal, onCancel, metadata } = options;
|
|
38
38
|
|
|
39
39
|
return new Promise(resolve => {
|
|
40
40
|
let settled = false;
|
|
@@ -78,9 +78,14 @@ function waitForToolApproval(requestId, options = {}) {
|
|
|
78
78
|
signal.addEventListener('abort', abortHandler, { once: true });
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
const resolver = (decision) => {
|
|
82
82
|
finalize(decision);
|
|
83
|
-
}
|
|
83
|
+
};
|
|
84
|
+
// Attach metadata for getPendingApprovalsForSession lookup
|
|
85
|
+
if (metadata) {
|
|
86
|
+
Object.assign(resolver, metadata);
|
|
87
|
+
}
|
|
88
|
+
pendingToolApprovals.set(requestId, resolver);
|
|
84
89
|
});
|
|
85
90
|
}
|
|
86
91
|
|
|
@@ -209,13 +214,14 @@ function mapCliOptionsToSDK(options = {}) {
|
|
|
209
214
|
* @param {Array<string>} tempImagePaths - Temp image file paths for cleanup
|
|
210
215
|
* @param {string} tempDir - Temp directory for cleanup
|
|
211
216
|
*/
|
|
212
|
-
function addSession(sessionId, queryInstance, tempImagePaths = [], tempDir = null) {
|
|
217
|
+
function addSession(sessionId, queryInstance, tempImagePaths = [], tempDir = null, writer = null) {
|
|
213
218
|
activeSessions.set(sessionId, {
|
|
214
219
|
instance: queryInstance,
|
|
215
220
|
startTime: Date.now(),
|
|
216
221
|
status: 'active',
|
|
217
222
|
tempImagePaths,
|
|
218
|
-
tempDir
|
|
223
|
+
tempDir,
|
|
224
|
+
writer
|
|
219
225
|
});
|
|
220
226
|
}
|
|
221
227
|
|
|
@@ -512,6 +518,12 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
512
518
|
const decision = await waitForToolApproval(requestId, {
|
|
513
519
|
timeoutMs: requiresInteraction ? 0 : undefined,
|
|
514
520
|
signal: context?.signal,
|
|
521
|
+
metadata: {
|
|
522
|
+
_sessionId: capturedSessionId || sessionId || null,
|
|
523
|
+
_toolName: toolName,
|
|
524
|
+
_input: input,
|
|
525
|
+
_receivedAt: new Date(),
|
|
526
|
+
},
|
|
515
527
|
onCancel: (reason) => {
|
|
516
528
|
ws.send({
|
|
517
529
|
type: 'claude-permission-cancelled',
|
|
@@ -562,7 +574,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
562
574
|
|
|
563
575
|
// Track the query instance for abort capability
|
|
564
576
|
if (capturedSessionId) {
|
|
565
|
-
addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir);
|
|
577
|
+
addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir, ws);
|
|
566
578
|
}
|
|
567
579
|
|
|
568
580
|
// Process streaming messages
|
|
@@ -572,7 +584,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
572
584
|
if (message.session_id && !capturedSessionId) {
|
|
573
585
|
|
|
574
586
|
capturedSessionId = message.session_id;
|
|
575
|
-
addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir);
|
|
587
|
+
addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir, ws);
|
|
576
588
|
|
|
577
589
|
// Set session ID on writer
|
|
578
590
|
if (ws.setSessionId && typeof ws.setSessionId === 'function') {
|
|
@@ -712,11 +724,50 @@ function getActiveClaudeSDKSessions() {
|
|
|
712
724
|
return getAllSessions();
|
|
713
725
|
}
|
|
714
726
|
|
|
727
|
+
/**
|
|
728
|
+
* Get pending tool approvals for a specific session.
|
|
729
|
+
* @param {string} sessionId - The session ID
|
|
730
|
+
* @returns {Array} Array of pending permission request objects
|
|
731
|
+
*/
|
|
732
|
+
function getPendingApprovalsForSession(sessionId) {
|
|
733
|
+
const pending = [];
|
|
734
|
+
for (const [requestId, resolver] of pendingToolApprovals.entries()) {
|
|
735
|
+
if (resolver._sessionId === sessionId) {
|
|
736
|
+
pending.push({
|
|
737
|
+
requestId,
|
|
738
|
+
toolName: resolver._toolName || 'UnknownTool',
|
|
739
|
+
input: resolver._input,
|
|
740
|
+
context: resolver._context,
|
|
741
|
+
sessionId,
|
|
742
|
+
receivedAt: resolver._receivedAt || new Date(),
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
return pending;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Reconnect a session's WebSocketWriter to a new raw WebSocket.
|
|
751
|
+
* Called when client reconnects (e.g. page refresh) while SDK is still running.
|
|
752
|
+
* @param {string} sessionId - The session ID
|
|
753
|
+
* @param {Object} newRawWs - The new raw WebSocket connection
|
|
754
|
+
* @returns {boolean} True if writer was successfully reconnected
|
|
755
|
+
*/
|
|
756
|
+
function reconnectSessionWriter(sessionId, newRawWs) {
|
|
757
|
+
const session = getSession(sessionId);
|
|
758
|
+
if (!session?.writer?.updateWebSocket) return false;
|
|
759
|
+
session.writer.updateWebSocket(newRawWs);
|
|
760
|
+
console.log(`[RECONNECT] Writer swapped for session ${sessionId}`);
|
|
761
|
+
return true;
|
|
762
|
+
}
|
|
763
|
+
|
|
715
764
|
// Export public API
|
|
716
765
|
export {
|
|
717
766
|
queryClaudeSDK,
|
|
718
767
|
abortClaudeSDKSession,
|
|
719
768
|
isClaudeSDKSessionActive,
|
|
720
769
|
getActiveClaudeSDKSessions,
|
|
721
|
-
resolveToolApproval
|
|
770
|
+
resolveToolApproval,
|
|
771
|
+
getPendingApprovalsForSession,
|
|
772
|
+
reconnectSessionWriter
|
|
722
773
|
};
|
package/server/database/db.js
CHANGED
|
@@ -91,6 +91,18 @@ const runMigrations = () => {
|
|
|
91
91
|
db.exec('ALTER TABLE users ADD COLUMN has_completed_onboarding BOOLEAN DEFAULT 0');
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
// Create session_names table if it doesn't exist (for existing installations)
|
|
95
|
+
db.exec(`CREATE TABLE IF NOT EXISTS session_names (
|
|
96
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
97
|
+
session_id TEXT NOT NULL,
|
|
98
|
+
provider TEXT NOT NULL DEFAULT 'claude',
|
|
99
|
+
custom_name TEXT NOT NULL,
|
|
100
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
101
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
102
|
+
UNIQUE(session_id, provider)
|
|
103
|
+
)`);
|
|
104
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_session_names_lookup ON session_names(session_id, provider)');
|
|
105
|
+
|
|
94
106
|
console.log('Database migrations completed successfully');
|
|
95
107
|
} catch (error) {
|
|
96
108
|
console.error('Error running migrations:', error.message);
|
|
@@ -348,6 +360,60 @@ const credentialsDb = {
|
|
|
348
360
|
}
|
|
349
361
|
};
|
|
350
362
|
|
|
363
|
+
// Session custom names database operations
|
|
364
|
+
const sessionNamesDb = {
|
|
365
|
+
// Set (insert or update) a custom session name
|
|
366
|
+
setName: (sessionId, provider, customName) => {
|
|
367
|
+
db.prepare(`
|
|
368
|
+
INSERT INTO session_names (session_id, provider, custom_name)
|
|
369
|
+
VALUES (?, ?, ?)
|
|
370
|
+
ON CONFLICT(session_id, provider)
|
|
371
|
+
DO UPDATE SET custom_name = excluded.custom_name, updated_at = CURRENT_TIMESTAMP
|
|
372
|
+
`).run(sessionId, provider, customName);
|
|
373
|
+
},
|
|
374
|
+
|
|
375
|
+
// Get a single custom session name
|
|
376
|
+
getName: (sessionId, provider) => {
|
|
377
|
+
const row = db.prepare(
|
|
378
|
+
'SELECT custom_name FROM session_names WHERE session_id = ? AND provider = ?'
|
|
379
|
+
).get(sessionId, provider);
|
|
380
|
+
return row?.custom_name || null;
|
|
381
|
+
},
|
|
382
|
+
|
|
383
|
+
// Batch lookup — returns Map<sessionId, customName>
|
|
384
|
+
getNames: (sessionIds, provider) => {
|
|
385
|
+
if (!sessionIds.length) return new Map();
|
|
386
|
+
const placeholders = sessionIds.map(() => '?').join(',');
|
|
387
|
+
const rows = db.prepare(
|
|
388
|
+
`SELECT session_id, custom_name FROM session_names
|
|
389
|
+
WHERE session_id IN (${placeholders}) AND provider = ?`
|
|
390
|
+
).all(...sessionIds, provider);
|
|
391
|
+
return new Map(rows.map(r => [r.session_id, r.custom_name]));
|
|
392
|
+
},
|
|
393
|
+
|
|
394
|
+
// Delete a custom session name
|
|
395
|
+
deleteName: (sessionId, provider) => {
|
|
396
|
+
return db.prepare(
|
|
397
|
+
'DELETE FROM session_names WHERE session_id = ? AND provider = ?'
|
|
398
|
+
).run(sessionId, provider).changes > 0;
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
// Apply custom session names from the database (overrides CLI-generated summaries)
|
|
403
|
+
function applyCustomSessionNames(sessions, provider) {
|
|
404
|
+
if (!sessions?.length) return;
|
|
405
|
+
try {
|
|
406
|
+
const ids = sessions.map(s => s.id);
|
|
407
|
+
const customNames = sessionNamesDb.getNames(ids, provider);
|
|
408
|
+
for (const session of sessions) {
|
|
409
|
+
const custom = customNames.get(session.id);
|
|
410
|
+
if (custom) session.summary = custom;
|
|
411
|
+
}
|
|
412
|
+
} catch (error) {
|
|
413
|
+
console.warn(`[DB] Failed to apply custom session names for ${provider}:`, error.message);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
351
417
|
// Backward compatibility - keep old names pointing to new system
|
|
352
418
|
const githubTokensDb = {
|
|
353
419
|
createGithubToken: (userId, tokenName, githubToken, description = null) => {
|
|
@@ -373,5 +439,7 @@ export {
|
|
|
373
439
|
userDb,
|
|
374
440
|
apiKeysDb,
|
|
375
441
|
credentialsDb,
|
|
442
|
+
sessionNamesDb,
|
|
443
|
+
applyCustomSessionNames,
|
|
376
444
|
githubTokensDb // Backward compatibility
|
|
377
445
|
};
|
package/server/database/init.sql
CHANGED
|
@@ -49,4 +49,17 @@ CREATE TABLE IF NOT EXISTS user_credentials (
|
|
|
49
49
|
|
|
50
50
|
CREATE INDEX IF NOT EXISTS idx_user_credentials_user_id ON user_credentials(user_id);
|
|
51
51
|
CREATE INDEX IF NOT EXISTS idx_user_credentials_type ON user_credentials(credential_type);
|
|
52
|
-
CREATE INDEX IF NOT EXISTS idx_user_credentials_active ON user_credentials(is_active);
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_user_credentials_active ON user_credentials(is_active);
|
|
53
|
+
|
|
54
|
+
-- Session custom names (provider-agnostic display name overrides)
|
|
55
|
+
CREATE TABLE IF NOT EXISTS session_names (
|
|
56
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
57
|
+
session_id TEXT NOT NULL,
|
|
58
|
+
provider TEXT NOT NULL DEFAULT 'claude',
|
|
59
|
+
custom_name TEXT NOT NULL,
|
|
60
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
61
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
62
|
+
UNIQUE(session_id, provider)
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
CREATE INDEX IF NOT EXISTS idx_session_names_lookup ON session_names(session_id, provider);
|
package/server/index.js
CHANGED
|
@@ -45,7 +45,7 @@ import fetch from 'node-fetch';
|
|
|
45
45
|
import mime from 'mime-types';
|
|
46
46
|
|
|
47
47
|
import { getProjects, getSessions, getSessionMessages, renameProject, deleteSession, deleteProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache } from './projects.js';
|
|
48
|
-
import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getActiveClaudeSDKSessions, resolveToolApproval } from './claude-sdk.js';
|
|
48
|
+
import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getActiveClaudeSDKSessions, resolveToolApproval, getPendingApprovalsForSession, reconnectSessionWriter } from './claude-sdk.js';
|
|
49
49
|
import { spawnCursor, abortCursorSession, isCursorSessionActive, getActiveCursorSessions } from './cursor-cli.js';
|
|
50
50
|
import { queryCodex, abortCodexSession, isCodexSessionActive, getActiveCodexSessions } from './openai-codex.js';
|
|
51
51
|
import { spawnGemini, abortGeminiSession, isGeminiSessionActive, getActiveGeminiSessions } from './gemini-cli.js';
|
|
@@ -64,10 +64,12 @@ import cliAuthRoutes from './routes/cli-auth.js';
|
|
|
64
64
|
import userRoutes from './routes/user.js';
|
|
65
65
|
import codexRoutes from './routes/codex.js';
|
|
66
66
|
import geminiRoutes from './routes/gemini.js';
|
|
67
|
-
import { initializeDatabase } from './database/db.js';
|
|
67
|
+
import { initializeDatabase, sessionNamesDb, applyCustomSessionNames } from './database/db.js';
|
|
68
68
|
import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
|
|
69
69
|
import { IS_PLATFORM } from './constants/config.js';
|
|
70
70
|
|
|
71
|
+
const VALID_PROVIDERS = ['claude', 'codex', 'cursor', 'gemini'];
|
|
72
|
+
|
|
71
73
|
// File system watchers for provider project/session folders
|
|
72
74
|
const PROVIDER_WATCH_PATHS = [
|
|
73
75
|
{ provider: 'claude', rootPath: path.join(os.homedir(), '.claude', 'projects') },
|
|
@@ -493,6 +495,7 @@ app.get('/api/projects/:projectName/sessions', authenticateToken, async (req, re
|
|
|
493
495
|
try {
|
|
494
496
|
const { limit = 5, offset = 0 } = req.query;
|
|
495
497
|
const result = await getSessions(req.params.projectName, parseInt(limit), parseInt(offset));
|
|
498
|
+
applyCustomSessionNames(result.sessions, 'claude');
|
|
496
499
|
res.json(result);
|
|
497
500
|
} catch (error) {
|
|
498
501
|
res.status(500).json({ error: error.message });
|
|
@@ -541,6 +544,7 @@ app.delete('/api/projects/:projectName/sessions/:sessionId', authenticateToken,
|
|
|
541
544
|
const { projectName, sessionId } = req.params;
|
|
542
545
|
console.log(`[API] Deleting session: ${sessionId} from project: ${projectName}`);
|
|
543
546
|
await deleteSession(projectName, sessionId);
|
|
547
|
+
sessionNamesDb.deleteName(sessionId, 'claude');
|
|
544
548
|
console.log(`[API] Session ${sessionId} deleted successfully`);
|
|
545
549
|
res.json({ success: true });
|
|
546
550
|
} catch (error) {
|
|
@@ -549,6 +553,32 @@ app.delete('/api/projects/:projectName/sessions/:sessionId', authenticateToken,
|
|
|
549
553
|
}
|
|
550
554
|
});
|
|
551
555
|
|
|
556
|
+
// Rename session endpoint
|
|
557
|
+
app.put('/api/sessions/:sessionId/rename', authenticateToken, async (req, res) => {
|
|
558
|
+
try {
|
|
559
|
+
const { sessionId } = req.params;
|
|
560
|
+
const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9._-]/g, '');
|
|
561
|
+
if (!safeSessionId || safeSessionId !== String(sessionId)) {
|
|
562
|
+
return res.status(400).json({ error: 'Invalid sessionId' });
|
|
563
|
+
}
|
|
564
|
+
const { summary, provider } = req.body;
|
|
565
|
+
if (!summary || typeof summary !== 'string' || summary.trim() === '') {
|
|
566
|
+
return res.status(400).json({ error: 'Summary is required' });
|
|
567
|
+
}
|
|
568
|
+
if (summary.trim().length > 500) {
|
|
569
|
+
return res.status(400).json({ error: 'Summary must not exceed 500 characters' });
|
|
570
|
+
}
|
|
571
|
+
if (!provider || !VALID_PROVIDERS.includes(provider)) {
|
|
572
|
+
return res.status(400).json({ error: `Provider must be one of: ${VALID_PROVIDERS.join(', ')}` });
|
|
573
|
+
}
|
|
574
|
+
sessionNamesDb.setName(safeSessionId, provider, summary.trim());
|
|
575
|
+
res.json({ success: true });
|
|
576
|
+
} catch (error) {
|
|
577
|
+
console.error(`[API] Error renaming session ${req.params.sessionId}:`, error);
|
|
578
|
+
res.status(500).json({ error: error.message });
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
|
|
552
582
|
// Delete project endpoint (force=true to delete with sessions)
|
|
553
583
|
app.delete('/api/projects/:projectName', authenticateToken, async (req, res) => {
|
|
554
584
|
try {
|
|
@@ -1350,6 +1380,10 @@ class WebSocketWriter {
|
|
|
1350
1380
|
}
|
|
1351
1381
|
}
|
|
1352
1382
|
|
|
1383
|
+
updateWebSocket(newRawWs) {
|
|
1384
|
+
this.ws = newRawWs;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1353
1387
|
setSessionId(sessionId) {
|
|
1354
1388
|
this.sessionId = sessionId;
|
|
1355
1389
|
}
|
|
@@ -1464,6 +1498,11 @@ function handleChatConnection(ws) {
|
|
|
1464
1498
|
} else {
|
|
1465
1499
|
// Use Claude Agents SDK
|
|
1466
1500
|
isActive = isClaudeSDKSessionActive(sessionId);
|
|
1501
|
+
if (isActive) {
|
|
1502
|
+
// Reconnect the session's writer to the new WebSocket so
|
|
1503
|
+
// subsequent SDK output flows to the refreshed client.
|
|
1504
|
+
reconnectSessionWriter(sessionId, ws);
|
|
1505
|
+
}
|
|
1467
1506
|
}
|
|
1468
1507
|
|
|
1469
1508
|
writer.send({
|
|
@@ -1472,6 +1511,17 @@ function handleChatConnection(ws) {
|
|
|
1472
1511
|
provider,
|
|
1473
1512
|
isProcessing: isActive
|
|
1474
1513
|
});
|
|
1514
|
+
} else if (data.type === 'get-pending-permissions') {
|
|
1515
|
+
// Return pending permission requests for a session
|
|
1516
|
+
const sessionId = data.sessionId;
|
|
1517
|
+
if (sessionId && isClaudeSDKSessionActive(sessionId)) {
|
|
1518
|
+
const pending = getPendingApprovalsForSession(sessionId);
|
|
1519
|
+
writer.send({
|
|
1520
|
+
type: 'pending-permissions-response',
|
|
1521
|
+
sessionId,
|
|
1522
|
+
data: pending
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1475
1525
|
} else if (data.type === 'get-active-sessions') {
|
|
1476
1526
|
// Get all currently active sessions
|
|
1477
1527
|
const activeSessions = {
|
|
@@ -2112,7 +2162,7 @@ app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authentica
|
|
|
2112
2162
|
|
|
2113
2163
|
// Allow only safe characters in sessionId
|
|
2114
2164
|
const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9._-]/g, '');
|
|
2115
|
-
if (!safeSessionId) {
|
|
2165
|
+
if (!safeSessionId || safeSessionId !== String(sessionId)) {
|
|
2116
2166
|
return res.status(400).json({ error: 'Invalid sessionId' });
|
|
2117
2167
|
}
|
|
2118
2168
|
|
package/server/projects.js
CHANGED
|
@@ -66,6 +66,7 @@ import sqlite3 from 'sqlite3';
|
|
|
66
66
|
import { open } from 'sqlite';
|
|
67
67
|
import os from 'os';
|
|
68
68
|
import sessionManager from './sessionManager.js';
|
|
69
|
+
import { applyCustomSessionNames } from './database/db.js';
|
|
69
70
|
|
|
70
71
|
// Import TaskMaster detection functions
|
|
71
72
|
async function detectTaskMasterFolder(projectPath) {
|
|
@@ -458,6 +459,7 @@ async function getProjects(progressCallback = null) {
|
|
|
458
459
|
total: 0
|
|
459
460
|
};
|
|
460
461
|
}
|
|
462
|
+
applyCustomSessionNames(project.sessions, 'claude');
|
|
461
463
|
|
|
462
464
|
// Also fetch Cursor sessions for this project
|
|
463
465
|
try {
|
|
@@ -466,6 +468,7 @@ async function getProjects(progressCallback = null) {
|
|
|
466
468
|
console.warn(`Could not load Cursor sessions for project ${entry.name}:`, e.message);
|
|
467
469
|
project.cursorSessions = [];
|
|
468
470
|
}
|
|
471
|
+
applyCustomSessionNames(project.cursorSessions, 'cursor');
|
|
469
472
|
|
|
470
473
|
// Also fetch Codex sessions for this project
|
|
471
474
|
try {
|
|
@@ -476,6 +479,7 @@ async function getProjects(progressCallback = null) {
|
|
|
476
479
|
console.warn(`Could not load Codex sessions for project ${entry.name}:`, e.message);
|
|
477
480
|
project.codexSessions = [];
|
|
478
481
|
}
|
|
482
|
+
applyCustomSessionNames(project.codexSessions, 'codex');
|
|
479
483
|
|
|
480
484
|
// Also fetch Gemini sessions for this project
|
|
481
485
|
try {
|
|
@@ -484,6 +488,7 @@ async function getProjects(progressCallback = null) {
|
|
|
484
488
|
console.warn(`Could not load Gemini sessions for project ${entry.name}:`, e.message);
|
|
485
489
|
project.geminiSessions = [];
|
|
486
490
|
}
|
|
491
|
+
applyCustomSessionNames(project.geminiSessions, 'gemini');
|
|
487
492
|
|
|
488
493
|
// Add TaskMaster detection
|
|
489
494
|
try {
|
|
@@ -567,6 +572,7 @@ async function getProjects(progressCallback = null) {
|
|
|
567
572
|
} catch (e) {
|
|
568
573
|
console.warn(`Could not load Cursor sessions for manual project ${projectName}:`, e.message);
|
|
569
574
|
}
|
|
575
|
+
applyCustomSessionNames(project.cursorSessions, 'cursor');
|
|
570
576
|
|
|
571
577
|
// Try to fetch Codex sessions for manual projects too
|
|
572
578
|
try {
|
|
@@ -576,6 +582,7 @@ async function getProjects(progressCallback = null) {
|
|
|
576
582
|
} catch (e) {
|
|
577
583
|
console.warn(`Could not load Codex sessions for manual project ${projectName}:`, e.message);
|
|
578
584
|
}
|
|
585
|
+
applyCustomSessionNames(project.codexSessions, 'codex');
|
|
579
586
|
|
|
580
587
|
// Try to fetch Gemini sessions for manual projects too
|
|
581
588
|
try {
|
|
@@ -583,6 +590,7 @@ async function getProjects(progressCallback = null) {
|
|
|
583
590
|
} catch (e) {
|
|
584
591
|
console.warn(`Could not load Gemini sessions for manual project ${projectName}:`, e.message);
|
|
585
592
|
}
|
|
593
|
+
applyCustomSessionNames(project.geminiSessions, 'gemini');
|
|
586
594
|
|
|
587
595
|
// Add TaskMaster detection for manual projects
|
|
588
596
|
try {
|
|
@@ -1071,10 +1079,13 @@ async function renameProject(projectName, newDisplayName) {
|
|
|
1071
1079
|
|
|
1072
1080
|
if (!newDisplayName || newDisplayName.trim() === '') {
|
|
1073
1081
|
// Remove custom name if empty, will fall back to auto-generated
|
|
1074
|
-
|
|
1082
|
+
if (config[projectName]) {
|
|
1083
|
+
delete config[projectName].displayName;
|
|
1084
|
+
}
|
|
1075
1085
|
} else {
|
|
1076
|
-
// Set custom display name
|
|
1086
|
+
// Set custom display name, preserving other properties (manuallyAdded, originalPath)
|
|
1077
1087
|
config[projectName] = {
|
|
1088
|
+
...config[projectName],
|
|
1078
1089
|
displayName: newDisplayName.trim()
|
|
1079
1090
|
};
|
|
1080
1091
|
}
|
|
@@ -1479,6 +1490,23 @@ async function getCodexSessions(projectPath, options = {}) {
|
|
|
1479
1490
|
}
|
|
1480
1491
|
}
|
|
1481
1492
|
|
|
1493
|
+
function isVisibleCodexUserMessage(payload) {
|
|
1494
|
+
if (!payload || payload.type !== 'user_message') {
|
|
1495
|
+
return false;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
// Codex logs internal context (environment, instructions) as non-plain user_message kinds.
|
|
1499
|
+
if (payload.kind && payload.kind !== 'plain') {
|
|
1500
|
+
return false;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
if (typeof payload.message !== 'string' || payload.message.trim().length === 0) {
|
|
1504
|
+
return false;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
return true;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1482
1510
|
// Parse a Codex session JSONL file to extract metadata
|
|
1483
1511
|
async function parseCodexSessionFile(filePath) {
|
|
1484
1512
|
try {
|
|
@@ -1514,8 +1542,8 @@ async function parseCodexSessionFile(filePath) {
|
|
|
1514
1542
|
};
|
|
1515
1543
|
}
|
|
1516
1544
|
|
|
1517
|
-
// Count messages and extract
|
|
1518
|
-
if (entry.type === 'event_msg' && entry.payload
|
|
1545
|
+
// Count visible user messages and extract summary from the latest plain user input.
|
|
1546
|
+
if (entry.type === 'event_msg' && isVisibleCodexUserMessage(entry.payload)) {
|
|
1519
1547
|
messageCount++;
|
|
1520
1548
|
if (entry.payload.message) {
|
|
1521
1549
|
lastUserMessage = entry.payload.message;
|
|
@@ -1622,25 +1650,36 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
|
|
|
1622
1650
|
};
|
|
1623
1651
|
}
|
|
1624
1652
|
}
|
|
1653
|
+
|
|
1654
|
+
// Use event_msg.user_message for user-visible inputs.
|
|
1655
|
+
if (entry.type === 'event_msg' && isVisibleCodexUserMessage(entry.payload)) {
|
|
1656
|
+
messages.push({
|
|
1657
|
+
type: 'user',
|
|
1658
|
+
timestamp: entry.timestamp,
|
|
1659
|
+
message: {
|
|
1660
|
+
role: 'user',
|
|
1661
|
+
content: entry.payload.message
|
|
1662
|
+
}
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1625
1665
|
|
|
1626
|
-
//
|
|
1627
|
-
|
|
1666
|
+
// response_item.message may include internal prompts for non-assistant roles.
|
|
1667
|
+
// Keep only assistant output from response_item.
|
|
1668
|
+
if (
|
|
1669
|
+
entry.type === 'response_item' &&
|
|
1670
|
+
entry.payload?.type === 'message' &&
|
|
1671
|
+
entry.payload.role === 'assistant'
|
|
1672
|
+
) {
|
|
1628
1673
|
const content = entry.payload.content;
|
|
1629
|
-
const role = entry.payload.role || 'assistant';
|
|
1630
1674
|
const textContent = extractText(content);
|
|
1631
1675
|
|
|
1632
|
-
// Skip system context messages (environment_context)
|
|
1633
|
-
if (textContent?.includes('<environment_context>')) {
|
|
1634
|
-
continue;
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
1676
|
// Only add if there's actual content
|
|
1638
1677
|
if (textContent?.trim()) {
|
|
1639
1678
|
messages.push({
|
|
1640
|
-
type:
|
|
1679
|
+
type: 'assistant',
|
|
1641
1680
|
timestamp: entry.timestamp,
|
|
1642
1681
|
message: {
|
|
1643
|
-
role:
|
|
1682
|
+
role: 'assistant',
|
|
1644
1683
|
content: textContent
|
|
1645
1684
|
}
|
|
1646
1685
|
});
|