clay-server 4.1.0-beta.16 → 4.1.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/lib/notes.js +20 -3
- package/lib/os-users.js +6 -0
- package/lib/project-connection.js +5 -2
- package/lib/project-http.js +34 -37
- package/lib/project-sessions.js +1 -0
- package/lib/project-user-message.js +43 -0
- package/lib/project.js +21 -0
- package/lib/public/css/issues.css +17 -3
- package/lib/public/css/sticky-notes.css +29 -0
- package/lib/public/modules/app-messages.js +5 -0
- package/lib/public/modules/scheduled-tasks.js +1 -1
- package/lib/public/modules/sticky-notes-browser.js +126 -5
- package/lib/public/modules/sticky-notes.js +2 -3
- package/lib/sdk-bridge.js +63 -19
- package/lib/sdk-message-processor.js +19 -8
- package/lib/sdk-skill-discovery.js +6 -28
- package/lib/sessions.js +8 -1
- package/lib/yoke/adapters/acp.js +3 -2
- package/lib/yoke/adapters/antigravity.js +2 -2
- package/lib/yoke/adapters/claude.js +25 -7
- package/lib/yoke/adapters/codex.js +158 -16
- package/lib/yoke/adapters/kiro.js +6 -5
- package/lib/yoke/session-skill-catalog.js +45 -0
- package/lib/yoke/skill-discovery.js +60 -20
- package/package.json +1 -1
package/lib/notes.js
CHANGED
|
@@ -39,8 +39,10 @@ function createNotesManager(opts) {
|
|
|
39
39
|
var tmpPath = notesFile + ".tmp";
|
|
40
40
|
fs.writeFileSync(tmpPath, JSON.stringify({ notes: notes }, null, 2));
|
|
41
41
|
fs.renameSync(tmpPath, notesFile);
|
|
42
|
+
return true;
|
|
42
43
|
} catch (e) {
|
|
43
44
|
console.error("[notes] Failed to save:", e.message);
|
|
45
|
+
return false;
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
48
|
|
|
@@ -139,9 +141,23 @@ function createNotesManager(opts) {
|
|
|
139
141
|
return lifecycle.closedNotes(notes);
|
|
140
142
|
}
|
|
141
143
|
|
|
142
|
-
// Permanent deletion
|
|
143
|
-
//
|
|
144
|
-
//
|
|
144
|
+
// Permanent deletion for the human-only closed-note path. Callers must still
|
|
145
|
+
// authorize the actor and recheck the lifecycle before invoking this method.
|
|
146
|
+
// The closed-state guard makes a stale confirmation harmless at storage.
|
|
147
|
+
function removeClosed(id) {
|
|
148
|
+
for (var i = 0; i < notes.length; i++) {
|
|
149
|
+
if (notes[i].id !== id || !lifecycle.isClosed(notes[i])) continue;
|
|
150
|
+
var removed = notes[i];
|
|
151
|
+
notes.splice(i, 1);
|
|
152
|
+
if (saveToDisk()) return removed;
|
|
153
|
+
notes.splice(i, 0, removed);
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Retained for maintenance callers that must truly discard a record. It is
|
|
160
|
+
// intentionally not exposed through WebSocket, MCP, or the UI.
|
|
145
161
|
function remove(id) {
|
|
146
162
|
for (var i = 0; i < notes.length; i++) {
|
|
147
163
|
if (notes[i].id === id) {
|
|
@@ -197,6 +213,7 @@ function createNotesManager(opts) {
|
|
|
197
213
|
close: close,
|
|
198
214
|
reopen: reopen,
|
|
199
215
|
remove: remove,
|
|
216
|
+
removeClosed: removeClosed,
|
|
200
217
|
bringToFront: bringToFront,
|
|
201
218
|
getActiveNotesText: getActiveNotesText,
|
|
202
219
|
};
|
package/lib/os-users.js
CHANGED
|
@@ -139,6 +139,12 @@ function fsAsUser(op, args, osUserInfo) {
|
|
|
139
139
|
"var stat = fs.statSync(f);",
|
|
140
140
|
"process.stdout.write(JSON.stringify({ size: stat.size, isDir: stat.isDirectory(), mtime: stat.mtimeMs }));",
|
|
141
141
|
].join(" ");
|
|
142
|
+
} else if (op === "realpath") {
|
|
143
|
+
script = [
|
|
144
|
+
"var fs = require('fs');",
|
|
145
|
+
"var f = " + JSON.stringify(args.file) + ";",
|
|
146
|
+
"process.stdout.write(JSON.stringify({ path: fs.realpathSync(f) }));",
|
|
147
|
+
].join(" ");
|
|
142
148
|
} else if (op === "read_binary") {
|
|
143
149
|
// Read file as base64 for binary content (images, etc.)
|
|
144
150
|
script = [
|
|
@@ -95,6 +95,7 @@ function attachConnection(ctx) {
|
|
|
95
95
|
var getLatestVersion = ctx.getLatestVersion;
|
|
96
96
|
var getTitle = ctx.getTitle;
|
|
97
97
|
var getProject = ctx.getProject;
|
|
98
|
+
var hydrateSkillCatalog = ctx.hydrateSkillCatalog || function () { return []; };
|
|
98
99
|
var warmup = ctx.warmup;
|
|
99
100
|
var scheduledMessages = ctx.scheduledMessages;
|
|
100
101
|
var sendCursorSharingState = ctx.sendCursorSharingState;
|
|
@@ -169,6 +170,7 @@ function attachConnection(ctx) {
|
|
|
169
170
|
? { active: null, storedPresence: null }
|
|
170
171
|
: findRestoredActiveSession(ws, wsUser, allSessions);
|
|
171
172
|
var restoredActive = restoredState.active;
|
|
173
|
+
if (restoredActive) hydrateSkillCatalog(restoredActive);
|
|
172
174
|
var initialVendor = (restoredActive && restoredActive.vendor) || sm.defaultVendor || "claude";
|
|
173
175
|
var initialEffort = (restoredActive && restoredActive.effort) || yoke.clampEffort(
|
|
174
176
|
initialVendor,
|
|
@@ -184,8 +186,9 @@ function attachConnection(ctx) {
|
|
|
184
186
|
// Update notifications are pushed on a scheduled interval (see
|
|
185
187
|
// scheduleUpdateBroadcast). We no longer push on connect to avoid
|
|
186
188
|
// re-triggering the banner on every page refresh.
|
|
187
|
-
|
|
188
|
-
|
|
189
|
+
var initialSlashCommands = restoredActive && (restoredActive.slashCommandsByVendor && restoredActive.slashCommandsByVendor[initialVendor] || restoredActive.slashCommands);
|
|
190
|
+
if (initialSlashCommands) {
|
|
191
|
+
sendTo(ws, { type: "slash_commands", commands: initialSlashCommands, vendor: initialVendor });
|
|
189
192
|
}
|
|
190
193
|
var initialModel = (restoredActive && restoredActive.model) || ((sm.defaultModelByVendor || {})[initialVendor]) || "";
|
|
191
194
|
// Vendor installation state is needed even before a new project has a
|
package/lib/project-http.js
CHANGED
|
@@ -7,6 +7,7 @@ var defaultFsAsUser = require("./os-users").fsAsUser;
|
|
|
7
7
|
var defaultUsersModule = require("./users");
|
|
8
8
|
var gitCli = require("./git-cli");
|
|
9
9
|
var resolveFilePath = require("./project-file-path").resolveFilePath;
|
|
10
|
+
var sharedSkillDiscovery = require("./yoke/skill-discovery");
|
|
10
11
|
|
|
11
12
|
var IMAGE_EXTS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp", ".ico"]);
|
|
12
13
|
var MIME_TYPES = {
|
|
@@ -560,44 +561,40 @@ function attachHTTP(ctx) {
|
|
|
560
561
|
// Installed skills (global + project)
|
|
561
562
|
if (req.method === "GET" && urlPath === "/api/installed-skills") {
|
|
562
563
|
var installed = {};
|
|
563
|
-
var
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
{
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
installed[ent.name] = { scope: scanDirs[sd].scope, description: desc, version: version, path: path.join(scanDirs[sd].dir, ent.name) };
|
|
593
|
-
} else {
|
|
594
|
-
// project-level adds to existing global entry
|
|
595
|
-
installed[ent.name].scope = "both";
|
|
596
|
-
if (desc && !installed[ent.name].description) installed[ent.name].description = desc;
|
|
597
|
-
if (version && !installed[ent.name].version) installed[ent.name].version = version;
|
|
564
|
+
var installedUserInfo = getOsUserInfoForReq(req);
|
|
565
|
+
if (osUsers && !installedUserInfo) {
|
|
566
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
567
|
+
res.end('{"error":"A mapped OS user is required to list skills"}');
|
|
568
|
+
return true;
|
|
569
|
+
}
|
|
570
|
+
var installedOptions = installedUserInfo
|
|
571
|
+
? sharedSkillDiscovery.optionsForOsUser(installedUserInfo, fsAsUser)
|
|
572
|
+
: { homeDir: require("./config").REAL_HOME };
|
|
573
|
+
installedOptions.includeDuplicates = true;
|
|
574
|
+
var discoveredInstalled = sharedSkillDiscovery.discoverSkills(cwd, installedOptions);
|
|
575
|
+
for (var si = 0; si < discoveredInstalled.length; si++) {
|
|
576
|
+
var discoveredSkill = discoveredInstalled[si];
|
|
577
|
+
try {
|
|
578
|
+
var desc = discoveredSkill.description || "";
|
|
579
|
+
var version = discoveredSkill.version || "";
|
|
580
|
+
var scope = discoveredSkill.source.indexOf("-project") !== -1 || discoveredSkill.source === "project" ? "project" : "global";
|
|
581
|
+
if (!installed[discoveredSkill.name]) {
|
|
582
|
+
installed[discoveredSkill.name] = { scope: scope, description: desc || discoveredSkill.description, version: version, path: path.dirname(discoveredSkill.path), source: discoveredSkill.source, sources: [discoveredSkill.source] };
|
|
583
|
+
} else {
|
|
584
|
+
// project-level adds to existing global entry
|
|
585
|
+
var priorScope = installed[discoveredSkill.name].scope;
|
|
586
|
+
installed[discoveredSkill.name].scope = priorScope === scope ? scope : "both";
|
|
587
|
+
if (installed[discoveredSkill.name].sources.indexOf(discoveredSkill.source) === -1) installed[discoveredSkill.name].sources.push(discoveredSkill.source);
|
|
588
|
+
if (scope === "project" || priorScope === "project") {
|
|
589
|
+
installed[discoveredSkill.name].path = path.dirname(discoveredSkill.path);
|
|
590
|
+
installed[discoveredSkill.name].source = discoveredSkill.source;
|
|
591
|
+
installed[discoveredSkill.name].description = desc || discoveredSkill.description;
|
|
592
|
+
installed[discoveredSkill.name].version = version;
|
|
598
593
|
}
|
|
599
|
-
|
|
600
|
-
|
|
594
|
+
if (desc && !installed[discoveredSkill.name].description) installed[discoveredSkill.name].description = desc;
|
|
595
|
+
if (version && !installed[discoveredSkill.name].version) installed[discoveredSkill.name].version = version;
|
|
596
|
+
}
|
|
597
|
+
} catch (e) {}
|
|
601
598
|
}
|
|
602
599
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
603
600
|
res.end(JSON.stringify({ installed: installed }));
|
package/lib/project-sessions.js
CHANGED
|
@@ -110,6 +110,7 @@ function attachSessions(ctx) {
|
|
|
110
110
|
var getLinuxUserForSession = ctx.getLinuxUserForSession;
|
|
111
111
|
var ensureProjectAccessForSession = ctx.ensureProjectAccessForSession;
|
|
112
112
|
var getOsUserInfoForWs = ctx.getOsUserInfoForWs;
|
|
113
|
+
var hydrateSkillCatalog = ctx.hydrateSkillCatalog || function () { return []; };
|
|
113
114
|
function stopSession(session, actor) {
|
|
114
115
|
if (!session) return false;
|
|
115
116
|
session._pendingMessageDrainPaused = true;
|
|
@@ -24,6 +24,7 @@ var fs = require("fs");
|
|
|
24
24
|
* pendingMessageQueue,
|
|
25
25
|
* loadContextSources, saveContextSources,
|
|
26
26
|
* digestDmTurn, gateMemory,
|
|
27
|
+
* getProjectAccess, canAccessProjectSlug,
|
|
27
28
|
* adapter - YOKE adapter instance
|
|
28
29
|
*/
|
|
29
30
|
function attachUserMessage(ctx) {
|
|
@@ -55,6 +56,8 @@ function attachUserMessage(ctx) {
|
|
|
55
56
|
var getLinuxUserForSession = ctx.getLinuxUserForSession;
|
|
56
57
|
var ensureProjectAccessForSession = ctx.ensureProjectAccessForSession;
|
|
57
58
|
var getOsUserInfoForWs = ctx.getOsUserInfoForWs;
|
|
59
|
+
var getProjectAccess = ctx.getProjectAccess;
|
|
60
|
+
var canAccessProjectSlug = ctx.canAccessProjectSlug;
|
|
58
61
|
|
|
59
62
|
var hydrateImageRefs = ctx.hydrateImageRefs;
|
|
60
63
|
var saveImageFile = ctx.saveImageFile;
|
|
@@ -99,6 +102,21 @@ function attachUserMessage(ctx) {
|
|
|
99
102
|
}
|
|
100
103
|
}
|
|
101
104
|
|
|
105
|
+
function canHumanManageNotes(ws) {
|
|
106
|
+
if (!usersModule.isMultiUser()) return true;
|
|
107
|
+
if (!ws || !ws._clayUser || !ws._clayUser.id) return false;
|
|
108
|
+
var actor = usersModule.findUserById(ws._clayUser.id);
|
|
109
|
+
if (!actor) return false;
|
|
110
|
+
if (typeof canAccessProjectSlug === "function" && !canAccessProjectSlug(actor.id, slug)) return false;
|
|
111
|
+
var access = typeof getProjectAccess === "function" ? getProjectAccess() : null;
|
|
112
|
+
return !!(access && !access.error && usersModule.canAccessProject(actor.id, access));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function noteDeleteResult(ws, msg, ok, error) {
|
|
116
|
+
sendTo(ws, { type: "note_delete_result", projectSlug: slug, noteId: msg.id || null,
|
|
117
|
+
requestId: msg.requestId || null, ok: !!ok, error: error || null });
|
|
118
|
+
}
|
|
119
|
+
|
|
102
120
|
// --------------- Main handler ---------------
|
|
103
121
|
|
|
104
122
|
function handleUserMessage(ws, msg, forcedSession, internal) {
|
|
@@ -171,6 +189,31 @@ function attachUserMessage(ctx) {
|
|
|
171
189
|
return true;
|
|
172
190
|
}
|
|
173
191
|
|
|
192
|
+
if (msg.type === "note_delete_permanently") {
|
|
193
|
+
if (!msg.id || !canHumanManageNotes(ws)) {
|
|
194
|
+
noteDeleteResult(ws, msg, false, "You are not authorized to delete notes in this project.");
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
var current = nm.list().filter(function (item) { return item && item.id === msg.id; })[0];
|
|
198
|
+
if (!current) {
|
|
199
|
+
noteDeleteResult(ws, msg, false, "Note not found.");
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
if (!notesLifecycle.isClosed(current)) {
|
|
203
|
+
noteDeleteResult(ws, msg, false, "Only closed notes can be permanently deleted.");
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
var removed = nm.removeClosed(msg.id);
|
|
207
|
+
if (!removed) {
|
|
208
|
+
noteDeleteResult(ws, msg, false, "The note changed before it could be deleted.");
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
send({ type: "note_deleted", id: removed.id });
|
|
212
|
+
noteDeleteResult(ws, msg, true, null);
|
|
213
|
+
syncNotesKnowledge();
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
|
|
174
217
|
if (msg.type === "note_list_request") {
|
|
175
218
|
sendTo(ws, { type: "notes_list", notes: nm.list() });
|
|
176
219
|
return true;
|
package/lib/project.js
CHANGED
|
@@ -46,6 +46,7 @@ var { attachVendorLogin } = require("./project-vendor-login");
|
|
|
46
46
|
var { attachConnection } = require("./project-connection");
|
|
47
47
|
var { attachProjectScheduledMessages } = require("./project-scheduled-messages");
|
|
48
48
|
var { attachMcp } = require("./project-mcp");
|
|
49
|
+
var { createSessionSkillCatalog } = require("./yoke/session-skill-catalog");
|
|
49
50
|
var { createLocalMcp } = require("./mcp-local");
|
|
50
51
|
var { attachEmail: attachEmailModule } = require("./project-email");
|
|
51
52
|
var { attachSessionSpawn } = require("./project-session-spawn");
|
|
@@ -1878,6 +1879,12 @@ function createProjectContext(opts) {
|
|
|
1878
1879
|
}
|
|
1879
1880
|
|
|
1880
1881
|
// --- Sessions/config/project handler (delegated to project-sessions.js) ---
|
|
1882
|
+
var _sessionSkillCatalog = createSessionSkillCatalog({
|
|
1883
|
+
cwd: cwd,
|
|
1884
|
+
osUsers: osUsers,
|
|
1885
|
+
getLinuxUser: getLinuxUserForSession,
|
|
1886
|
+
});
|
|
1887
|
+
sm.setSessionHydrator(_sessionSkillCatalog.hydrate);
|
|
1881
1888
|
var _sessions = attachSessions({
|
|
1882
1889
|
cwd: cwd,
|
|
1883
1890
|
slug: slug,
|
|
@@ -1917,6 +1924,7 @@ function createProjectContext(opts) {
|
|
|
1917
1924
|
getLinuxUserForSession: getLinuxUserForSession,
|
|
1918
1925
|
ensureProjectAccessForSession: ensureProjectAccessForSession,
|
|
1919
1926
|
getOsUserInfoForWs: getOsUserInfoForWs,
|
|
1927
|
+
hydrateSkillCatalog: _sessionSkillCatalog.hydrate,
|
|
1920
1928
|
hydrateImageRefs: hydrateImageRefs,
|
|
1921
1929
|
onProcessingChanged: onProcessingChanged,
|
|
1922
1930
|
broadcastPresence: broadcastPresence,
|
|
@@ -2046,6 +2054,8 @@ function createProjectContext(opts) {
|
|
|
2046
2054
|
getSessionForWs: getSessionForWs,
|
|
2047
2055
|
getLinuxUserForSession: getLinuxUserForSession,
|
|
2048
2056
|
ensureProjectAccessForSession: ensureProjectAccessForSession,
|
|
2057
|
+
getProjectAccess: function () { return opts.getProjectAccess ? opts.getProjectAccess() : { visibility: "public", ownerId: projectOwnerId || null }; },
|
|
2058
|
+
canAccessProjectSlug: opts.canAccessProjectSlug || function () { return !usersModule.isMultiUser(); },
|
|
2049
2059
|
getOsUserInfoForWs: getOsUserInfoForWs,
|
|
2050
2060
|
hydrateImageRefs: hydrateImageRefs,
|
|
2051
2061
|
saveImageFile: saveImageFile,
|
|
@@ -2128,6 +2138,12 @@ function createProjectContext(opts) {
|
|
|
2128
2138
|
function getMcpBridgeHandler(sessionId, sessionOnly, queryGeneration) {
|
|
2129
2139
|
var boundSession = Number.isInteger(sessionId) ? sm.sessions.get(sessionId) : null;
|
|
2130
2140
|
var queryToolBridge = createSessionQueryToolBridge({ session: boundSession, sdk: sdk });
|
|
2141
|
+
function sessionQueryValid() {
|
|
2142
|
+
return !!boundSession
|
|
2143
|
+
&& Number.isInteger(queryGeneration)
|
|
2144
|
+
&& sm.sessions.get(boundSession.localId) === boundSession
|
|
2145
|
+
&& Number(boundSession._sdkQueryGeneration || 0) === queryGeneration;
|
|
2146
|
+
}
|
|
2131
2147
|
// Build set of local MCP server names to exclude (Codex handles these natively)
|
|
2132
2148
|
var localMcpNames = {};
|
|
2133
2149
|
try {
|
|
@@ -2142,6 +2158,7 @@ function createProjectContext(opts) {
|
|
|
2142
2158
|
return {
|
|
2143
2159
|
listTools: function () {
|
|
2144
2160
|
var tools = [];
|
|
2161
|
+
if (sessionOnly && !sessionQueryValid()) return Promise.resolve(tools);
|
|
2145
2162
|
var toJSONSchema;
|
|
2146
2163
|
var zod;
|
|
2147
2164
|
try { zod = require("zod"); toJSONSchema = zod.toJSONSchema; } catch (e) { /* fallback */ }
|
|
@@ -2267,6 +2284,9 @@ function createProjectContext(opts) {
|
|
|
2267
2284
|
return Promise.resolve(tools);
|
|
2268
2285
|
},
|
|
2269
2286
|
callTool: function (serverName, toolName, args) {
|
|
2287
|
+
if (sessionOnly && !sessionQueryValid()) {
|
|
2288
|
+
return Promise.reject(new Error("Session tool unavailable or older query: " + serverName + "/" + toolName));
|
|
2289
|
+
}
|
|
2270
2290
|
var queryCall = queryToolBridge.callTool(queryGeneration, serverName, toolName, args);
|
|
2271
2291
|
if (queryCall) return queryCall;
|
|
2272
2292
|
if (boundSession && serverName === "clay-sessions") {
|
|
@@ -2425,6 +2445,7 @@ function createProjectContext(opts) {
|
|
|
2425
2445
|
getLatestVersion: function () { return latestVersion; },
|
|
2426
2446
|
getTitle: function () { return title; },
|
|
2427
2447
|
getProject: function () { return project; },
|
|
2448
|
+
hydrateSkillCatalog: _sessionSkillCatalog.hydrate,
|
|
2428
2449
|
// Exposed so the first websocket connection can lazily warm up the
|
|
2429
2450
|
// adapters for this project (see project-connection handleConnection).
|
|
2430
2451
|
warmup: function (linuxUser) {
|
|
@@ -1,7 +1,20 @@
|
|
|
1
|
-
#issues-panel { position: relative; width:
|
|
1
|
+
#issues-panel { position: relative; width: 50%; max-width: 720px; min-width: 360px; height: calc(100% - 16px); margin: 8px 8px 8px 10px; min-height: 0; align-self: center; display: flex; flex-direction: column; background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; flex-shrink: 0; }
|
|
2
|
+
@media (min-width: 1024px) {
|
|
3
|
+
#issues-panel {
|
|
4
|
+
box-shadow:
|
|
5
|
+
4px 8px 18px rgba(var(--shadow-rgb), 0.14),
|
|
6
|
+
2px 2px 4px rgba(var(--shadow-rgb), 0.08),
|
|
7
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
|
8
|
+
animation: workbench-panel-in 0.18s cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
9
|
+
transition: max-width 0.2s ease, width 0.2s ease, box-shadow 0.2s ease;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
@media (prefers-reduced-motion: reduce) {
|
|
13
|
+
#issues-panel { animation: none; }
|
|
14
|
+
}
|
|
2
15
|
#issues-panel.hidden { display: none; }
|
|
3
16
|
#issues-panel.issues-wide { width: 70%; max-width: 1200px; }
|
|
4
|
-
#issues-panel.panel-fullscreen { width: 100%; max-width: none; height: 100%; margin: 0; }
|
|
17
|
+
#issues-panel.panel-fullscreen { width: 100%; max-width: none; min-width: 0; height: 100%; margin: 0; border: 0; border-radius: 0; box-shadow: none; animation: none; }
|
|
5
18
|
#issues-panel header { height: 48px; box-sizing: border-box; display: flex; align-items: center; gap: 12px; padding: 0 16px; border-bottom: 1px solid var(--border-subtle); flex: 0 0 auto; }
|
|
6
19
|
#issues-panel .issues-title { display: inline-flex; align-items: center; gap: 7px; font-weight: 700; }
|
|
7
20
|
#issues-panel .issues-title .lucide { width: 15px; color: var(--accent); }
|
|
@@ -79,7 +92,8 @@
|
|
|
79
92
|
.issues-content > h2 { margin: 0 0 10px; font-family: var(--font-display); font-size: 14px; line-height: 1.2; }
|
|
80
93
|
.issues-history-note { margin: 0; padding: 14px 18px; border-bottom: 1px solid var(--border-subtle); color: var(--text-dimmer); font-size: 11px; font-family: var(--font-mono); }
|
|
81
94
|
.clayos-issue-link { display: inline-flex; max-width: 100%; border: 1px solid var(--border); border-radius: 5px; background: var(--bg); color: inherit; font: inherit; font-size: .85em; padding: 2px 7px; cursor: pointer; vertical-align: baseline; overflow-wrap: anywhere; }
|
|
82
|
-
@media (max-width: 1023px) { #issues-panel, #issues-panel.issues-wide { position: fixed; inset: 0; z-index:
|
|
95
|
+
@media (max-width: 1023px) { #issues-panel, #issues-panel.issues-wide { position: fixed; inset: 0; z-index: 300; width: 100%; max-width: none; min-width: 0; height: 100%; margin: 0; border: 0; border-radius: 0; padding-top: var(--safe-top, 0px); padding-bottom: var(--safe-bottom, 0px); box-sizing: border-box; } #issues-panel button { min-height: 36px; } }
|
|
96
|
+
@media (max-width: 1023px) { #issues-panel [data-issues-wide], #issues-panel [data-issues-full] { display: none; } }
|
|
83
97
|
@media (max-width: 390px) {
|
|
84
98
|
#issues-panel header { gap: 4px; padding: 8px; }
|
|
85
99
|
#issues-panel header button { padding: 7px; }
|
|
@@ -764,6 +764,35 @@
|
|
|
764
764
|
.notes-browser-card-action:hover { background: var(--bg-alt); color: var(--text); }
|
|
765
765
|
.notes-browser-card-action:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
766
766
|
|
|
767
|
+
.notes-delete-dialog {
|
|
768
|
+
position: absolute;
|
|
769
|
+
inset: 0;
|
|
770
|
+
z-index: 2;
|
|
771
|
+
display: flex;
|
|
772
|
+
align-items: center;
|
|
773
|
+
justify-content: center;
|
|
774
|
+
padding: 24px;
|
|
775
|
+
background: rgba(var(--shadow-rgb), 0.28);
|
|
776
|
+
}
|
|
777
|
+
.notes-delete-dialog.hidden { display: none; }
|
|
778
|
+
.notes-delete-dialog-card {
|
|
779
|
+
width: min(420px, 100%);
|
|
780
|
+
padding: 20px;
|
|
781
|
+
border: 1px solid var(--border);
|
|
782
|
+
border-radius: 10px;
|
|
783
|
+
background: var(--bg);
|
|
784
|
+
box-shadow: 0 12px 36px rgba(var(--shadow-rgb), 0.24);
|
|
785
|
+
}
|
|
786
|
+
.notes-delete-dialog-card h2 { margin: 0 0 10px; font-size: 16px; }
|
|
787
|
+
.notes-delete-dialog-card p { margin: 8px 0; color: var(--text-secondary); font-size: 12px; }
|
|
788
|
+
.notes-delete-dialog-title { font-weight: 700; color: var(--text) !important; overflow-wrap: anywhere; }
|
|
789
|
+
.notes-delete-dialog-status { min-height: 18px; }
|
|
790
|
+
.notes-delete-dialog-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }
|
|
791
|
+
.notes-delete-dialog-actions button { padding: 7px 12px; border: 1px solid var(--border); border-radius: 6px; background: var(--bg); color: var(--text); font: inherit; cursor: pointer; }
|
|
792
|
+
.notes-delete-dialog-actions button.danger { background: var(--danger, #b42318); border-color: var(--danger, #b42318); color: #fff; }
|
|
793
|
+
.notes-delete-dialog-actions button:disabled { opacity: 0.55; cursor: default; }
|
|
794
|
+
.notes-delete-dialog-actions button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
795
|
+
|
|
767
796
|
.notes-browser-card-body {
|
|
768
797
|
flex: 1;
|
|
769
798
|
font-size: 11.5px;
|
|
@@ -46,6 +46,7 @@ import { tuiModalHandleTermOutput, tuiModalHandleTermResized, tuiModalHandleTerm
|
|
|
46
46
|
import { updateTerminalList, handleContextSourcesState, updateEmailAccountList, updateEmailUnreadCounts, handleEmailTestResult, handleEmailAddResult, handleEmailRemoveResult } from './context-sources.js';
|
|
47
47
|
import { refreshEmailSettings } from './user-settings.js';
|
|
48
48
|
import { handleNotesList, handleNoteCreated, handleNoteUpdated, handleNoteDeleted, handleNoteWritten } from './sticky-notes.js';
|
|
49
|
+
import { handleNoteDeleteResult } from './sticky-notes-browser.js';
|
|
49
50
|
import { handleProjectLogsState, handleProjectLogEntry, handleProjectLogCommented, handleProjectLogCommentReviewed, handleProjectLogDeleted, handleProjectLogUpdated, handleProjectLogsError, handleScheduledTaskResultsState, handleScheduledTaskResultCreated, handleScheduledTaskResultSession } from './project-logs.js';
|
|
50
51
|
import { handleSkillInstalled, handleSkillUninstalled } from './skills.js';
|
|
51
52
|
import { showRewindModal, onRewindComplete, setRewindMode, onRewindError, clearPendingRewindUuid, addRewindButton } from './rewind.js';
|
|
@@ -1646,6 +1647,10 @@ export function processMessage(msg) {
|
|
|
1646
1647
|
handleNoteDeleted(msg);
|
|
1647
1648
|
break;
|
|
1648
1649
|
|
|
1650
|
+
case "note_delete_result":
|
|
1651
|
+
handleNoteDeleteResult(msg);
|
|
1652
|
+
break;
|
|
1653
|
+
|
|
1649
1654
|
case "project_logs_state":
|
|
1650
1655
|
handleProjectLogsState(msg);
|
|
1651
1656
|
break;
|
|
@@ -257,7 +257,7 @@ function handleClick(event) {
|
|
|
257
257
|
var button = event.target.closest("button[data-action]");
|
|
258
258
|
if (!button) return;
|
|
259
259
|
var action = button.dataset.action;
|
|
260
|
-
if (action === "default-ai") { openDefaultAi(); return; }
|
|
260
|
+
if (action === "default-ai") { event.stopPropagation(); openDefaultAi(); return; }
|
|
261
261
|
if (action === "close") { closeScheduledTasks(); return; }
|
|
262
262
|
if (action === "wide") { applyWindowState(!store.get('scheduledTasksWide'), store.get('scheduledTasksFullscreen')); return; }
|
|
263
263
|
if (action === "fullscreen") { applyWindowState(store.get('scheduledTasksWide'), !store.get('scheduledTasksFullscreen')); return; }
|
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
// the canvas over the conversation; this pane lists them, splits them into Open
|
|
5
5
|
// and Closed, and is where a note is completed or brought back.
|
|
6
6
|
//
|
|
7
|
-
// Completion here is Close
|
|
8
|
-
//
|
|
9
|
-
// There is deliberately no destructive control on this surface at all.
|
|
7
|
+
// Completion here is Close. Permanent deletion is available only for closed
|
|
8
|
+
// notes and always requires an explicit, accessible confirmation.
|
|
10
9
|
|
|
11
10
|
import { store } from './store.js';
|
|
12
11
|
import { iconHtml, refreshIcons } from './icons.js';
|
|
@@ -19,6 +18,10 @@ import { claimRightWorkbench, registerRightWorkbench, releaseRightWorkbench } fr
|
|
|
19
18
|
var panel = null;
|
|
20
19
|
var gridEl = null;
|
|
21
20
|
var tabsEl = null;
|
|
21
|
+
var deleteDialog = null;
|
|
22
|
+
var deleteTrigger = null;
|
|
23
|
+
var deleteRequestCounter = 0;
|
|
24
|
+
var deleteRequestTimer = null;
|
|
22
25
|
|
|
23
26
|
// Registered by app.js so this module never imports the other right-pane tools
|
|
24
27
|
// and cannot form an import cycle with them.
|
|
@@ -63,11 +66,15 @@ function ensurePanel() {
|
|
|
63
66
|
'</div>' +
|
|
64
67
|
'</header>' +
|
|
65
68
|
'<div id="notes-browser-tabs" class="notes-browser-tabs" role="tablist" aria-label="Sticky Notes state"></div>' +
|
|
66
|
-
'<div id="notes-browser-grid" class="notes-browser-grid" role="list"></div>'
|
|
69
|
+
'<div id="notes-browser-grid" class="notes-browser-grid" role="list"></div>' +
|
|
70
|
+
'<div id="notes-delete-dialog" class="notes-delete-dialog hidden" role="dialog" aria-modal="true" aria-labelledby="notes-delete-heading">' +
|
|
71
|
+
'<div class="notes-delete-dialog-card"><h2 id="notes-delete-heading">Delete this note permanently?</h2><p>This permanently removes the note. It cannot be restored.</p><p class="notes-delete-dialog-title"></p><p class="notes-delete-dialog-status" role="status"></p><div class="notes-delete-dialog-actions"><button type="button" data-action="cancel">Cancel</button><button type="button" class="danger" data-action="delete">Delete permanently</button></div></div>' +
|
|
72
|
+
'</div>';
|
|
67
73
|
panels.appendChild(panel);
|
|
68
74
|
|
|
69
75
|
gridEl = panel.querySelector("#notes-browser-grid");
|
|
70
76
|
tabsEl = panel.querySelector("#notes-browser-tabs");
|
|
77
|
+
deleteDialog = panel.querySelector("#notes-delete-dialog");
|
|
71
78
|
|
|
72
79
|
panel.querySelector("#notes-browser-close").addEventListener("click", closeNotesBrowser);
|
|
73
80
|
panel.querySelector("#notes-browser-wide").addEventListener("click", function () {
|
|
@@ -76,6 +83,29 @@ function ensurePanel() {
|
|
|
76
83
|
panel.querySelector("#notes-browser-fullscreen").addEventListener("click", function () {
|
|
77
84
|
applyWindowState(store.get('notesBrowserWide'), !store.get('notesBrowserFullscreen'));
|
|
78
85
|
});
|
|
86
|
+
deleteDialog.querySelector('[data-action="cancel"]').addEventListener("click", closeDeleteDialog);
|
|
87
|
+
deleteDialog.querySelector('[data-action="delete"]').addEventListener("click", confirmDelete);
|
|
88
|
+
deleteDialog.addEventListener("click", function (event) { if (event.target === deleteDialog) closeDeleteDialog(); });
|
|
89
|
+
deleteDialog.addEventListener("keydown", function (event) {
|
|
90
|
+
if (event.key === "Escape") {
|
|
91
|
+
event.preventDefault();
|
|
92
|
+
event.stopPropagation();
|
|
93
|
+
closeDeleteDialog(true);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (event.key !== "Tab") return;
|
|
97
|
+
var focusable = deleteDialog.querySelectorAll("button:not(:disabled), [href], input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex='-1'])");
|
|
98
|
+
if (!focusable.length) return;
|
|
99
|
+
var first = focusable[0];
|
|
100
|
+
var last = focusable[focusable.length - 1];
|
|
101
|
+
if (event.shiftKey && document.activeElement === first) {
|
|
102
|
+
event.preventDefault();
|
|
103
|
+
last.focus();
|
|
104
|
+
} else if (!event.shiftKey && document.activeElement === last) {
|
|
105
|
+
event.preventDefault();
|
|
106
|
+
first.focus();
|
|
107
|
+
}
|
|
108
|
+
});
|
|
79
109
|
refreshIcons();
|
|
80
110
|
}
|
|
81
111
|
|
|
@@ -166,7 +196,8 @@ function buildCard(data) {
|
|
|
166
196
|
title.textContent = getTitle(data.text) || "Untitled";
|
|
167
197
|
header.appendChild(title);
|
|
168
198
|
|
|
169
|
-
//
|
|
199
|
+
// Closing and reopening are lifecycle actions; permanent deletion is offered
|
|
200
|
+
// separately below only because this card is known to be closed.
|
|
170
201
|
var action = document.createElement("button");
|
|
171
202
|
action.type = "button";
|
|
172
203
|
action.className = "notes-browser-card-action";
|
|
@@ -200,6 +231,14 @@ function buildCard(data) {
|
|
|
200
231
|
card.appendChild(body);
|
|
201
232
|
|
|
202
233
|
if (closed) {
|
|
234
|
+
var deleteAction = document.createElement("button");
|
|
235
|
+
deleteAction.type = "button";
|
|
236
|
+
deleteAction.className = "notes-browser-card-action notes-browser-card-delete";
|
|
237
|
+
deleteAction.innerHTML = iconHtml("trash-2") + "<span>Delete permanently</span>";
|
|
238
|
+
deleteAction.title = "Permanently delete this closed note";
|
|
239
|
+
deleteAction.setAttribute("aria-label", "Delete permanently: " + title.textContent);
|
|
240
|
+
deleteAction.addEventListener("click", function (e) { e.stopPropagation(); openDeleteDialog(data, deleteAction); });
|
|
241
|
+
header.appendChild(deleteAction);
|
|
203
242
|
var meta = document.createElement("p");
|
|
204
243
|
meta.className = "notes-browser-card-meta";
|
|
205
244
|
meta.textContent = closedMeta(data);
|
|
@@ -224,6 +263,77 @@ function buildCard(data) {
|
|
|
224
263
|
return card;
|
|
225
264
|
}
|
|
226
265
|
|
|
266
|
+
function nextDeleteRequestId() {
|
|
267
|
+
deleteRequestCounter++;
|
|
268
|
+
return "delete-note-" + Date.now() + "-" + deleteRequestCounter;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function accountKey() {
|
|
272
|
+
return store.get('myUserId') ? String(store.get('myUserId')) : "single";
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function openDeleteDialog(data, trigger) {
|
|
276
|
+
if (!deleteDialog || !data || !isClosed(data)) return;
|
|
277
|
+
deleteTrigger = trigger || null;
|
|
278
|
+
store.set({ notesDeletePending: { noteId: data.id, requestId: null, projectSlug: store.get('currentSlug') || null, accountKey: accountKey() } });
|
|
279
|
+
deleteDialog.querySelector(".notes-delete-dialog-title").textContent = getTitle(data.text) || "Untitled";
|
|
280
|
+
deleteDialog.querySelector(".notes-delete-dialog-status").textContent = "";
|
|
281
|
+
deleteDialog.querySelector('[data-action="delete"]').disabled = false;
|
|
282
|
+
deleteDialog.querySelector('[data-action="cancel"]').disabled = false;
|
|
283
|
+
deleteDialog.classList.remove("hidden");
|
|
284
|
+
deleteDialog.querySelector('[data-action="cancel"]').focus();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function closeDeleteDialog(force) {
|
|
288
|
+
var pending = store.get('notesDeletePending');
|
|
289
|
+
if (!force && pending && pending.requestId) return;
|
|
290
|
+
if (deleteRequestTimer) { clearTimeout(deleteRequestTimer); deleteRequestTimer = null; }
|
|
291
|
+
if (deleteDialog) deleteDialog.classList.add("hidden");
|
|
292
|
+
store.set({ notesDeletePending: null });
|
|
293
|
+
if (deleteTrigger && deleteTrigger.isConnected) deleteTrigger.focus();
|
|
294
|
+
deleteTrigger = null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function confirmDelete() {
|
|
298
|
+
var pending = store.get('notesDeletePending');
|
|
299
|
+
if (!pending || !deleteDialog) return;
|
|
300
|
+
if (!store.get('connected')) {
|
|
301
|
+
deleteDialog.querySelector(".notes-delete-dialog-status").textContent = "Sticky Notes are unavailable while disconnected.";
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
var requestId = nextDeleteRequestId();
|
|
305
|
+
store.set({ notesDeletePending: Object.assign({}, pending, { requestId: requestId }) });
|
|
306
|
+
deleteDialog.querySelector('[data-action="delete"]').disabled = true;
|
|
307
|
+
deleteDialog.querySelector('[data-action="cancel"]').disabled = true;
|
|
308
|
+
deleteDialog.querySelector(".notes-delete-dialog-status").textContent = "Deleting permanently...";
|
|
309
|
+
send({ type: "note_delete_permanently", id: pending.noteId, requestId: requestId });
|
|
310
|
+
deleteRequestTimer = setTimeout(function () {
|
|
311
|
+
var active = store.get('notesDeletePending');
|
|
312
|
+
if (!deleteDialog || !active || active.requestId !== requestId) return;
|
|
313
|
+
store.set({ notesDeletePending: Object.assign({}, active, { requestId: null }) });
|
|
314
|
+
deleteDialog.querySelector('[data-action="delete"]').disabled = false;
|
|
315
|
+
deleteDialog.querySelector('[data-action="cancel"]').disabled = false;
|
|
316
|
+
deleteDialog.querySelector(".notes-delete-dialog-status").textContent = "The delete request timed out. Try again.";
|
|
317
|
+
deleteRequestTimer = null;
|
|
318
|
+
}, 10000);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function handleNoteDeleteResult(msg) {
|
|
322
|
+
var pending = store.get('notesDeletePending');
|
|
323
|
+
if (!deleteDialog || !msg || msg.projectSlug !== store.get('currentSlug') ||
|
|
324
|
+
msg.requestId !== (pending && pending.requestId) || !pending || msg.noteId !== pending.noteId ||
|
|
325
|
+
msg.projectSlug !== pending.projectSlug || pending.accountKey !== accountKey()) return;
|
|
326
|
+
if (deleteRequestTimer) { clearTimeout(deleteRequestTimer); deleteRequestTimer = null; }
|
|
327
|
+
if (msg.ok) {
|
|
328
|
+
closeDeleteDialog(true);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
store.set({ notesDeletePending: Object.assign({}, pending, { requestId: null }) });
|
|
332
|
+
deleteDialog.querySelector('[data-action="delete"]').disabled = false;
|
|
333
|
+
deleteDialog.querySelector('[data-action="cancel"]').disabled = false;
|
|
334
|
+
deleteDialog.querySelector(".notes-delete-dialog-status").textContent = msg.error || "The note could not be deleted.";
|
|
335
|
+
}
|
|
336
|
+
|
|
227
337
|
function emptyState(tab) {
|
|
228
338
|
var empty = document.createElement("div");
|
|
229
339
|
empty.className = "notes-browser-empty";
|
|
@@ -242,6 +352,11 @@ function emptyState(tab) {
|
|
|
242
352
|
export function renderNotesBrowser() {
|
|
243
353
|
if (!gridEl) return;
|
|
244
354
|
var all = listNoteData();
|
|
355
|
+
var pending = store.get('notesDeletePending');
|
|
356
|
+
if (pending) {
|
|
357
|
+
var pendingCurrent = all.filter(function (item) { return item && item.id === pending.noteId; })[0];
|
|
358
|
+
if (!pendingCurrent || !isClosed(pendingCurrent)) closeDeleteDialog(true);
|
|
359
|
+
}
|
|
245
360
|
var open = openNotes(all);
|
|
246
361
|
var closed = closedNotes(all);
|
|
247
362
|
renderTabs(open.length, closed.length);
|
|
@@ -284,6 +399,9 @@ export function openNotesBrowser() {
|
|
|
284
399
|
}
|
|
285
400
|
|
|
286
401
|
export function closeNotesBrowser() {
|
|
402
|
+
// Reset destructive UI state even when the panel was already closed. Right
|
|
403
|
+
// workbench/project transitions can race with panel visibility updates.
|
|
404
|
+
closeDeleteDialog(true);
|
|
287
405
|
releaseRightWorkbench("notes-browser");
|
|
288
406
|
if (!store.get('notesBrowserOpen')) return;
|
|
289
407
|
if (panel) panel.classList.add("hidden");
|
|
@@ -304,10 +422,13 @@ export function initNotesBrowser() {
|
|
|
304
422
|
registerRightWorkbench("notes-browser", closeNotesBrowser);
|
|
305
423
|
document.addEventListener("keydown", function (e) {
|
|
306
424
|
if (e.key !== "Escape") return;
|
|
425
|
+
if (deleteDialog && !deleteDialog.classList.contains("hidden")) return;
|
|
307
426
|
if (!store.get('notesBrowserOpen')) return;
|
|
308
427
|
closeNotesBrowser();
|
|
309
428
|
});
|
|
310
429
|
store.subscribe(function (state, previous) {
|
|
430
|
+
if (state.connected !== previous.connected || state.myUserId !== previous.myUserId ||
|
|
431
|
+
state.isMultiUserMode !== previous.isMultiUserMode) closeDeleteDialog(true);
|
|
311
432
|
if (state.currentSlug === previous.currentSlug) return;
|
|
312
433
|
// A different project starts from a closed, bounded, Open-tab pane.
|
|
313
434
|
closeNotesBrowser();
|