@siteboon/claude-code-ui 1.16.4 → 1.18.0
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.ko.md +369 -0
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/dist/assets/index-BiDFBFLP.css +32 -0
- package/dist/assets/index-CF54Qj8d.js +1405 -0
- package/dist/assets/{vendor-codemirror-CJLzwpLB.js → vendor-codemirror-l-lAmaJ1.js} +21 -19
- package/dist/assets/{vendor-react-DcyRfQm3.js → vendor-react-DIN4KjD2.js} +1 -1
- package/dist/index.html +4 -4
- package/package.json +3 -2
- package/server/claude-sdk.js +48 -60
- package/server/database/auth.db +0 -0
- package/server/index.js +107 -78
- package/server/openai-codex.js +25 -12
- package/server/projects.js +117 -63
- package/server/routes/commands.js +80 -0
- package/server/routes/git.js +57 -17
- package/dist/assets/index-Cep8Annb.js +0 -1239
- package/dist/assets/index-DQad8ylc.css +0 -32
|
@@ -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,id as R,j as a,fd as b,sp as c,op as d,
|
|
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,id as R,j as a,fd as b,sp 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
|
@@ -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-CF54Qj8d.js"></script>
|
|
29
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-react-DIN4KjD2.js">
|
|
30
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-codemirror-l-lAmaJ1.js">
|
|
31
31
|
<link rel="modulepreload" crossorigin href="/assets/vendor-xterm-DfaPXD3y.js">
|
|
32
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
32
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BiDFBFLP.css">
|
|
33
33
|
</head>
|
|
34
34
|
<body>
|
|
35
35
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@siteboon/claude-code-ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.0",
|
|
4
4
|
"description": "A web-based UI for Claude Code CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server/index.js",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"author": "Claude Code UI Contributors",
|
|
43
43
|
"license": "GPL-3.0",
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@anthropic-ai/claude-agent-sdk": "^0.1.
|
|
45
|
+
"@anthropic-ai/claude-agent-sdk": "^0.1.71",
|
|
46
46
|
"@codemirror/lang-css": "^6.3.1",
|
|
47
47
|
"@codemirror/lang-html": "^6.4.9",
|
|
48
48
|
"@codemirror/lang-javascript": "^6.2.4",
|
|
@@ -89,6 +89,7 @@
|
|
|
89
89
|
"react-router-dom": "^6.8.1",
|
|
90
90
|
"react-syntax-highlighter": "^15.6.1",
|
|
91
91
|
"rehype-katex": "^7.0.1",
|
|
92
|
+
"rehype-raw": "^7.0.0",
|
|
92
93
|
"remark-gfm": "^4.0.0",
|
|
93
94
|
"remark-math": "^6.0.0",
|
|
94
95
|
"sqlite": "^5.1.1",
|
package/server/claude-sdk.js
CHANGED
|
@@ -13,41 +13,26 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
16
|
-
// Used to mint unique approval request IDs when randomUUID is not available.
|
|
17
|
-
// This keeps parallel tool approvals from colliding; it does not add any crypto/security guarantees.
|
|
18
16
|
import crypto from 'crypto';
|
|
19
17
|
import { promises as fs } from 'fs';
|
|
20
18
|
import path from 'path';
|
|
21
19
|
import os from 'os';
|
|
22
20
|
import { CLAUDE_MODELS } from '../shared/modelConstants.js';
|
|
23
21
|
|
|
24
|
-
// Session tracking: Map of session IDs to active query instances
|
|
25
22
|
const activeSessions = new Map();
|
|
26
|
-
// In-memory registry of pending tool approvals keyed by requestId.
|
|
27
|
-
// This does not persist approvals or share across processes; it exists so the
|
|
28
|
-
// SDK can pause tool execution while the UI decides what to do.
|
|
29
23
|
const pendingToolApprovals = new Map();
|
|
30
24
|
|
|
31
|
-
// Default approval timeout kept under the SDK's 60s control timeout.
|
|
32
|
-
// This does not change SDK limits; it only defines how long we wait for the UI,
|
|
33
|
-
// introduced to avoid hanging the run when no decision arrives.
|
|
34
25
|
const TOOL_APPROVAL_TIMEOUT_MS = parseInt(process.env.CLAUDE_TOOL_APPROVAL_TIMEOUT_MS, 10) || 55000;
|
|
35
26
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
// can respond to the correct pending request without collisions.
|
|
27
|
+
const TOOLS_REQUIRING_INTERACTION = new Set(['AskUserQuestion']);
|
|
28
|
+
|
|
39
29
|
function createRequestId() {
|
|
40
|
-
// if clause is used because randomUUID is not available in older Node.js versions
|
|
41
30
|
if (typeof crypto.randomUUID === 'function') {
|
|
42
31
|
return crypto.randomUUID();
|
|
43
32
|
}
|
|
44
33
|
return crypto.randomBytes(16).toString('hex');
|
|
45
34
|
}
|
|
46
35
|
|
|
47
|
-
// Wait for a UI approval decision, honoring SDK cancellation.
|
|
48
|
-
// This does not auto-approve or auto-deny; it only resolves with UI input,
|
|
49
|
-
// and it cleans up the pending map to avoid leaks, introduced to prevent
|
|
50
|
-
// replying after the SDK cancels the control request.
|
|
51
36
|
function waitForToolApproval(requestId, options = {}) {
|
|
52
37
|
const { timeoutMs = TOOL_APPROVAL_TIMEOUT_MS, signal, onCancel } = options;
|
|
53
38
|
|
|
@@ -61,24 +46,25 @@ function waitForToolApproval(requestId, options = {}) {
|
|
|
61
46
|
resolve(decision);
|
|
62
47
|
};
|
|
63
48
|
|
|
49
|
+
let timeout;
|
|
50
|
+
|
|
64
51
|
const cleanup = () => {
|
|
65
52
|
pendingToolApprovals.delete(requestId);
|
|
66
|
-
clearTimeout(timeout);
|
|
53
|
+
if (timeout) clearTimeout(timeout);
|
|
67
54
|
if (signal && abortHandler) {
|
|
68
55
|
signal.removeEventListener('abort', abortHandler);
|
|
69
56
|
}
|
|
70
57
|
};
|
|
71
58
|
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
59
|
+
// timeoutMs 0 = wait indefinitely (interactive tools)
|
|
60
|
+
if (timeoutMs > 0) {
|
|
61
|
+
timeout = setTimeout(() => {
|
|
62
|
+
onCancel?.('timeout');
|
|
63
|
+
finalize(null);
|
|
64
|
+
}, timeoutMs);
|
|
65
|
+
}
|
|
78
66
|
|
|
79
67
|
const abortHandler = () => {
|
|
80
|
-
// If the SDK cancels the control request, stop waiting to avoid
|
|
81
|
-
// replying after the process is no longer ready for writes.
|
|
82
68
|
onCancel?.('cancelled');
|
|
83
69
|
finalize({ cancelled: true });
|
|
84
70
|
};
|
|
@@ -98,9 +84,6 @@ function waitForToolApproval(requestId, options = {}) {
|
|
|
98
84
|
});
|
|
99
85
|
}
|
|
100
86
|
|
|
101
|
-
// Resolve a pending approval. This does not validate the decision payload;
|
|
102
|
-
// validation and tool matching remain in canUseTool, which keeps this as a
|
|
103
|
-
// lightweight WebSocket -> SDK relay.
|
|
104
87
|
function resolveToolApproval(requestId, decision) {
|
|
105
88
|
const resolver = pendingToolApprovals.get(requestId);
|
|
106
89
|
if (resolver) {
|
|
@@ -175,9 +158,6 @@ function mapCliOptionsToSDK(options = {}) {
|
|
|
175
158
|
sdkOptions.permissionMode = 'bypassPermissions';
|
|
176
159
|
}
|
|
177
160
|
|
|
178
|
-
// Map allowed tools (always set to avoid implicit "allow all" defaults).
|
|
179
|
-
// This does not grant permissions by itself; it just configures the SDK,
|
|
180
|
-
// introduced because leaving it undefined made the SDK treat it as "all tools allowed."
|
|
181
161
|
let allowedTools = [...(settings.allowedTools || [])];
|
|
182
162
|
|
|
183
163
|
// Add plan mode default tools
|
|
@@ -192,8 +172,11 @@ function mapCliOptionsToSDK(options = {}) {
|
|
|
192
172
|
|
|
193
173
|
sdkOptions.allowedTools = allowedTools;
|
|
194
174
|
|
|
195
|
-
//
|
|
196
|
-
// This
|
|
175
|
+
// Use the tools preset to make all default built-in tools available (including AskUserQuestion).
|
|
176
|
+
// This was introduced in SDK 0.1.57. Omitting this preserves existing behavior (all tools available),
|
|
177
|
+
// but being explicit ensures forward compatibility and clarity.
|
|
178
|
+
sdkOptions.tools = { type: 'preset', preset: 'claude_code' };
|
|
179
|
+
|
|
197
180
|
sdkOptions.disallowedTools = settings.disallowedTools || [];
|
|
198
181
|
|
|
199
182
|
// Map model (default to sonnet)
|
|
@@ -267,9 +250,7 @@ function getAllSessions() {
|
|
|
267
250
|
* @returns {Object} Transformed message ready for WebSocket
|
|
268
251
|
*/
|
|
269
252
|
function transformMessage(sdkMessage) {
|
|
270
|
-
// SDK messages
|
|
271
|
-
// The CLI sends them wrapped in {type: 'claude-response', data: message}
|
|
272
|
-
// We'll do the same here to maintain compatibility
|
|
253
|
+
// Pass-through; SDK messages match frontend format.
|
|
273
254
|
return sdkMessage;
|
|
274
255
|
}
|
|
275
256
|
|
|
@@ -490,27 +471,27 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
490
471
|
tempImagePaths = imageResult.tempImagePaths;
|
|
491
472
|
tempDir = imageResult.tempDir;
|
|
492
473
|
|
|
493
|
-
// Gate tool usage with explicit UI approval when not auto-approved.
|
|
494
|
-
// This does not render UI or persist permissions; it only bridges to the UI
|
|
495
|
-
// via WebSocket and waits for the response, introduced so tool calls pause
|
|
496
|
-
// instead of auto-running when the allowlist is empty.
|
|
497
474
|
sdkOptions.canUseTool = async (toolName, input, context) => {
|
|
498
|
-
|
|
499
|
-
return { behavior: 'allow', updatedInput: input };
|
|
500
|
-
}
|
|
475
|
+
const requiresInteraction = TOOLS_REQUIRING_INTERACTION.has(toolName);
|
|
501
476
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
return { behavior: 'deny', message: 'Tool disallowed by settings' };
|
|
507
|
-
}
|
|
477
|
+
if (!requiresInteraction) {
|
|
478
|
+
if (sdkOptions.permissionMode === 'bypassPermissions') {
|
|
479
|
+
return { behavior: 'allow', updatedInput: input };
|
|
480
|
+
}
|
|
508
481
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
482
|
+
const isDisallowed = (sdkOptions.disallowedTools || []).some(entry =>
|
|
483
|
+
matchesToolPermission(entry, toolName, input)
|
|
484
|
+
);
|
|
485
|
+
if (isDisallowed) {
|
|
486
|
+
return { behavior: 'deny', message: 'Tool disallowed by settings' };
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const isAllowed = (sdkOptions.allowedTools || []).some(entry =>
|
|
490
|
+
matchesToolPermission(entry, toolName, input)
|
|
491
|
+
);
|
|
492
|
+
if (isAllowed) {
|
|
493
|
+
return { behavior: 'allow', updatedInput: input };
|
|
494
|
+
}
|
|
514
495
|
}
|
|
515
496
|
|
|
516
497
|
const requestId = createRequestId();
|
|
@@ -522,9 +503,8 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
522
503
|
sessionId: capturedSessionId || sessionId || null
|
|
523
504
|
});
|
|
524
505
|
|
|
525
|
-
// Wait for the UI; if the SDK cancels, notify the UI so it can dismiss the banner.
|
|
526
|
-
// This does not retry or resurface the prompt; it just reflects the cancellation.
|
|
527
506
|
const decision = await waitForToolApproval(requestId, {
|
|
507
|
+
timeoutMs: requiresInteraction ? 0 : undefined,
|
|
528
508
|
signal: context?.signal,
|
|
529
509
|
onCancel: (reason) => {
|
|
530
510
|
ws.send({
|
|
@@ -544,8 +524,6 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
544
524
|
}
|
|
545
525
|
|
|
546
526
|
if (decision.allow) {
|
|
547
|
-
// rememberEntry only updates this run's in-memory allowlist to prevent
|
|
548
|
-
// repeated prompts in the same session; persistence is handled by the UI.
|
|
549
527
|
if (decision.rememberEntry && typeof decision.rememberEntry === 'string') {
|
|
550
528
|
if (!sdkOptions.allowedTools.includes(decision.rememberEntry)) {
|
|
551
529
|
sdkOptions.allowedTools.push(decision.rememberEntry);
|
|
@@ -560,12 +538,22 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
560
538
|
return { behavior: 'deny', message: decision.message ?? 'User denied tool use' };
|
|
561
539
|
};
|
|
562
540
|
|
|
563
|
-
//
|
|
541
|
+
// Set stream-close timeout for interactive tools (Query constructor reads it synchronously). Claude Agent SDK has a default of 5s and this overrides it
|
|
542
|
+
const prevStreamTimeout = process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT;
|
|
543
|
+
process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT = '300000';
|
|
544
|
+
|
|
564
545
|
const queryInstance = query({
|
|
565
546
|
prompt: finalCommand,
|
|
566
547
|
options: sdkOptions
|
|
567
548
|
});
|
|
568
549
|
|
|
550
|
+
// Restore immediately — Query constructor already captured the value
|
|
551
|
+
if (prevStreamTimeout !== undefined) {
|
|
552
|
+
process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT = prevStreamTimeout;
|
|
553
|
+
} else {
|
|
554
|
+
delete process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT;
|
|
555
|
+
}
|
|
556
|
+
|
|
569
557
|
// Track the query instance for abort capability
|
|
570
558
|
if (capturedSessionId) {
|
|
571
559
|
addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir);
|
|
Binary file
|
package/server/index.js
CHANGED
|
@@ -63,8 +63,24 @@ import { initializeDatabase } from './database/db.js';
|
|
|
63
63
|
import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
|
|
64
64
|
import { IS_PLATFORM } from './constants/config.js';
|
|
65
65
|
|
|
66
|
-
// File system
|
|
67
|
-
|
|
66
|
+
// File system watchers for provider project/session folders
|
|
67
|
+
const PROVIDER_WATCH_PATHS = [
|
|
68
|
+
{ provider: 'claude', rootPath: path.join(os.homedir(), '.claude', 'projects') },
|
|
69
|
+
{ provider: 'cursor', rootPath: path.join(os.homedir(), '.cursor', 'chats') },
|
|
70
|
+
{ provider: 'codex', rootPath: path.join(os.homedir(), '.codex', 'sessions') }
|
|
71
|
+
];
|
|
72
|
+
const WATCHER_IGNORED_PATTERNS = [
|
|
73
|
+
'**/node_modules/**',
|
|
74
|
+
'**/.git/**',
|
|
75
|
+
'**/dist/**',
|
|
76
|
+
'**/build/**',
|
|
77
|
+
'**/*.tmp',
|
|
78
|
+
'**/*.swp',
|
|
79
|
+
'**/.DS_Store'
|
|
80
|
+
];
|
|
81
|
+
const WATCHER_DEBOUNCE_MS = 300;
|
|
82
|
+
let projectsWatchers = [];
|
|
83
|
+
let projectsWatcherDebounceTimer = null;
|
|
68
84
|
const connectedClients = new Set();
|
|
69
85
|
let isGetProjectsRunning = false; // Flag to prevent reentrant calls
|
|
70
86
|
|
|
@@ -81,94 +97,110 @@ function broadcastProgress(progress) {
|
|
|
81
97
|
});
|
|
82
98
|
}
|
|
83
99
|
|
|
84
|
-
// Setup file system
|
|
100
|
+
// Setup file system watchers for Claude, Cursor, and Codex project/session folders
|
|
85
101
|
async function setupProjectsWatcher() {
|
|
86
102
|
const chokidar = (await import('chokidar')).default;
|
|
87
|
-
const claudeProjectsPath = path.join(os.homedir(), '.claude', 'projects');
|
|
88
103
|
|
|
89
|
-
if (
|
|
90
|
-
|
|
104
|
+
if (projectsWatcherDebounceTimer) {
|
|
105
|
+
clearTimeout(projectsWatcherDebounceTimer);
|
|
106
|
+
projectsWatcherDebounceTimer = null;
|
|
91
107
|
}
|
|
92
108
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
'
|
|
99
|
-
'**/dist/**',
|
|
100
|
-
'**/build/**',
|
|
101
|
-
'**/*.tmp',
|
|
102
|
-
'**/*.swp',
|
|
103
|
-
'**/.DS_Store'
|
|
104
|
-
],
|
|
105
|
-
persistent: true,
|
|
106
|
-
ignoreInitial: true, // Don't fire events for existing files on startup
|
|
107
|
-
followSymlinks: false,
|
|
108
|
-
depth: 10, // Reasonable depth limit
|
|
109
|
-
awaitWriteFinish: {
|
|
110
|
-
stabilityThreshold: 100, // Wait 100ms for file to stabilize
|
|
111
|
-
pollInterval: 50
|
|
109
|
+
await Promise.all(
|
|
110
|
+
projectsWatchers.map(async (watcher) => {
|
|
111
|
+
try {
|
|
112
|
+
await watcher.close();
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error('[WARN] Failed to close watcher:', error);
|
|
112
115
|
}
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
let debounceTimer;
|
|
117
|
-
const debouncedUpdate = async (eventType, filePath) => {
|
|
118
|
-
clearTimeout(debounceTimer);
|
|
119
|
-
debounceTimer = setTimeout(async () => {
|
|
120
|
-
// Prevent reentrant calls
|
|
121
|
-
if (isGetProjectsRunning) {
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
116
|
+
})
|
|
117
|
+
);
|
|
118
|
+
projectsWatchers = [];
|
|
124
119
|
|
|
125
|
-
|
|
126
|
-
|
|
120
|
+
const debouncedUpdate = (eventType, filePath, provider, rootPath) => {
|
|
121
|
+
if (projectsWatcherDebounceTimer) {
|
|
122
|
+
clearTimeout(projectsWatcherDebounceTimer);
|
|
123
|
+
}
|
|
127
124
|
|
|
128
|
-
|
|
129
|
-
|
|
125
|
+
projectsWatcherDebounceTimer = setTimeout(async () => {
|
|
126
|
+
// Prevent reentrant calls
|
|
127
|
+
if (isGetProjectsRunning) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
130
|
|
|
131
|
-
|
|
132
|
-
|
|
131
|
+
try {
|
|
132
|
+
isGetProjectsRunning = true;
|
|
133
|
+
|
|
134
|
+
// Clear project directory cache when files change
|
|
135
|
+
clearProjectDirectoryCache();
|
|
136
|
+
|
|
137
|
+
// Get updated projects list
|
|
138
|
+
const updatedProjects = await getProjects(broadcastProgress);
|
|
139
|
+
|
|
140
|
+
// Notify all connected clients about the project changes
|
|
141
|
+
const updateMessage = JSON.stringify({
|
|
142
|
+
type: 'projects_updated',
|
|
143
|
+
projects: updatedProjects,
|
|
144
|
+
timestamp: new Date().toISOString(),
|
|
145
|
+
changeType: eventType,
|
|
146
|
+
changedFile: path.relative(rootPath, filePath),
|
|
147
|
+
watchProvider: provider
|
|
148
|
+
});
|
|
133
149
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
changeType: eventType,
|
|
140
|
-
changedFile: path.relative(claudeProjectsPath, filePath)
|
|
141
|
-
});
|
|
150
|
+
connectedClients.forEach(client => {
|
|
151
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
152
|
+
client.send(updateMessage);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
142
155
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
156
|
+
} catch (error) {
|
|
157
|
+
console.error('[ERROR] Error handling project changes:', error);
|
|
158
|
+
} finally {
|
|
159
|
+
isGetProjectsRunning = false;
|
|
160
|
+
}
|
|
161
|
+
}, WATCHER_DEBOUNCE_MS);
|
|
162
|
+
};
|
|
148
163
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
164
|
+
for (const { provider, rootPath } of PROVIDER_WATCH_PATHS) {
|
|
165
|
+
try {
|
|
166
|
+
// chokidar v4 emits ENOENT via the "error" event for missing roots and will not auto-recover.
|
|
167
|
+
// Ensure provider folders exist before creating the watcher so watching stays active.
|
|
168
|
+
await fsPromises.mkdir(rootPath, { recursive: true });
|
|
169
|
+
|
|
170
|
+
// Initialize chokidar watcher with optimized settings
|
|
171
|
+
const watcher = chokidar.watch(rootPath, {
|
|
172
|
+
ignored: WATCHER_IGNORED_PATTERNS,
|
|
173
|
+
persistent: true,
|
|
174
|
+
ignoreInitial: true, // Don't fire events for existing files on startup
|
|
175
|
+
followSymlinks: false,
|
|
176
|
+
depth: 10, // Reasonable depth limit
|
|
177
|
+
awaitWriteFinish: {
|
|
178
|
+
stabilityThreshold: 100, // Wait 100ms for file to stabilize
|
|
179
|
+
pollInterval: 50
|
|
153
180
|
}
|
|
154
|
-
}, 300); // 300ms debounce (slightly faster than before)
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
// Set up event listeners
|
|
158
|
-
projectsWatcher
|
|
159
|
-
.on('add', (filePath) => debouncedUpdate('add', filePath))
|
|
160
|
-
.on('change', (filePath) => debouncedUpdate('change', filePath))
|
|
161
|
-
.on('unlink', (filePath) => debouncedUpdate('unlink', filePath))
|
|
162
|
-
.on('addDir', (dirPath) => debouncedUpdate('addDir', dirPath))
|
|
163
|
-
.on('unlinkDir', (dirPath) => debouncedUpdate('unlinkDir', dirPath))
|
|
164
|
-
.on('error', (error) => {
|
|
165
|
-
console.error('[ERROR] Chokidar watcher error:', error);
|
|
166
|
-
})
|
|
167
|
-
.on('ready', () => {
|
|
168
181
|
});
|
|
169
182
|
|
|
170
|
-
|
|
171
|
-
|
|
183
|
+
// Set up event listeners
|
|
184
|
+
watcher
|
|
185
|
+
.on('add', (filePath) => debouncedUpdate('add', filePath, provider, rootPath))
|
|
186
|
+
.on('change', (filePath) => debouncedUpdate('change', filePath, provider, rootPath))
|
|
187
|
+
.on('unlink', (filePath) => debouncedUpdate('unlink', filePath, provider, rootPath))
|
|
188
|
+
.on('addDir', (dirPath) => debouncedUpdate('addDir', dirPath, provider, rootPath))
|
|
189
|
+
.on('unlinkDir', (dirPath) => debouncedUpdate('unlinkDir', dirPath, provider, rootPath))
|
|
190
|
+
.on('error', (error) => {
|
|
191
|
+
console.error(`[ERROR] ${provider} watcher error:`, error);
|
|
192
|
+
})
|
|
193
|
+
.on('ready', () => {
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
projectsWatchers.push(watcher);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
console.error(`[ERROR] Failed to setup ${provider} watcher for ${rootPath}:`, error);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (projectsWatchers.length === 0) {
|
|
203
|
+
console.error('[ERROR] Failed to setup any provider watchers');
|
|
172
204
|
}
|
|
173
205
|
}
|
|
174
206
|
|
|
@@ -671,7 +703,6 @@ app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) =
|
|
|
671
703
|
const { projectName } = req.params;
|
|
672
704
|
const { filePath } = req.query;
|
|
673
705
|
|
|
674
|
-
console.log('[DEBUG] File read request:', projectName, filePath);
|
|
675
706
|
|
|
676
707
|
// Security: ensure the requested path is inside the project root
|
|
677
708
|
if (!filePath) {
|
|
@@ -712,7 +743,6 @@ app.get('/api/projects/:projectName/files/content', authenticateToken, async (re
|
|
|
712
743
|
const { projectName } = req.params;
|
|
713
744
|
const { path: filePath } = req.query;
|
|
714
745
|
|
|
715
|
-
console.log('[DEBUG] Binary file serve request:', projectName, filePath);
|
|
716
746
|
|
|
717
747
|
// Security: ensure the requested path is inside the project root
|
|
718
748
|
if (!filePath) {
|
|
@@ -766,7 +796,6 @@ app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) =
|
|
|
766
796
|
const { projectName } = req.params;
|
|
767
797
|
const { filePath, content } = req.body;
|
|
768
798
|
|
|
769
|
-
console.log('[DEBUG] File save request:', projectName, filePath);
|
|
770
799
|
|
|
771
800
|
// Security: ensure the requested path is inside the project root
|
|
772
801
|
if (!filePath) {
|
package/server/openai-codex.js
CHANGED
|
@@ -203,6 +203,7 @@ export async function queryCodex(command, options = {}, ws) {
|
|
|
203
203
|
let codex;
|
|
204
204
|
let thread;
|
|
205
205
|
let currentSessionId = sessionId;
|
|
206
|
+
const abortController = new AbortController();
|
|
206
207
|
|
|
207
208
|
try {
|
|
208
209
|
// Initialize Codex SDK
|
|
@@ -232,6 +233,7 @@ export async function queryCodex(command, options = {}, ws) {
|
|
|
232
233
|
thread,
|
|
233
234
|
codex,
|
|
234
235
|
status: 'running',
|
|
236
|
+
abortController,
|
|
235
237
|
startedAt: new Date().toISOString()
|
|
236
238
|
});
|
|
237
239
|
|
|
@@ -243,7 +245,9 @@ export async function queryCodex(command, options = {}, ws) {
|
|
|
243
245
|
});
|
|
244
246
|
|
|
245
247
|
// Execute with streaming
|
|
246
|
-
const streamedTurn = await thread.runStreamed(command
|
|
248
|
+
const streamedTurn = await thread.runStreamed(command, {
|
|
249
|
+
signal: abortController.signal
|
|
250
|
+
});
|
|
247
251
|
|
|
248
252
|
for await (const event of streamedTurn.events) {
|
|
249
253
|
// Check if session was aborted
|
|
@@ -286,20 +290,27 @@ export async function queryCodex(command, options = {}, ws) {
|
|
|
286
290
|
});
|
|
287
291
|
|
|
288
292
|
} catch (error) {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
error
|
|
294
|
-
|
|
295
|
-
|
|
293
|
+
const session = currentSessionId ? activeCodexSessions.get(currentSessionId) : null;
|
|
294
|
+
const wasAborted =
|
|
295
|
+
session?.status === 'aborted' ||
|
|
296
|
+
error?.name === 'AbortError' ||
|
|
297
|
+
String(error?.message || '').toLowerCase().includes('aborted');
|
|
298
|
+
|
|
299
|
+
if (!wasAborted) {
|
|
300
|
+
console.error('[Codex] Error:', error);
|
|
301
|
+
sendMessage(ws, {
|
|
302
|
+
type: 'codex-error',
|
|
303
|
+
error: error.message,
|
|
304
|
+
sessionId: currentSessionId
|
|
305
|
+
});
|
|
306
|
+
}
|
|
296
307
|
|
|
297
308
|
} finally {
|
|
298
309
|
// Update session status
|
|
299
310
|
if (currentSessionId) {
|
|
300
311
|
const session = activeCodexSessions.get(currentSessionId);
|
|
301
312
|
if (session) {
|
|
302
|
-
session.status = 'completed';
|
|
313
|
+
session.status = session.status === 'aborted' ? 'aborted' : 'completed';
|
|
303
314
|
}
|
|
304
315
|
}
|
|
305
316
|
}
|
|
@@ -318,9 +329,11 @@ export function abortCodexSession(sessionId) {
|
|
|
318
329
|
}
|
|
319
330
|
|
|
320
331
|
session.status = 'aborted';
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
332
|
+
try {
|
|
333
|
+
session.abortController?.abort();
|
|
334
|
+
} catch (error) {
|
|
335
|
+
console.warn(`[Codex] Failed to abort session ${sessionId}:`, error);
|
|
336
|
+
}
|
|
324
337
|
|
|
325
338
|
return true;
|
|
326
339
|
}
|