clay-server 2.11.0-beta.15 → 2.11.0-beta.17
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/bin/cli.js +55 -9
- package/lib/daemon.js +58 -27
- package/lib/project.js +26 -7
- package/lib/public/app.js +141 -12
- package/lib/public/css/filebrowser.css +6 -0
- package/lib/public/css/icon-strip.css +123 -1
- package/lib/public/css/mobile-nav.css +17 -0
- package/lib/public/css/overlays.css +49 -0
- package/lib/public/css/sidebar.css +26 -0
- package/lib/public/index.html +1 -0
- package/lib/public/modules/sidebar.js +299 -21
- package/lib/public/modules/sticky-notes.js +6 -2
- package/lib/public/modules/terminal.js +23 -15
- package/lib/sdk-bridge.js +21 -7
- package/lib/server.js +110 -16
- package/lib/sessions.js +48 -7
- package/lib/users.js +90 -0
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -443,6 +443,7 @@ async function restartDaemonFromConfig() {
|
|
|
443
443
|
projects: (lastConfig.projects || []).filter(function (p) {
|
|
444
444
|
return fs.existsSync(p.path);
|
|
445
445
|
}),
|
|
446
|
+
removedProjects: lastConfig.removedProjects || [],
|
|
446
447
|
};
|
|
447
448
|
|
|
448
449
|
ensureConfigDir();
|
|
@@ -1223,13 +1224,29 @@ function setup(callback) {
|
|
|
1223
1224
|
log("");
|
|
1224
1225
|
log(sym.pointer + " " + a.bold + "Clay" + a.reset + a.dim + " · Unofficial, open-source project" + a.reset);
|
|
1225
1226
|
log(sym.bar);
|
|
1226
|
-
log(sym.bar + " " + a.
|
|
1227
|
-
log(sym.bar
|
|
1228
|
-
log(sym.bar + " " + a.dim + "
|
|
1227
|
+
log(sym.bar + " " + a.yellow + sym.warn + " Disclaimer" + a.reset);
|
|
1228
|
+
log(sym.bar);
|
|
1229
|
+
log(sym.bar + " " + a.dim + "This is an independent project and is not affiliated with Anthropic." + a.reset);
|
|
1230
|
+
log(sym.bar + " " + a.dim + "Claude is a trademark of Anthropic." + a.reset);
|
|
1231
|
+
log(sym.bar);
|
|
1232
|
+
log(sym.bar + " " + a.dim + "Clay is provided \"as is\" without warranty of any kind. Users are" + a.reset);
|
|
1233
|
+
log(sym.bar + " " + a.dim + "responsible for complying with the terms of service of underlying AI" + a.reset);
|
|
1234
|
+
log(sym.bar + " " + a.dim + "providers (e.g., Anthropic, OpenAI) and all applicable terms of any" + a.reset);
|
|
1235
|
+
log(sym.bar + " " + a.dim + "third-party services." + a.reset);
|
|
1236
|
+
log(sym.bar);
|
|
1237
|
+
log(sym.bar + " " + a.dim + "Features such as multi-user mode are experimental and may involve" + a.reset);
|
|
1238
|
+
log(sym.bar + " " + a.dim + "sharing access to API-based services. Before enabling such features," + a.reset);
|
|
1239
|
+
log(sym.bar + " " + a.dim + "review your provider's usage policies regarding account sharing," + a.reset);
|
|
1240
|
+
log(sym.bar + " " + a.dim + "acceptable use, and any applicable rate limits or restrictions." + a.reset);
|
|
1241
|
+
log(sym.bar);
|
|
1242
|
+
log(sym.bar + " " + a.dim + "The authors assume no liability for misuse or violations arising" + a.reset);
|
|
1243
|
+
log(sym.bar + " " + a.dim + "from the use of this software." + a.reset);
|
|
1244
|
+
log(sym.bar);
|
|
1245
|
+
log(sym.bar + " Type " + a.bold + "agree" + a.reset + " to accept and continue.");
|
|
1229
1246
|
log(sym.bar);
|
|
1230
1247
|
|
|
1231
|
-
|
|
1232
|
-
if (!
|
|
1248
|
+
promptText("", "", function (val) {
|
|
1249
|
+
if (!val || val.trim().toLowerCase() !== "agree") {
|
|
1233
1250
|
log(sym.end + " " + a.dim + "Aborted." + a.reset);
|
|
1234
1251
|
log("");
|
|
1235
1252
|
process.exit(0);
|
|
@@ -1284,8 +1301,37 @@ function setup(callback) {
|
|
|
1284
1301
|
return;
|
|
1285
1302
|
}
|
|
1286
1303
|
log(sym.bar);
|
|
1287
|
-
|
|
1288
|
-
|
|
1304
|
+
promptSelect("Enable OS-level user isolation?", [
|
|
1305
|
+
{ label: "Yes", value: "yes" },
|
|
1306
|
+
{ label: "No", value: "no" },
|
|
1307
|
+
], function (choice) {
|
|
1308
|
+
if (choice !== "yes") {
|
|
1309
|
+
finishSetup(mode, false);
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
log(sym.bar);
|
|
1313
|
+
log(sym.bar + " " + a.yellow + sym.warn + " OS-Level User Isolation" + a.reset);
|
|
1314
|
+
log(sym.bar);
|
|
1315
|
+
log(sym.bar + " " + a.dim + "This feature maps each Clay user to a Linux OS user account." + a.reset);
|
|
1316
|
+
log(sym.bar + " " + a.dim + "The daemon must run as root and will spawn processes (SDK workers," + a.reset);
|
|
1317
|
+
log(sym.bar + " " + a.dim + "terminals, file operations) as the mapped Linux user." + a.reset);
|
|
1318
|
+
log(sym.bar);
|
|
1319
|
+
log(sym.bar + " " + a.dim + "What this means:" + a.reset);
|
|
1320
|
+
log(sym.bar + " " + a.dim + "- Each mapped user uses their own ~/.claude/ credentials" + a.reset);
|
|
1321
|
+
log(sym.bar + " " + a.dim + "- Terminals and file access follow Linux permissions" + a.reset);
|
|
1322
|
+
log(sym.bar + " " + a.dim + "- Linux user accounts are created automatically (clay-username)" + a.reset);
|
|
1323
|
+
log(sym.bar);
|
|
1324
|
+
log(sym.bar + " " + a.dim + "Recommended: Run on a dedicated Clay server or cloud instance," + a.reset);
|
|
1325
|
+
log(sym.bar + " " + a.dim + "not on a personal computer or general-purpose server." + a.reset);
|
|
1326
|
+
log(sym.bar);
|
|
1327
|
+
promptSelect("Confirm", [
|
|
1328
|
+
{ label: "Enable OS-level user isolation", value: "confirm" },
|
|
1329
|
+
{ label: "Cancel", value: "cancel" },
|
|
1330
|
+
], function (confirmChoice) {
|
|
1331
|
+
if (confirmChoice !== "confirm") {
|
|
1332
|
+
finishSetup(mode, false);
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1289
1335
|
var isRoot = typeof process.getuid === "function" && process.getuid() === 0;
|
|
1290
1336
|
if (!isRoot) {
|
|
1291
1337
|
// Save config so sudo clay can pick it up
|
|
@@ -1307,8 +1353,8 @@ function setup(callback) {
|
|
|
1307
1353
|
process.exit(0);
|
|
1308
1354
|
return;
|
|
1309
1355
|
}
|
|
1310
|
-
|
|
1311
|
-
|
|
1356
|
+
finishSetup(mode, true);
|
|
1357
|
+
});
|
|
1312
1358
|
});
|
|
1313
1359
|
}
|
|
1314
1360
|
|
package/lib/daemon.js
CHANGED
|
@@ -82,6 +82,18 @@ var lanIp = (function () {
|
|
|
82
82
|
return null;
|
|
83
83
|
})();
|
|
84
84
|
|
|
85
|
+
// --- Helper: get removed projects filtered by existing paths and userId ---
|
|
86
|
+
function getFilteredRemovedProjects(userId) {
|
|
87
|
+
if (!config.removedProjects || config.removedProjects.length === 0) return [];
|
|
88
|
+
return config.removedProjects.filter(function (rp) {
|
|
89
|
+
// In single-user mode (no userId), show entries with no userId
|
|
90
|
+
// In multi-user mode, only show entries belonging to this user
|
|
91
|
+
if (userId && rp.userId && rp.userId !== userId) return false;
|
|
92
|
+
if (!userId && rp.userId) return false;
|
|
93
|
+
return fs.existsSync(rp.path);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
85
97
|
// --- Create multi-project server ---
|
|
86
98
|
var listenHost = config.host || "0.0.0.0";
|
|
87
99
|
|
|
@@ -94,6 +106,7 @@ var relay = createServer({
|
|
|
94
106
|
dangerouslySkipPermissions: config.dangerouslySkipPermissions || false,
|
|
95
107
|
osUsers: config.osUsers || false,
|
|
96
108
|
lanHost: lanIp ? lanIp + ":" + config.port : null,
|
|
109
|
+
getRemovedProjects: function (userId) { return getFilteredRemovedProjects(userId); },
|
|
97
110
|
onAddProject: function (absPath) {
|
|
98
111
|
// Check if already registered
|
|
99
112
|
for (var j = 0; j < config.projects.length; j++) {
|
|
@@ -105,6 +118,10 @@ var relay = createServer({
|
|
|
105
118
|
var slug = generateSlug(absPath, slugs);
|
|
106
119
|
relay.addProject(absPath, slug);
|
|
107
120
|
config.projects.push({ path: absPath, slug: slug, addedAt: Date.now() });
|
|
121
|
+
// Remove from removedProjects if present
|
|
122
|
+
if (config.removedProjects) {
|
|
123
|
+
config.removedProjects = config.removedProjects.filter(function (rp) { return rp.path !== absPath; });
|
|
124
|
+
}
|
|
108
125
|
saveConfig(config);
|
|
109
126
|
try { syncClayrc(config.projects); } catch (e) {}
|
|
110
127
|
console.log("[daemon] Added project (web):", slug, "→", absPath);
|
|
@@ -261,22 +278,30 @@ var relay = createServer({
|
|
|
261
278
|
callback({ ok: false, error: "Failed to start git clone: " + err.message });
|
|
262
279
|
});
|
|
263
280
|
},
|
|
264
|
-
onRemoveProject: function (slug) {
|
|
265
|
-
var found =
|
|
281
|
+
onRemoveProject: function (slug, userId) {
|
|
282
|
+
var found = null;
|
|
266
283
|
for (var j = 0; j < config.projects.length; j++) {
|
|
267
|
-
if (config.projects[j].slug === slug) { found =
|
|
284
|
+
if (config.projects[j].slug === slug) { found = config.projects[j]; break; }
|
|
268
285
|
}
|
|
269
286
|
if (!found) return { ok: false, error: "Project not found" };
|
|
270
|
-
//
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
287
|
+
// Save to removedProjects for re-add functionality
|
|
288
|
+
if (!config.removedProjects) config.removedProjects = [];
|
|
289
|
+
config.removedProjects.push({
|
|
290
|
+
path: found.path,
|
|
291
|
+
title: found.title || null,
|
|
292
|
+
icon: found.icon || null,
|
|
293
|
+
userId: userId || null,
|
|
294
|
+
removedAt: Date.now(),
|
|
295
|
+
});
|
|
296
|
+
// Cap at 20 entries (oldest first)
|
|
297
|
+
if (config.removedProjects.length > 20) {
|
|
298
|
+
config.removedProjects = config.removedProjects.slice(config.removedProjects.length - 20);
|
|
274
299
|
}
|
|
275
300
|
relay.removeProject(slug);
|
|
276
301
|
config.projects = config.projects.filter(function (p) { return p.slug !== slug; });
|
|
277
302
|
saveConfig(config);
|
|
278
303
|
// Remove from .clayrc so it doesn't appear in restore prompt
|
|
279
|
-
if (
|
|
304
|
+
if (found.path) { try { removeFromClayrc(found.path); } catch (e) {} }
|
|
280
305
|
try { syncClayrc(config.projects); } catch (e) {}
|
|
281
306
|
console.log("[daemon] Removed project (web):", slug);
|
|
282
307
|
relay.broadcastAll({
|
|
@@ -1056,25 +1081,31 @@ if (config.keepAwake && process.platform === "darwin") {
|
|
|
1056
1081
|
|
|
1057
1082
|
// --- Spawn new daemon and graceful restart ---
|
|
1058
1083
|
function spawnAndRestart() {
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1084
|
+
try {
|
|
1085
|
+
var { spawn: spawnRestart } = require("child_process");
|
|
1086
|
+
var { logPath: restartLogPath, configPath: restartConfigPath } = require("./config");
|
|
1087
|
+
var daemonScript = path.join(__dirname, "daemon.js");
|
|
1088
|
+
var logFd = fs.openSync(restartLogPath(), "a");
|
|
1089
|
+
var child = spawnRestart(process.execPath, [daemonScript], {
|
|
1090
|
+
detached: true,
|
|
1091
|
+
windowsHide: true,
|
|
1092
|
+
stdio: ["ignore", logFd, logFd],
|
|
1093
|
+
env: Object.assign({}, process.env, {
|
|
1094
|
+
CLAY_CONFIG: restartConfigPath(),
|
|
1095
|
+
}),
|
|
1096
|
+
});
|
|
1097
|
+
child.unref();
|
|
1098
|
+
fs.closeSync(logFd);
|
|
1099
|
+
config.pid = child.pid;
|
|
1100
|
+
saveConfig(config);
|
|
1101
|
+
console.log("[daemon] Spawned new daemon (PID " + child.pid + "), shutting down...");
|
|
1102
|
+
updateHandoff = true;
|
|
1103
|
+
setTimeout(function () { gracefulShutdown(); }, 100);
|
|
1104
|
+
} catch (e) {
|
|
1105
|
+
console.error("[daemon] Restart failed:", e.message);
|
|
1106
|
+
relay.broadcastAll({ type: "toast", level: "error", message: "Restart failed: " + e.message });
|
|
1107
|
+
relay.broadcastAll({ type: "restart_server_result", ok: false, error: e.message });
|
|
1108
|
+
}
|
|
1078
1109
|
}
|
|
1079
1110
|
|
|
1080
1111
|
// --- Graceful shutdown ---
|
package/lib/project.js
CHANGED
|
@@ -108,6 +108,7 @@ function createProjectContext(opts) {
|
|
|
108
108
|
var moveAllSchedulesToProject = opts.moveAllSchedulesToProject || function () { return { ok: false, error: "Not supported" }; };
|
|
109
109
|
var getScheduleCount = opts.getScheduleCount || function () { return 0; };
|
|
110
110
|
var onProcessingChanged = opts.onProcessingChanged || function () {};
|
|
111
|
+
var onSessionDone = opts.onSessionDone || function () {};
|
|
111
112
|
var onPresenceChange = opts.onPresenceChange || function () {};
|
|
112
113
|
var updateChannel = opts.updateChannel || "stable";
|
|
113
114
|
var osUsers = opts.osUsers || false;
|
|
@@ -176,6 +177,13 @@ function createProjectContext(opts) {
|
|
|
176
177
|
if (ws.readyState === 1) ws.send(JSON.stringify(obj));
|
|
177
178
|
}
|
|
178
179
|
|
|
180
|
+
function sendToAdmins(obj) {
|
|
181
|
+
var data = JSON.stringify(obj);
|
|
182
|
+
for (var ws of clients) {
|
|
183
|
+
if (ws.readyState === 1 && ws._clayUser && ws._clayUser.role === "admin") ws.send(data);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
179
187
|
function broadcastClientCount() {
|
|
180
188
|
var msg = { type: "client_count", count: clients.size };
|
|
181
189
|
if (usersModule.isMultiUser()) {
|
|
@@ -338,6 +346,7 @@ function createProjectContext(opts) {
|
|
|
338
346
|
fn(ws, filterFn);
|
|
339
347
|
}
|
|
340
348
|
},
|
|
349
|
+
onSessionDone: onSessionDone,
|
|
341
350
|
});
|
|
342
351
|
var _projMode = typeof opts.onGetProjectDefaultMode === "function" ? opts.onGetProjectDefaultMode(slug) : null;
|
|
343
352
|
var _srvMode = typeof opts.onGetServerDefaultMode === "function" ? opts.onGetServerDefaultMode() : null;
|
|
@@ -998,11 +1007,11 @@ function createProjectContext(opts) {
|
|
|
998
1007
|
var tm = createTerminalManager({ cwd: cwd, send: send, sendTo: sendTo });
|
|
999
1008
|
var nm = createNotesManager({ cwd: cwd, send: send, sendTo: sendTo });
|
|
1000
1009
|
|
|
1001
|
-
// Check for updates in background
|
|
1010
|
+
// Check for updates in background (admin only)
|
|
1002
1011
|
fetchVersion(updateChannel).then(function (v) {
|
|
1003
1012
|
if (v && isNewer(v, currentVersion)) {
|
|
1004
1013
|
latestVersion = v;
|
|
1005
|
-
|
|
1014
|
+
sendToAdmins({ type: "update_available", version: v });
|
|
1006
1015
|
}
|
|
1007
1016
|
});
|
|
1008
1017
|
|
|
@@ -1022,7 +1031,7 @@ function createProjectContext(opts) {
|
|
|
1022
1031
|
var _userId = ws._clayUser ? ws._clayUser.id : null;
|
|
1023
1032
|
var _filteredProjects = getProjectList(_userId);
|
|
1024
1033
|
sendTo(ws, { type: "info", cwd: cwd, slug: slug, project: title || project, version: currentVersion, debug: !!debug, dangerouslySkipPermissions: dangerouslySkipPermissions, osUsers: osUsers, lanHost: lanHost, projectCount: _filteredProjects.length, projects: _filteredProjects, projectOwnerId: projectOwnerId });
|
|
1025
|
-
if (latestVersion) {
|
|
1034
|
+
if (latestVersion && ws._clayUser && ws._clayUser.role === "admin") {
|
|
1026
1035
|
sendTo(ws, { type: "update_available", version: latestVersion });
|
|
1027
1036
|
}
|
|
1028
1037
|
if (sm.slashCommands) {
|
|
@@ -1203,7 +1212,7 @@ function createProjectContext(opts) {
|
|
|
1203
1212
|
|
|
1204
1213
|
function handleMessage(ws, msg) {
|
|
1205
1214
|
// --- DM messages (delegated to server-level handler) ---
|
|
1206
|
-
if (msg.type === "dm_open" || msg.type === "dm_send" || msg.type === "dm_list" || msg.type === "dm_typing") {
|
|
1215
|
+
if (msg.type === "dm_open" || msg.type === "dm_send" || msg.type === "dm_list" || msg.type === "dm_typing" || msg.type === "dm_add_favorite" || msg.type === "dm_remove_favorite") {
|
|
1207
1216
|
if (typeof opts.onDmMessage === "function") {
|
|
1208
1217
|
opts.onDmMessage(ws, msg);
|
|
1209
1218
|
}
|
|
@@ -1405,9 +1414,17 @@ function createProjectContext(opts) {
|
|
|
1405
1414
|
if (usersModule.isMultiUser() && (!ws._clayUser || ws._clayUser.role !== "admin")) return;
|
|
1406
1415
|
var newChannel = msg.channel === "beta" ? "beta" : "stable";
|
|
1407
1416
|
updateChannel = newChannel;
|
|
1417
|
+
latestVersion = null;
|
|
1408
1418
|
if (typeof opts.onSetUpdateChannel === "function") {
|
|
1409
1419
|
opts.onSetUpdateChannel(newChannel);
|
|
1410
1420
|
}
|
|
1421
|
+
// Re-fetch with new channel and broadcast to admin clients
|
|
1422
|
+
fetchVersion(updateChannel).then(function (v) {
|
|
1423
|
+
if (v && isNewer(v, currentVersion)) {
|
|
1424
|
+
latestVersion = v;
|
|
1425
|
+
sendToAdmins({ type: "update_available", version: v });
|
|
1426
|
+
}
|
|
1427
|
+
}).catch(function () {});
|
|
1411
1428
|
return;
|
|
1412
1429
|
}
|
|
1413
1430
|
|
|
@@ -1815,7 +1832,7 @@ function createProjectContext(opts) {
|
|
|
1815
1832
|
new Promise(function (resolve) { setTimeout(resolve, 3000); }),
|
|
1816
1833
|
]).then(function () {
|
|
1817
1834
|
try {
|
|
1818
|
-
var newSession = sm.createSession();
|
|
1835
|
+
var newSession = sm.createSession(null, ws);
|
|
1819
1836
|
// Send the plan as the first user message (with planContent for UI rendering)
|
|
1820
1837
|
var userMsg = { type: "user_message", text: planPrompt, planContent: clientPlanContent || null };
|
|
1821
1838
|
newSession.history.push(userMsg);
|
|
@@ -2029,8 +2046,10 @@ function createProjectContext(opts) {
|
|
|
2029
2046
|
moveAllSchedulesToProject(removeSlug, msg.moveTasksTo);
|
|
2030
2047
|
}
|
|
2031
2048
|
if (typeof opts.onRemoveProject === "function") {
|
|
2032
|
-
|
|
2033
|
-
sendTo(ws, { type: "remove_project_result", ok:
|
|
2049
|
+
// Send result before removing so the WS is still open
|
|
2050
|
+
sendTo(ws, { type: "remove_project_result", ok: true, slug: removeSlug });
|
|
2051
|
+
var removeUserId = ws._clayUser ? ws._clayUser.id : null;
|
|
2052
|
+
opts.onRemoveProject(removeSlug, removeUserId);
|
|
2034
2053
|
} else {
|
|
2035
2054
|
sendTo(ws, { type: "remove_project_result", ok: false, error: "Not supported" });
|
|
2036
2055
|
}
|
package/lib/public/app.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { showToast, copyToClipboard, escapeHtml } from './modules/utils.js';
|
|
2
2
|
import { refreshIcons, iconHtml, randomThinkingVerb } from './modules/icons.js';
|
|
3
3
|
import { renderMarkdown, highlightCodeBlocks, renderMermaidBlocks, closeMermaidModal, parseEmojis } from './modules/markdown.js';
|
|
4
|
-
import { initSidebar, renderSessionList, handleSearchResults, updateSessionPresence, updatePageTitle, getActiveSearchQuery, buildSearchTimeline, removeSearchTimeline, populateCliSessionList, renderIconStrip, renderSidebarPresence, initIconStrip, getEmojiCategories, renderUserStrip, setCurrentDmUser, updateDmBadge } from './modules/sidebar.js';
|
|
4
|
+
import { initSidebar, renderSessionList, handleSearchResults, updateSessionPresence, updatePageTitle, getActiveSearchQuery, buildSearchTimeline, removeSearchTimeline, populateCliSessionList, renderIconStrip, renderSidebarPresence, initIconStrip, getEmojiCategories, renderUserStrip, setCurrentDmUser, updateDmBadge, updateSessionBadge, updateProjectBadge, closeDmUserPicker, spawnDustParticles } from './modules/sidebar.js';
|
|
5
5
|
import { initRewind, setRewindMode, showRewindModal, clearPendingRewindUuid, addRewindButton } from './modules/rewind.js';
|
|
6
6
|
import { initNotifications, showDoneNotification, playDoneSound, isNotifAlertEnabled, isNotifSoundEnabled } from './modules/notifications.js';
|
|
7
7
|
import { initInput, clearPendingImages, handleInputSync, autoResize, builtinCommands, sendMessage } from './modules/input.js';
|
|
8
8
|
import { initQrCode } from './modules/qrcode.js';
|
|
9
9
|
import { initFileBrowser, loadRootDirectory, refreshTree, handleFsList, handleFsRead, handleDirChanged, refreshIfOpen, handleFileChanged, handleFileHistory, handleGitDiff, handleFileAt, getPendingNavigate, closeFileViewer, resetFileBrowser } from './modules/filebrowser.js';
|
|
10
10
|
import { initTerminal, openTerminal, closeTerminal, resetTerminals, handleTermList, handleTermCreated, handleTermOutput, handleTermExited, handleTermClosed, sendTerminalCommand } from './modules/terminal.js';
|
|
11
|
-
import { initStickyNotes, handleNotesList, handleNoteCreated, handleNoteUpdated, handleNoteDeleted, openArchive, closeArchive, isArchiveOpen } from './modules/sticky-notes.js';
|
|
11
|
+
import { initStickyNotes, handleNotesList, handleNoteCreated, handleNoteUpdated, handleNoteDeleted, openArchive, closeArchive, isArchiveOpen, hideNotes, showNotes, isNotesVisible } from './modules/sticky-notes.js';
|
|
12
12
|
import { initTheme, getThemeColor, getComputedVar, onThemeChange, getCurrentTheme } from './modules/theme.js';
|
|
13
13
|
import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionResolved, markPermissionCancelled, renderElicitationRequest, markElicitationResolved, renderPlanBanner, renderPlanCard, handleTodoWrite, handleTaskCreate, handleTaskUpdate, startThinking, appendThinking, stopThinking, resetThinkingGroup, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, addTurnMeta, enableMainInput, getTools, getPlanContent, setPlanContent, isPlanFilePath, getTodoTools, updateSubagentActivity, addSubagentToolEntry, markSubagentDone, updateSubagentProgress, initSubagentStop, closeToolGroup, removeToolFromGroup } from './modules/tools.js';
|
|
14
14
|
import { initServerSettings, updateSettingsStats, updateSettingsModels, updateDaemonConfig, handleSetPinResult, handleKeepAwakeChanged, handleRestartResult, handleShutdownResult, handleSharedEnv, handleSharedEnvSaved, handleGlobalClaudeMdRead, handleGlobalClaudeMdWrite } from './modules/server-settings.js';
|
|
@@ -55,6 +55,9 @@ import { initAdmin, checkAdminAccess } from './modules/admin.js';
|
|
|
55
55
|
var dmTargetUser = null;
|
|
56
56
|
var dmUnread = {}; // { otherUserId: count }
|
|
57
57
|
var cachedAllUsers = [];
|
|
58
|
+
var cachedDmFavorites = [];
|
|
59
|
+
var cachedDmConversations = [];
|
|
60
|
+
var dmRemovedUsers = {}; // { userId: true } - users explicitly removed from favorites
|
|
58
61
|
|
|
59
62
|
// --- Home Hub ---
|
|
60
63
|
var homeHub = $("home-hub");
|
|
@@ -562,6 +565,9 @@ import { initAdmin, checkAdminAccess } from './modules/admin.js';
|
|
|
562
565
|
// Hide home hub if visible
|
|
563
566
|
hideHomeHub();
|
|
564
567
|
|
|
568
|
+
// Hide sticky notes if visible
|
|
569
|
+
hideNotes();
|
|
570
|
+
|
|
565
571
|
// Hide project UI + sidebar, show DM UI
|
|
566
572
|
var mainCol = document.getElementById("main-column");
|
|
567
573
|
if (mainCol) mainCol.classList.add("dm-mode");
|
|
@@ -821,12 +827,16 @@ import { initAdmin, checkAdminAccess } from './modules/admin.js';
|
|
|
821
827
|
var projectHintDismiss = $("project-hint-dismiss");
|
|
822
828
|
var cachedProjects = [];
|
|
823
829
|
var cachedProjectCount = 0;
|
|
830
|
+
var cachedRemovedProjects = [];
|
|
824
831
|
var currentProjectOwnerId = null;
|
|
825
832
|
var currentSlug = slugMatch ? slugMatch[1] : null;
|
|
826
833
|
|
|
827
834
|
function updateProjectList(msg) {
|
|
828
835
|
if (typeof msg.projectCount === "number") cachedProjectCount = msg.projectCount;
|
|
829
836
|
if (msg.projects) cachedProjects = msg.projects;
|
|
837
|
+
if (msg.removedProjects) cachedRemovedProjects = msg.removedProjects;
|
|
838
|
+
else if (msg.removedProjects === undefined) { /* keep cached */ }
|
|
839
|
+
else cachedRemovedProjects = [];
|
|
830
840
|
var count = cachedProjectCount || 0;
|
|
831
841
|
renderProjectList();
|
|
832
842
|
if (count === 1 && projectHint) {
|
|
@@ -845,8 +855,10 @@ import { initAdmin, checkAdminAccess } from './modules/admin.js';
|
|
|
845
855
|
// Update user strip (DM targets) in icon strip
|
|
846
856
|
if (msg.allUsers) {
|
|
847
857
|
cachedAllUsers = msg.allUsers;
|
|
858
|
+
if (msg.dmFavorites) cachedDmFavorites = msg.dmFavorites;
|
|
859
|
+
if (msg.dmConversations) cachedDmConversations = msg.dmConversations;
|
|
848
860
|
var onlineIds = (msg.serverUsers || []).map(function (u) { return u.id; });
|
|
849
|
-
renderUserStrip(msg.allUsers, onlineIds, myUserId);
|
|
861
|
+
renderUserStrip(msg.allUsers, onlineIds, myUserId, cachedDmFavorites, cachedDmConversations, dmUnread, dmRemovedUsers);
|
|
850
862
|
// Render my avatar (always present, hidden behind user-island)
|
|
851
863
|
var meEl = document.getElementById("icon-strip-me");
|
|
852
864
|
if (meEl && !meEl.hasChildNodes()) {
|
|
@@ -885,7 +897,7 @@ import { initAdmin, checkAdminAccess } from './modules/admin.js';
|
|
|
885
897
|
function renderProjectList() {
|
|
886
898
|
// Render icon strip projects
|
|
887
899
|
var iconStripProjects = cachedProjects.map(function (p) {
|
|
888
|
-
return { slug: p.slug, name: p.title || p.project, icon: p.icon || null, isProcessing: p.isProcessing, onlineUsers: p.onlineUsers || [] };
|
|
900
|
+
return { slug: p.slug, name: p.title || p.project, icon: p.icon || null, isProcessing: p.isProcessing, onlineUsers: p.onlineUsers || [], unread: p.unread || 0 };
|
|
889
901
|
});
|
|
890
902
|
renderIconStrip(iconStripProjects, currentSlug);
|
|
891
903
|
// Update title bar project name and icon if it changed
|
|
@@ -1225,6 +1237,8 @@ import { initAdmin, checkAdminAccess } from './modules/admin.js';
|
|
|
1225
1237
|
get projectOwnerId() { return currentProjectOwnerId; },
|
|
1226
1238
|
openDm: function (userId) { openDm(userId); },
|
|
1227
1239
|
openAddProjectModal: function () { openAddProjectModal(); },
|
|
1240
|
+
sendWs: function (msg) { if (ws && ws.readyState === 1) ws.send(JSON.stringify(msg)); },
|
|
1241
|
+
onDmRemoveUser: function (userId) { dmRemovedUsers[userId] = true; },
|
|
1228
1242
|
};
|
|
1229
1243
|
initSidebar(sidebarCtx);
|
|
1230
1244
|
initIconStrip(sidebarCtx);
|
|
@@ -2734,12 +2748,19 @@ import { initAdmin, checkAdminAccess } from './modules/admin.js';
|
|
|
2734
2748
|
// --- Project switching (no full reload) ---
|
|
2735
2749
|
function switchProject(slug) {
|
|
2736
2750
|
if (!slug) return;
|
|
2751
|
+
var wasDm = dmMode;
|
|
2737
2752
|
if (dmMode) exitDmMode();
|
|
2738
2753
|
if (homeHubVisible) {
|
|
2739
2754
|
hideHomeHub();
|
|
2740
2755
|
if (slug === currentSlug) return;
|
|
2741
2756
|
}
|
|
2742
|
-
if (slug === currentSlug)
|
|
2757
|
+
if (slug === currentSlug) {
|
|
2758
|
+
// Returning from DM mode to the same project: re-switch to restore session
|
|
2759
|
+
if (wasDm && ws && ws.readyState === 1) {
|
|
2760
|
+
ws.send(JSON.stringify({ type: "switch_session", id: activeSessionId }));
|
|
2761
|
+
}
|
|
2762
|
+
return;
|
|
2763
|
+
}
|
|
2743
2764
|
resetFileBrowser();
|
|
2744
2765
|
closeArchive();
|
|
2745
2766
|
if (isSchedulerOpen()) closeScheduler();
|
|
@@ -3126,6 +3147,10 @@ import { initAdmin, checkAdminAccess } from './modules/admin.js';
|
|
|
3126
3147
|
blinkSessionDot(msg.id);
|
|
3127
3148
|
break;
|
|
3128
3149
|
|
|
3150
|
+
case "session_unread":
|
|
3151
|
+
updateSessionBadge(msg.id, msg.count);
|
|
3152
|
+
break;
|
|
3153
|
+
|
|
3129
3154
|
case "search_results":
|
|
3130
3155
|
handleSearchResults(msg);
|
|
3131
3156
|
break;
|
|
@@ -3622,6 +3647,12 @@ import { initAdmin, checkAdminAccess } from './modules/admin.js';
|
|
|
3622
3647
|
var fromId = msg.message.from;
|
|
3623
3648
|
if (fromId && fromId !== myUserId) {
|
|
3624
3649
|
dmUnread[fromId] = (dmUnread[fromId] || 0) + 1;
|
|
3650
|
+
// Re-render strip so non-favorited sender appears
|
|
3651
|
+
var onlineIdsForDm = (cachedAllUsers || []).filter(function (u) {
|
|
3652
|
+
var el = document.querySelector('.icon-strip-user[data-user-id="' + u.id + '"]');
|
|
3653
|
+
return el && el.classList.contains("online");
|
|
3654
|
+
}).map(function (u) { return u.id; });
|
|
3655
|
+
renderUserStrip(cachedAllUsers, onlineIdsForDm, myUserId, cachedDmFavorites, cachedDmConversations, dmUnread, dmRemovedUsers);
|
|
3625
3656
|
updateDmBadge(fromId, dmUnread[fromId]);
|
|
3626
3657
|
}
|
|
3627
3658
|
}
|
|
@@ -3637,6 +3668,29 @@ import { initAdmin, checkAdminAccess } from './modules/admin.js';
|
|
|
3637
3668
|
// Could be used for DM list view later
|
|
3638
3669
|
break;
|
|
3639
3670
|
|
|
3671
|
+
case "dm_favorites_updated":
|
|
3672
|
+
// Track users explicitly removed from favorites
|
|
3673
|
+
if (cachedDmFavorites && msg.dmFavorites) {
|
|
3674
|
+
for (var ri = 0; ri < cachedDmFavorites.length; ri++) {
|
|
3675
|
+
if (msg.dmFavorites.indexOf(cachedDmFavorites[ri]) === -1) {
|
|
3676
|
+
dmRemovedUsers[cachedDmFavorites[ri]] = true;
|
|
3677
|
+
}
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
// Clear removed flag for users being added back
|
|
3681
|
+
if (msg.dmFavorites) {
|
|
3682
|
+
for (var ai = 0; ai < msg.dmFavorites.length; ai++) {
|
|
3683
|
+
delete dmRemovedUsers[msg.dmFavorites[ai]];
|
|
3684
|
+
}
|
|
3685
|
+
}
|
|
3686
|
+
cachedDmFavorites = msg.dmFavorites || [];
|
|
3687
|
+
var onlineIds2 = (cachedAllUsers || []).filter(function (u) {
|
|
3688
|
+
var el = document.querySelector('.icon-strip-user[data-user-id="' + u.id + '"]');
|
|
3689
|
+
return el && el.classList.contains("online");
|
|
3690
|
+
}).map(function (u) { return u.id; });
|
|
3691
|
+
renderUserStrip(cachedAllUsers, onlineIds2, myUserId, cachedDmFavorites, cachedDmConversations, dmUnread, dmRemovedUsers);
|
|
3692
|
+
break;
|
|
3693
|
+
|
|
3640
3694
|
case "daemon_config":
|
|
3641
3695
|
updateDaemonConfig(msg.config);
|
|
3642
3696
|
break;
|
|
@@ -5022,12 +5076,20 @@ import { initAdmin, checkAdminAccess } from './modules/admin.js';
|
|
|
5022
5076
|
// Project has tasks — show dialog with options
|
|
5023
5077
|
showRemoveProjectTaskDialog(slug, name, msg.count);
|
|
5024
5078
|
} else {
|
|
5025
|
-
// No tasks —
|
|
5026
|
-
showConfirm('Remove
|
|
5027
|
-
|
|
5028
|
-
|
|
5079
|
+
// No tasks — confirm then particle burst + remove
|
|
5080
|
+
showConfirm('Remove "' + name + '"? You can re-add it later.', function () {
|
|
5081
|
+
// Find the icon strip item to anchor the particle burst
|
|
5082
|
+
var iconEl = document.querySelector('.icon-strip-item[data-slug="' + slug + '"]');
|
|
5083
|
+
if (iconEl) {
|
|
5084
|
+
var rect = iconEl.getBoundingClientRect();
|
|
5085
|
+
spawnDustParticles(rect.left + rect.width / 2, rect.top + rect.height / 2);
|
|
5029
5086
|
}
|
|
5030
|
-
|
|
5087
|
+
setTimeout(function () {
|
|
5088
|
+
if (ws && ws.readyState === 1) {
|
|
5089
|
+
ws.send(JSON.stringify({ type: "remove_project", slug: slug }));
|
|
5090
|
+
}
|
|
5091
|
+
}, 1000);
|
|
5092
|
+
}, "Remove", true);
|
|
5031
5093
|
}
|
|
5032
5094
|
pendingRemoveSlug = null;
|
|
5033
5095
|
pendingRemoveName = null;
|
|
@@ -5092,9 +5154,33 @@ import { initAdmin, checkAdminAccess } from './modules/admin.js';
|
|
|
5092
5154
|
function handleRemoveProjectResult(msg) {
|
|
5093
5155
|
if (msg.ok) {
|
|
5094
5156
|
showToast("Project removed", "success");
|
|
5095
|
-
// If we removed the current project,
|
|
5157
|
+
// If we removed the current project, go to home hub without full reload
|
|
5096
5158
|
if (msg.slug === currentSlug) {
|
|
5097
|
-
|
|
5159
|
+
// Suppress disconnect overlay and reconnect by detaching the WS
|
|
5160
|
+
if (ws) { ws.onclose = null; ws.onerror = null; ws.close(); ws = null; }
|
|
5161
|
+
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
|
5162
|
+
connected = false;
|
|
5163
|
+
connectOverlay.classList.add("hidden");
|
|
5164
|
+
// Add to cached removed projects for re-add UI
|
|
5165
|
+
var removedProj = null;
|
|
5166
|
+
for (var ri = 0; ri < cachedProjects.length; ri++) {
|
|
5167
|
+
if (cachedProjects[ri].slug === msg.slug) { removedProj = cachedProjects[ri]; break; }
|
|
5168
|
+
}
|
|
5169
|
+
if (removedProj) {
|
|
5170
|
+
cachedRemovedProjects.push({
|
|
5171
|
+
path: removedProj.path || "",
|
|
5172
|
+
title: removedProj.title || null,
|
|
5173
|
+
icon: removedProj.icon || null,
|
|
5174
|
+
removedAt: Date.now(),
|
|
5175
|
+
});
|
|
5176
|
+
}
|
|
5177
|
+
// Remove from cached projects and re-render icon strip
|
|
5178
|
+
cachedProjects = cachedProjects.filter(function (p) { return p.slug !== msg.slug; });
|
|
5179
|
+
cachedProjectCount = cachedProjects.length;
|
|
5180
|
+
currentSlug = null;
|
|
5181
|
+
renderProjectList();
|
|
5182
|
+
resetClientState();
|
|
5183
|
+
showHomeHub();
|
|
5098
5184
|
}
|
|
5099
5185
|
} else {
|
|
5100
5186
|
showToast(msg.error || "Failed to remove project", "error");
|
|
@@ -5113,6 +5199,7 @@ import { initAdmin, checkAdminAccess } from './modules/admin.js';
|
|
|
5113
5199
|
var addProjectCancel = document.getElementById("add-project-cancel");
|
|
5114
5200
|
var addProjectModeBtns = addProjectModal.querySelectorAll(".add-project-mode-btn");
|
|
5115
5201
|
var addProjectPanels = addProjectModal.querySelectorAll(".add-project-panel");
|
|
5202
|
+
var addProjectRemoved = document.getElementById("add-project-removed");
|
|
5116
5203
|
var addProjectDebounce = null;
|
|
5117
5204
|
var addProjectActiveIdx = -1;
|
|
5118
5205
|
var addProjectMode = "existing";
|
|
@@ -5185,6 +5272,48 @@ import { initAdmin, checkAdminAccess } from './modules/admin.js';
|
|
|
5185
5272
|
existingBtn.disabled = false;
|
|
5186
5273
|
switchAddProjectMode("existing");
|
|
5187
5274
|
}
|
|
5275
|
+
// Render removed projects for re-add
|
|
5276
|
+
renderRemovedProjectsList();
|
|
5277
|
+
}
|
|
5278
|
+
|
|
5279
|
+
function renderRemovedProjectsList() {
|
|
5280
|
+
if (!addProjectRemoved) return;
|
|
5281
|
+
addProjectRemoved.innerHTML = "";
|
|
5282
|
+
if (!cachedRemovedProjects || cachedRemovedProjects.length === 0) {
|
|
5283
|
+
addProjectRemoved.classList.add("hidden");
|
|
5284
|
+
return;
|
|
5285
|
+
}
|
|
5286
|
+
addProjectRemoved.classList.remove("hidden");
|
|
5287
|
+
for (var ri = 0; ri < cachedRemovedProjects.length; ri++) {
|
|
5288
|
+
var rp = cachedRemovedProjects[ri];
|
|
5289
|
+
var item = document.createElement("div");
|
|
5290
|
+
item.className = "add-project-removed-item";
|
|
5291
|
+
item.dataset.path = rp.path;
|
|
5292
|
+
item.addEventListener("click", function () {
|
|
5293
|
+
var p = this.dataset.path;
|
|
5294
|
+
if (ws && ws.readyState === 1) {
|
|
5295
|
+
ws.send(JSON.stringify({ type: "add_project", path: p }));
|
|
5296
|
+
}
|
|
5297
|
+
closeAddProjectModal();
|
|
5298
|
+
});
|
|
5299
|
+
var iconEl = document.createElement("span");
|
|
5300
|
+
iconEl.className = "add-project-removed-icon";
|
|
5301
|
+
iconEl.textContent = rp.icon || "📁";
|
|
5302
|
+
item.appendChild(iconEl);
|
|
5303
|
+
var info = document.createElement("div");
|
|
5304
|
+
info.className = "add-project-removed-info";
|
|
5305
|
+
var nameEl = document.createElement("div");
|
|
5306
|
+
nameEl.className = "add-project-removed-name";
|
|
5307
|
+
nameEl.textContent = rp.title || rp.path.split("/").pop() || rp.path;
|
|
5308
|
+
info.appendChild(nameEl);
|
|
5309
|
+
var pathEl = document.createElement("div");
|
|
5310
|
+
pathEl.className = "add-project-removed-path";
|
|
5311
|
+
pathEl.textContent = rp.path;
|
|
5312
|
+
info.appendChild(pathEl);
|
|
5313
|
+
item.appendChild(info);
|
|
5314
|
+
addProjectRemoved.appendChild(item);
|
|
5315
|
+
}
|
|
5316
|
+
try { parseEmojis(addProjectRemoved); } catch (e) {}
|
|
5188
5317
|
}
|
|
5189
5318
|
|
|
5190
5319
|
function closeAddProjectModal() {
|
|
@@ -895,6 +895,7 @@
|
|
|
895
895
|
width: 50%;
|
|
896
896
|
max-width: 720px;
|
|
897
897
|
min-width: 360px;
|
|
898
|
+
min-height: 0;
|
|
898
899
|
border-left: 1px solid var(--border);
|
|
899
900
|
background: var(--bg);
|
|
900
901
|
display: flex;
|
|
@@ -1382,6 +1383,11 @@
|
|
|
1382
1383
|
justify-content: space-between;
|
|
1383
1384
|
padding: 10px 16px;
|
|
1384
1385
|
flex-shrink: 0;
|
|
1386
|
+
position: sticky;
|
|
1387
|
+
top: 0;
|
|
1388
|
+
z-index: 10;
|
|
1389
|
+
background: var(--bg);
|
|
1390
|
+
border-bottom: 1px solid var(--border);
|
|
1385
1391
|
}
|
|
1386
1392
|
|
|
1387
1393
|
.file-history-view-toggle {
|