@epiphytic/claudecodeui 1.0.0 → 1.1.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/dist/assets/index-D0xTNXrF.js +1247 -0
- package/dist/assets/index-DKDK7xNY.css +32 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/server/database/db.js +124 -0
- package/server/database/init.sql +15 -1
- package/server/external-session-detector.js +403 -0
- package/server/index.js +816 -110
- package/server/orchestrator/client.js +37 -1
- package/server/projects-cache.js +196 -0
- package/server/projects.js +759 -464
- package/server/routes/projects.js +248 -92
- package/server/routes/sessions.js +106 -0
- package/server/session-lock.js +253 -0
- package/server/sessions-cache.js +183 -0
- package/server/tmux-manager.js +403 -0
- package/dist/assets/index-DfR9xEkp.css +0 -32
- package/dist/assets/index-DvlVn6Eb.js +0 -1231
|
@@ -277,7 +277,8 @@ export class OrchestratorClient extends EventEmitter {
|
|
|
277
277
|
// Set timeout for pong response
|
|
278
278
|
this.heartbeatTimeoutTimer = setTimeout(() => {
|
|
279
279
|
console.warn("[ORCHESTRATOR] Heartbeat timeout, reconnecting...");
|
|
280
|
-
|
|
280
|
+
// Force close without waiting - terminate() can block if socket is dead
|
|
281
|
+
this.forceReconnect();
|
|
281
282
|
}, DEFAULTS.heartbeatTimeout);
|
|
282
283
|
}
|
|
283
284
|
|
|
@@ -879,6 +880,41 @@ export class OrchestratorClient extends EventEmitter {
|
|
|
879
880
|
return result;
|
|
880
881
|
}
|
|
881
882
|
|
|
883
|
+
/**
|
|
884
|
+
* Force reconnect without waiting for socket close
|
|
885
|
+
* This handles the case where the socket is dead (e.g., after macOS sleep)
|
|
886
|
+
* and terminate() would block indefinitely
|
|
887
|
+
*/
|
|
888
|
+
forceReconnect() {
|
|
889
|
+
console.log("[ORCHESTRATOR] Force reconnecting...");
|
|
890
|
+
|
|
891
|
+
// Mark as disconnected immediately
|
|
892
|
+
this.isConnected = false;
|
|
893
|
+
this.isRegistered = false;
|
|
894
|
+
this.stopHeartbeat();
|
|
895
|
+
|
|
896
|
+
// Try to close the socket, but don't wait for it
|
|
897
|
+
if (this.ws) {
|
|
898
|
+
try {
|
|
899
|
+
// Destroy the underlying socket directly to avoid blocking
|
|
900
|
+
if (this.ws._socket) {
|
|
901
|
+
this.ws._socket.destroy();
|
|
902
|
+
}
|
|
903
|
+
this.ws.terminate();
|
|
904
|
+
} catch (e) {
|
|
905
|
+
// Ignore errors - socket may already be dead
|
|
906
|
+
}
|
|
907
|
+
this.ws = null;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
this.emit("disconnected", { code: 1006, reason: "Force reconnect" });
|
|
911
|
+
|
|
912
|
+
// Schedule reconnect
|
|
913
|
+
if (this.shouldReconnect) {
|
|
914
|
+
this.scheduleReconnect();
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
882
918
|
/**
|
|
883
919
|
* Starts the heartbeat interval
|
|
884
920
|
*/
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PROJECTS CACHE MODULE
|
|
3
|
+
* =====================
|
|
4
|
+
*
|
|
5
|
+
* In-memory cache for projects data with ETag support.
|
|
6
|
+
* Updated by the chokidar watcher when project files change.
|
|
7
|
+
* Mirrors the sessions-cache.js pattern for consistency.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import crypto from "crypto";
|
|
11
|
+
|
|
12
|
+
// Cache state
|
|
13
|
+
let cachedProjects = [];
|
|
14
|
+
let cacheVersion = 0;
|
|
15
|
+
let cacheTimestamp = null;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Timeframe definitions in milliseconds
|
|
19
|
+
* (Same as sessions-cache.js for consistency)
|
|
20
|
+
*/
|
|
21
|
+
const TIMEFRAME_MS = {
|
|
22
|
+
"1h": 60 * 60 * 1000,
|
|
23
|
+
"8h": 8 * 60 * 60 * 1000,
|
|
24
|
+
"1d": 24 * 60 * 60 * 1000,
|
|
25
|
+
"1w": 7 * 24 * 60 * 60 * 1000,
|
|
26
|
+
"2w": 14 * 24 * 60 * 60 * 1000,
|
|
27
|
+
"1m": 30 * 24 * 60 * 60 * 1000,
|
|
28
|
+
all: Infinity,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Calculate last activity timestamp for a project
|
|
33
|
+
* Based on the most recent session across all providers
|
|
34
|
+
*/
|
|
35
|
+
function calculateLastActivity(project) {
|
|
36
|
+
let lastActivity = null;
|
|
37
|
+
|
|
38
|
+
// Check Claude sessions
|
|
39
|
+
if (project.sessions && project.sessions.length > 0) {
|
|
40
|
+
for (const session of project.sessions) {
|
|
41
|
+
const sessionDate = new Date(session.lastActivity);
|
|
42
|
+
if (!lastActivity || sessionDate > lastActivity) {
|
|
43
|
+
lastActivity = sessionDate;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Check Cursor sessions
|
|
49
|
+
if (project.cursorSessions && project.cursorSessions.length > 0) {
|
|
50
|
+
for (const session of project.cursorSessions) {
|
|
51
|
+
const sessionDate = new Date(session.createdAt || session.lastActivity);
|
|
52
|
+
if (!lastActivity || sessionDate > lastActivity) {
|
|
53
|
+
lastActivity = sessionDate;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check Codex sessions
|
|
59
|
+
if (project.codexSessions && project.codexSessions.length > 0) {
|
|
60
|
+
for (const session of project.codexSessions) {
|
|
61
|
+
const sessionDate = new Date(session.lastActivity || session.createdAt);
|
|
62
|
+
if (!lastActivity || sessionDate > lastActivity) {
|
|
63
|
+
lastActivity = sessionDate;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return lastActivity ? lastActivity.toISOString() : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Transform full project data to slim format
|
|
73
|
+
*/
|
|
74
|
+
function toSlimProject(project) {
|
|
75
|
+
const claudeCount = project.sessions?.length || 0;
|
|
76
|
+
const cursorCount = project.cursorSessions?.length || 0;
|
|
77
|
+
const codexCount = project.codexSessions?.length || 0;
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
name: project.name,
|
|
81
|
+
displayName: project.displayName,
|
|
82
|
+
fullPath: project.fullPath || project.path,
|
|
83
|
+
sessionCount: claudeCount + cursorCount + codexCount,
|
|
84
|
+
lastActivity: calculateLastActivity(project),
|
|
85
|
+
hasClaudeSessions: claudeCount > 0,
|
|
86
|
+
hasCursorSessions: cursorCount > 0,
|
|
87
|
+
hasCodexSessions: codexCount > 0,
|
|
88
|
+
hasTaskmaster: project.taskmaster?.hasTaskmaster || false,
|
|
89
|
+
sessionMeta: project.sessionMeta,
|
|
90
|
+
isManuallyAdded: project.isManuallyAdded || false,
|
|
91
|
+
isCustomName: project.isCustomName || false,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Update the projects cache from full projects data
|
|
97
|
+
* Called after getProjects() completes
|
|
98
|
+
*/
|
|
99
|
+
function updateProjectsCache(projects) {
|
|
100
|
+
// Transform to slim format
|
|
101
|
+
cachedProjects = projects.map(toSlimProject);
|
|
102
|
+
|
|
103
|
+
// Sort by lastActivity descending (most recent first)
|
|
104
|
+
cachedProjects.sort((a, b) => {
|
|
105
|
+
const dateA = a.lastActivity ? new Date(a.lastActivity) : new Date(0);
|
|
106
|
+
const dateB = b.lastActivity ? new Date(b.lastActivity) : new Date(0);
|
|
107
|
+
return dateB - dateA;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
cacheVersion++;
|
|
111
|
+
cacheTimestamp = new Date().toISOString();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get projects filtered by timeframe
|
|
116
|
+
* Projects are included if their lastActivity is within the timeframe
|
|
117
|
+
*/
|
|
118
|
+
function getProjectsByTimeframe(timeframe = "1w") {
|
|
119
|
+
const now = Date.now();
|
|
120
|
+
const cutoffMs = TIMEFRAME_MS[timeframe] || TIMEFRAME_MS["1w"];
|
|
121
|
+
|
|
122
|
+
if (cutoffMs === Infinity) {
|
|
123
|
+
return {
|
|
124
|
+
projects: cachedProjects,
|
|
125
|
+
totalCount: cachedProjects.length,
|
|
126
|
+
filteredCount: cachedProjects.length,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const cutoffTime = now - cutoffMs;
|
|
131
|
+
const filteredProjects = cachedProjects.filter((project) => {
|
|
132
|
+
if (!project.lastActivity) {
|
|
133
|
+
return false; // Exclude projects with no sessions
|
|
134
|
+
}
|
|
135
|
+
const projectTime = new Date(project.lastActivity).getTime();
|
|
136
|
+
return projectTime >= cutoffTime;
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
projects: filteredProjects,
|
|
141
|
+
totalCount: cachedProjects.length,
|
|
142
|
+
filteredCount: filteredProjects.length,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Generate ETag for current cache state + timeframe
|
|
148
|
+
*/
|
|
149
|
+
function generateETag(timeframe = "1w") {
|
|
150
|
+
const hash = crypto.createHash("md5");
|
|
151
|
+
hash.update(`projects-${cacheVersion}-${cacheTimestamp}-${timeframe}`);
|
|
152
|
+
return `"${hash.digest("hex")}"`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get cache metadata
|
|
157
|
+
*/
|
|
158
|
+
function getCacheMeta() {
|
|
159
|
+
return {
|
|
160
|
+
version: cacheVersion,
|
|
161
|
+
timestamp: cacheTimestamp,
|
|
162
|
+
projectCount: cachedProjects.length,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Check if cache is initialized
|
|
168
|
+
*/
|
|
169
|
+
function isCacheInitialized() {
|
|
170
|
+
return cacheTimestamp !== null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get the raw cached projects (for initial load)
|
|
175
|
+
*/
|
|
176
|
+
function getCachedProjects() {
|
|
177
|
+
return cachedProjects;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Get a single project from cache by name
|
|
182
|
+
*/
|
|
183
|
+
function getProjectFromCache(projectName) {
|
|
184
|
+
return cachedProjects.find((p) => p.name === projectName) || null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export {
|
|
188
|
+
updateProjectsCache,
|
|
189
|
+
getProjectsByTimeframe,
|
|
190
|
+
generateETag,
|
|
191
|
+
getCacheMeta,
|
|
192
|
+
isCacheInitialized,
|
|
193
|
+
getCachedProjects,
|
|
194
|
+
getProjectFromCache,
|
|
195
|
+
TIMEFRAME_MS,
|
|
196
|
+
};
|