clay-server 2.32.0 → 2.32.1-beta.2
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/daemon.js +18 -0
- package/lib/project-email.js +16 -0
- package/lib/project.js +38 -8
- package/lib/public/app.js +11 -0
- package/lib/public/css/command-palette.css +186 -0
- package/lib/public/css/filebrowser.css +73 -49
- package/lib/public/css/icon-strip.css +44 -2
- package/lib/public/css/input.css +10 -5
- package/lib/public/css/mates.css +18 -29
- package/lib/public/css/notifications-center.css +32 -0
- package/lib/public/css/overlays.css +5 -22
- package/lib/public/css/sidebar.css +288 -27
- package/lib/public/index.html +24 -23
- package/lib/public/modules/app-notifications.js +52 -0
- package/lib/public/modules/app-rendering.js +1 -1
- package/lib/public/modules/filebrowser.js +171 -0
- package/lib/public/modules/input.js +12 -11
- package/lib/public/modules/markdown.js +12 -0
- package/lib/public/modules/project-switcher.js +404 -0
- package/lib/public/modules/sidebar.js +14 -10
- package/lib/public/modules/terminal.js +8 -3
- package/lib/public/modules/tool-palette.js +561 -0
- package/lib/sdk-bridge.js +20 -7
- package/lib/server-settings.js +53 -0
- package/lib/sessions.js +4 -2
- package/lib/users-preferences.js +48 -0
- package/lib/users.js +4 -0
- package/lib/yoke/index.js +4 -3
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { closeArchive } from './sticky-notes.js';
|
|
2
2
|
import { closeScheduler } from './scheduler.js';
|
|
3
3
|
import { initSidebarSessions } from './sidebar-sessions.js';
|
|
4
|
+
import { focusFileTree } from './filebrowser.js';
|
|
4
5
|
import { initSidebarProjects, closeProjectCtxMenu } from './sidebar-projects.js';
|
|
5
6
|
import {
|
|
6
7
|
initSidebarMates,
|
|
@@ -108,15 +109,6 @@ export function initSidebar(_ctx) {
|
|
|
108
109
|
});
|
|
109
110
|
}
|
|
110
111
|
|
|
111
|
-
// --- Tools section collapse/expand ---
|
|
112
|
-
var toolsToggle = ctx.$("sidebar-tools-toggle");
|
|
113
|
-
var toolsSection = ctx.$("sidebar-tools");
|
|
114
|
-
if (toolsToggle && toolsSection) {
|
|
115
|
-
toolsToggle.addEventListener("click", function () {
|
|
116
|
-
toolsSection.classList.toggle("collapsed");
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
112
|
// --- Panel switch (sessions / files / projects) ---
|
|
121
113
|
var fileBrowserBtn = ctx.$("file-browser-btn");
|
|
122
114
|
var projectsPanel = ctx.$("sidebar-panel-projects");
|
|
@@ -151,6 +143,9 @@ export function initSidebar(_ctx) {
|
|
|
151
143
|
filesPanel.classList.add("fb-enter");
|
|
152
144
|
}
|
|
153
145
|
if (ctx.onFilesTabOpen) ctx.onFilesTabOpen();
|
|
146
|
+
// Hand focus to the file tree so arrow keys work immediately
|
|
147
|
+
// after the panel opens (no click-to-focus extra step).
|
|
148
|
+
requestAnimationFrame(function () { focusFileTree(); });
|
|
154
149
|
}
|
|
155
150
|
|
|
156
151
|
function hideFilesPanel(cb) {
|
|
@@ -167,7 +162,16 @@ export function initSidebar(_ctx) {
|
|
|
167
162
|
}
|
|
168
163
|
|
|
169
164
|
if (fileBrowserBtn) {
|
|
170
|
-
|
|
165
|
+
// Clicking the tile toggles: open if closed, close (back to sessions)
|
|
166
|
+
// if it's already the visible panel. Same behavior via the Cmd+O
|
|
167
|
+
// hotkey since that calls .click().
|
|
168
|
+
fileBrowserBtn.addEventListener("click", function () {
|
|
169
|
+
if (filesPanel && !filesPanel.classList.contains("hidden")) {
|
|
170
|
+
hideFilesPanel(function () { showSessionsPanel(); });
|
|
171
|
+
} else {
|
|
172
|
+
showFilesPanel();
|
|
173
|
+
}
|
|
174
|
+
});
|
|
171
175
|
}
|
|
172
176
|
if (filePanelClose) {
|
|
173
177
|
filePanelClose.addEventListener("click", function() {
|
|
@@ -164,12 +164,17 @@ export function initTerminal(_ctx) {
|
|
|
164
164
|
});
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
// Sidebar terminal button
|
|
167
|
+
// Sidebar terminal button — toggles open/close. Second click (or the
|
|
168
|
+
// Cmd+O hotkey on the same tile) dismisses the terminal panel.
|
|
168
169
|
var sidebarTermBtn = document.getElementById("terminal-sidebar-btn");
|
|
169
170
|
if (sidebarTermBtn) {
|
|
170
171
|
sidebarTermBtn.addEventListener("click", function () {
|
|
171
|
-
|
|
172
|
-
|
|
172
|
+
if (isOpen && !ctx.terminalContainerEl.classList.contains("hidden")) {
|
|
173
|
+
closeTerminal();
|
|
174
|
+
} else {
|
|
175
|
+
closeSidebar();
|
|
176
|
+
openTerminal();
|
|
177
|
+
}
|
|
173
178
|
});
|
|
174
179
|
}
|
|
175
180
|
|
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
// tool-palette.js — Customizable sidebar tool palette
|
|
2
|
+
//
|
|
3
|
+
// Renders the per-session and per-mate tool grids from a data registry,
|
|
4
|
+
// so users can reorder tools and hide ones they don't use. Preserves the
|
|
5
|
+
// original button IDs so existing click handlers (attached by sidebar.js,
|
|
6
|
+
// terminal.js, mcp-ui.js, etc.) keep working on the rendered nodes.
|
|
7
|
+
//
|
|
8
|
+
// Design notes (see issue #325 for the broader context):
|
|
9
|
+
// - Buttons are created once on init and never destroyed; hide/reorder
|
|
10
|
+
// moves existing DOM nodes, so event listeners bound elsewhere survive.
|
|
11
|
+
// - Edit mode is a state on the palette container (class="edit-mode").
|
|
12
|
+
// CSS shows the × remove affordance and enables drag reordering.
|
|
13
|
+
// - Preferences persist per-user via the /api/user/tool-palettes endpoint.
|
|
14
|
+
// An in-flight save is debounced so rapid drag reorders don't spam.
|
|
15
|
+
|
|
16
|
+
import { refreshIcons } from './icons.js';
|
|
17
|
+
|
|
18
|
+
// Registry order = default order for users who haven't customized. Users
|
|
19
|
+
// with saved preferences keep their own order (applyPreferences uses
|
|
20
|
+
// saved order first, then appends any registry entries the user's saved
|
|
21
|
+
// list doesn't mention). So changes here only affect fresh palettes.
|
|
22
|
+
var SESSION_TOOLS = [
|
|
23
|
+
{ id: "file-browser-btn", icon: "folder-tree", label: "File browser" },
|
|
24
|
+
{ id: "terminal-sidebar-btn", icon: "square-terminal", label: "Terminal", countId: "terminal-sidebar-count" },
|
|
25
|
+
{ id: "sticky-notes-sidebar-btn", icon: "sticky-note", label: "Sticky Notes", countId: "sticky-notes-sidebar-count" },
|
|
26
|
+
{ id: "scheduler-btn", icon: "calendar-clock", label: "Scheduled Tasks" },
|
|
27
|
+
{ id: "email-sidebar-btn", icon: "mail", label: "Email" },
|
|
28
|
+
{ id: "mcp-btn", icon: "cable", label: "MCP Servers", countId: "mcp-sidebar-count" },
|
|
29
|
+
{ id: "skills-btn", icon: "puzzle", label: "Skills" },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
var MATE_TOOLS = [
|
|
33
|
+
{ id: "mate-memory-btn", icon: "brain", label: "Memory", countId: "mate-memory-count" },
|
|
34
|
+
{ id: "mate-knowledge-btn", icon: "book-open", label: "Knowledge", countId: "mate-knowledge-count" },
|
|
35
|
+
{ id: "mate-sticky-notes-btn", icon: "sticky-note", label: "Sticky Notes" },
|
|
36
|
+
{ id: "mate-email-btn", icon: "mail", label: "Email" },
|
|
37
|
+
{ id: "mate-mcp-btn", icon: "cable", label: "MCP Servers", countId: "mate-mcp-sidebar-count" },
|
|
38
|
+
{ id: "mate-skills-btn", icon: "puzzle", label: "Skills" },
|
|
39
|
+
{ id: "mate-scheduler-btn", icon: "calendar-clock", label: "Scheduled Tasks" },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
var PALETTES = {
|
|
43
|
+
session: {
|
|
44
|
+
tools: SESSION_TOOLS,
|
|
45
|
+
activeContainerId: "session-actions",
|
|
46
|
+
hiddenSectionId: "session-actions-hidden",
|
|
47
|
+
},
|
|
48
|
+
mate: {
|
|
49
|
+
tools: MATE_TOOLS,
|
|
50
|
+
activeContainerId: "mate-sidebar-tools",
|
|
51
|
+
hiddenSectionId: "mate-sidebar-tools-hidden",
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
var _saveTimers = {};
|
|
56
|
+
var _draggingEl = null;
|
|
57
|
+
var _draggingPaletteName = null;
|
|
58
|
+
|
|
59
|
+
export function initToolPalettes() {
|
|
60
|
+
for (var name in PALETTES) {
|
|
61
|
+
buildPalette(name);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Edit pills toggle edit mode on their associated palette.
|
|
65
|
+
var editBtns = document.querySelectorAll('.tool-palette-edit-btn');
|
|
66
|
+
for (var i = 0; i < editBtns.length; i++) {
|
|
67
|
+
(function (btn) {
|
|
68
|
+
btn.addEventListener('click', function () {
|
|
69
|
+
toggleEditMode(btn.dataset.palette);
|
|
70
|
+
});
|
|
71
|
+
})(editBtns[i]);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Keyboard-hotkey hint pill. Matches the icon-strip shortcut pills
|
|
75
|
+
// in shape — just the chord, no prose. Clicking the pill opens the
|
|
76
|
+
// hotkey overlay, same as pressing the shortcut.
|
|
77
|
+
var isMac = /Mac|iPhone|iPod|iPad/i.test(navigator.platform);
|
|
78
|
+
var hintLabel = isMac ? '\u2318O' : 'Ctrl+O';
|
|
79
|
+
var hints = document.querySelectorAll('.sidebar-tools-hint');
|
|
80
|
+
for (var h = 0; h < hints.length; h++) {
|
|
81
|
+
hints[h].textContent = hintLabel;
|
|
82
|
+
hints[h].title = 'Show keyboard hotkeys (' + hintLabel + ')';
|
|
83
|
+
hints[h].addEventListener('click', function (e) {
|
|
84
|
+
// Stop the click from bubbling to the document-level dismiss
|
|
85
|
+
// handler below, which would immediately tear down the pick-mode
|
|
86
|
+
// we just entered.
|
|
87
|
+
e.stopPropagation();
|
|
88
|
+
if (_pickActive) exitToolPickMode();
|
|
89
|
+
else enterToolPickMode();
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Load saved preferences and apply to both palettes once buttons exist.
|
|
94
|
+
loadPreferences();
|
|
95
|
+
|
|
96
|
+
refreshIcons();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function buildPalette(name) {
|
|
100
|
+
var palette = PALETTES[name];
|
|
101
|
+
var active = document.getElementById(palette.activeContainerId);
|
|
102
|
+
if (!active) return;
|
|
103
|
+
|
|
104
|
+
// Create buttons with stable IDs and append in registry order (default).
|
|
105
|
+
for (var i = 0; i < palette.tools.length; i++) {
|
|
106
|
+
active.appendChild(buildToolButton(palette.tools[i], name));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Scaffold the hidden section (its grid fills in when items are hidden).
|
|
110
|
+
var hiddenSection = document.getElementById(palette.hiddenSectionId);
|
|
111
|
+
if (hiddenSection) {
|
|
112
|
+
hiddenSection.innerHTML = '';
|
|
113
|
+
var labelEl = document.createElement('div');
|
|
114
|
+
labelEl.className = 'tool-palette-hidden-label';
|
|
115
|
+
labelEl.textContent = 'Add back';
|
|
116
|
+
var gridEl = document.createElement('div');
|
|
117
|
+
gridEl.className = 'tool-palette-hidden-grid';
|
|
118
|
+
hiddenSection.appendChild(labelEl);
|
|
119
|
+
hiddenSection.appendChild(gridEl);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Drag-reorder within the active container. Works in both normal and
|
|
123
|
+
// edit mode so users can rearrange without entering a separate mode
|
|
124
|
+
// (matches the macOS dock pattern). HTML5 drag has a small movement
|
|
125
|
+
// threshold, so quick clicks still activate the tool normally.
|
|
126
|
+
active.addEventListener('dragover', function (e) {
|
|
127
|
+
if (_draggingPaletteName !== name || !_draggingEl) return;
|
|
128
|
+
e.preventDefault();
|
|
129
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
|
|
130
|
+
var after = getDragAfterElement(active, e.clientX, e.clientY);
|
|
131
|
+
if (after == null) {
|
|
132
|
+
if (_draggingEl.parentNode !== active || _draggingEl.nextSibling) {
|
|
133
|
+
active.appendChild(_draggingEl);
|
|
134
|
+
}
|
|
135
|
+
} else if (after !== _draggingEl) {
|
|
136
|
+
active.insertBefore(_draggingEl, after);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
active.addEventListener('drop', function (e) {
|
|
140
|
+
if (_draggingPaletteName !== name) return;
|
|
141
|
+
e.preventDefault();
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function buildToolButton(tool, paletteName) {
|
|
146
|
+
var btn = document.createElement('button');
|
|
147
|
+
btn.id = tool.id;
|
|
148
|
+
btn.type = 'button';
|
|
149
|
+
btn.className = 'palette-tile';
|
|
150
|
+
btn.title = tool.label;
|
|
151
|
+
btn.setAttribute('aria-label', tool.label);
|
|
152
|
+
btn.dataset.toolId = tool.id;
|
|
153
|
+
btn.dataset.palette = paletteName;
|
|
154
|
+
// Draggable at all times so users can reorder without entering edit
|
|
155
|
+
// mode. Drag is gated to the active grid in the handlers below; tiles
|
|
156
|
+
// in the hidden grid get draggable=false set on move.
|
|
157
|
+
btn.draggable = true;
|
|
158
|
+
|
|
159
|
+
var icon = document.createElement('i');
|
|
160
|
+
icon.setAttribute('data-lucide', tool.icon);
|
|
161
|
+
btn.appendChild(icon);
|
|
162
|
+
|
|
163
|
+
var labelEl = document.createElement('span');
|
|
164
|
+
labelEl.className = 'tool-btn-label';
|
|
165
|
+
labelEl.textContent = tool.label;
|
|
166
|
+
btn.appendChild(labelEl);
|
|
167
|
+
|
|
168
|
+
if (tool.countId) {
|
|
169
|
+
var count = document.createElement('span');
|
|
170
|
+
count.id = tool.countId;
|
|
171
|
+
count.className = 'sidebar-badge hidden';
|
|
172
|
+
btn.appendChild(count);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// × affordance — shown only in edit mode via CSS.
|
|
176
|
+
var remove = document.createElement('span');
|
|
177
|
+
remove.className = 'tool-palette-remove';
|
|
178
|
+
remove.setAttribute('role', 'button');
|
|
179
|
+
remove.setAttribute('aria-label', 'Remove ' + tool.label + ' from palette');
|
|
180
|
+
remove.textContent = '\u00D7';
|
|
181
|
+
btn.appendChild(remove);
|
|
182
|
+
|
|
183
|
+
// Click interceptor, registered before any other module attaches to
|
|
184
|
+
// the button by ID. stopImmediatePropagation prevents the tool's own
|
|
185
|
+
// handler from running in three cases: clicking the × in edit mode,
|
|
186
|
+
// clicking a tile in the hidden-grid (which should restore it), and
|
|
187
|
+
// clicking any tile while edit mode is active (tiles are inert then).
|
|
188
|
+
btn.addEventListener('click', function (e) {
|
|
189
|
+
if (e.target.closest('.tool-palette-remove')) {
|
|
190
|
+
e.preventDefault();
|
|
191
|
+
e.stopImmediatePropagation();
|
|
192
|
+
moveToHidden(paletteName, tool.id);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
var container = btn.parentNode;
|
|
196
|
+
if (!container) return;
|
|
197
|
+
if (container.classList.contains('tool-palette-hidden-grid')) {
|
|
198
|
+
e.preventDefault();
|
|
199
|
+
e.stopImmediatePropagation();
|
|
200
|
+
moveToActive(paletteName, tool.id);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (container.classList.contains('edit-mode')) {
|
|
204
|
+
e.preventDefault();
|
|
205
|
+
e.stopImmediatePropagation();
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
btn.addEventListener('contextmenu', function (e) {
|
|
210
|
+
e.preventDefault();
|
|
211
|
+
e.stopPropagation();
|
|
212
|
+
var container = btn.parentNode;
|
|
213
|
+
var inHidden = container && container.classList.contains('tool-palette-hidden-grid');
|
|
214
|
+
openPaletteContextMenu(e.clientX, e.clientY, inHidden ? [
|
|
215
|
+
{ label: 'Add back to palette', action: function () { moveToActive(paletteName, tool.id); } },
|
|
216
|
+
] : [
|
|
217
|
+
{ label: 'Remove from palette', action: function () { moveToHidden(paletteName, tool.id); } },
|
|
218
|
+
{ label: 'Edit palette\u2026', action: function () {
|
|
219
|
+
if (!document.getElementById(PALETTES[paletteName].activeContainerId).classList.contains('edit-mode')) {
|
|
220
|
+
toggleEditMode(paletteName);
|
|
221
|
+
}
|
|
222
|
+
} },
|
|
223
|
+
]);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
btn.addEventListener('dragstart', function (e) {
|
|
227
|
+
var container = btn.parentNode;
|
|
228
|
+
// Only allow drag from the active grid; hidden tiles are added back
|
|
229
|
+
// via click, not drag.
|
|
230
|
+
if (!container || container.id !== PALETTES[paletteName].activeContainerId) {
|
|
231
|
+
e.preventDefault();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
_draggingEl = btn;
|
|
235
|
+
_draggingPaletteName = paletteName;
|
|
236
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
237
|
+
// Firefox requires data to be set for drag to start.
|
|
238
|
+
try { e.dataTransfer.setData('text/plain', tool.id); } catch (err) { /* ignore */ }
|
|
239
|
+
btn.classList.add('dragging');
|
|
240
|
+
});
|
|
241
|
+
btn.addEventListener('dragend', function () {
|
|
242
|
+
btn.classList.remove('dragging');
|
|
243
|
+
if (_draggingPaletteName) queueSave(_draggingPaletteName);
|
|
244
|
+
_draggingEl = null;
|
|
245
|
+
_draggingPaletteName = null;
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
return btn;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function toggleEditMode(name) {
|
|
252
|
+
var palette = PALETTES[name];
|
|
253
|
+
if (!palette) return;
|
|
254
|
+
var active = document.getElementById(palette.activeContainerId);
|
|
255
|
+
var hidden = document.getElementById(palette.hiddenSectionId);
|
|
256
|
+
var editBtn = document.querySelector('.tool-palette-edit-btn[data-palette="' + name + '"]');
|
|
257
|
+
if (!active || !editBtn) return;
|
|
258
|
+
|
|
259
|
+
var entering = !active.classList.contains('edit-mode');
|
|
260
|
+
active.classList.toggle('edit-mode', entering);
|
|
261
|
+
if (hidden) {
|
|
262
|
+
// Show hidden-section in edit mode if any items exist there (or to
|
|
263
|
+
// allow adding back even when empty, keep visible so the state is
|
|
264
|
+
// legible). We show whenever edit mode is on.
|
|
265
|
+
hidden.classList.toggle('hidden', !entering);
|
|
266
|
+
}
|
|
267
|
+
// Pencil icon stays; the .active class provides visual feedback by
|
|
268
|
+
// swapping to the accent background. No label text swap needed now.
|
|
269
|
+
editBtn.classList.toggle('active', entering);
|
|
270
|
+
|
|
271
|
+
if (!entering) queueSave(name);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function moveToHidden(name, toolId) {
|
|
275
|
+
var palette = PALETTES[name];
|
|
276
|
+
if (!palette) return;
|
|
277
|
+
var btn = document.getElementById(toolId);
|
|
278
|
+
var hiddenSection = document.getElementById(palette.hiddenSectionId);
|
|
279
|
+
if (!btn || !hiddenSection) return;
|
|
280
|
+
var grid = hiddenSection.querySelector('.tool-palette-hidden-grid');
|
|
281
|
+
if (!grid) return;
|
|
282
|
+
// Hidden tiles stay draggable=false so users don't drag them around
|
|
283
|
+
// before adding them back. The active-grid dragstart gate also
|
|
284
|
+
// enforces this.
|
|
285
|
+
btn.draggable = false;
|
|
286
|
+
grid.appendChild(btn);
|
|
287
|
+
queueSave(name);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function moveToActive(name, toolId) {
|
|
291
|
+
var palette = PALETTES[name];
|
|
292
|
+
if (!palette) return;
|
|
293
|
+
var btn = document.getElementById(toolId);
|
|
294
|
+
var active = document.getElementById(palette.activeContainerId);
|
|
295
|
+
if (!btn || !active) return;
|
|
296
|
+
active.appendChild(btn);
|
|
297
|
+
btn.draggable = true;
|
|
298
|
+
queueSave(name);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// (Hidden-tile clicks are handled in each button's own click listener,
|
|
302
|
+
// attached in buildToolButton before any external module attaches
|
|
303
|
+
// handlers by ID. See the stopImmediatePropagation logic there.)
|
|
304
|
+
|
|
305
|
+
// --- Right-click context menu ---
|
|
306
|
+
|
|
307
|
+
var _openMenu = null;
|
|
308
|
+
|
|
309
|
+
function openPaletteContextMenu(x, y, items) {
|
|
310
|
+
closePaletteContextMenu();
|
|
311
|
+
var menu = document.createElement('div');
|
|
312
|
+
menu.className = 'tool-palette-ctx-menu';
|
|
313
|
+
for (var i = 0; i < items.length; i++) {
|
|
314
|
+
(function (item) {
|
|
315
|
+
var btn = document.createElement('button');
|
|
316
|
+
btn.type = 'button';
|
|
317
|
+
btn.className = 'tool-palette-ctx-item';
|
|
318
|
+
btn.textContent = item.label;
|
|
319
|
+
btn.addEventListener('click', function (e) {
|
|
320
|
+
e.stopPropagation();
|
|
321
|
+
closePaletteContextMenu();
|
|
322
|
+
item.action();
|
|
323
|
+
});
|
|
324
|
+
menu.appendChild(btn);
|
|
325
|
+
})(items[i]);
|
|
326
|
+
}
|
|
327
|
+
document.body.appendChild(menu);
|
|
328
|
+
// Clamp to viewport.
|
|
329
|
+
var rect = menu.getBoundingClientRect();
|
|
330
|
+
var px = x, py = y;
|
|
331
|
+
if (px + rect.width > window.innerWidth - 4) px = window.innerWidth - rect.width - 4;
|
|
332
|
+
if (py + rect.height > window.innerHeight - 4) py = window.innerHeight - rect.height - 4;
|
|
333
|
+
menu.style.left = px + 'px';
|
|
334
|
+
menu.style.top = py + 'px';
|
|
335
|
+
_openMenu = menu;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function closePaletteContextMenu() {
|
|
339
|
+
if (_openMenu && _openMenu.parentNode) _openMenu.parentNode.removeChild(_openMenu);
|
|
340
|
+
_openMenu = null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
document.addEventListener('click', function () { closePaletteContextMenu(); });
|
|
344
|
+
document.addEventListener('keydown', function (e) {
|
|
345
|
+
if (e.key === 'Escape') closePaletteContextMenu();
|
|
346
|
+
});
|
|
347
|
+
window.addEventListener('blur', function () { closePaletteContextMenu(); });
|
|
348
|
+
window.addEventListener('resize', function () { closePaletteContextMenu(); });
|
|
349
|
+
|
|
350
|
+
// --- Hotkey overlay (Cmd/Ctrl + O) ---
|
|
351
|
+
//
|
|
352
|
+
// Press Cmd/Ctrl + O to reveal a hotkey badge on each visible palette
|
|
353
|
+
// tile. Then press the shown digit (1..9, 0) or letter (a..z) to
|
|
354
|
+
// activate that tool. Escape or clicking away dismisses. Inspired by
|
|
355
|
+
// Vimium / Superhuman-style "pick a tile with one keystroke" flows.
|
|
356
|
+
|
|
357
|
+
var _pickActive = false;
|
|
358
|
+
var HOTKEYS = (function () {
|
|
359
|
+
var k = [];
|
|
360
|
+
for (var i = 1; i <= 9; i++) k.push(String(i));
|
|
361
|
+
k.push('0');
|
|
362
|
+
for (var c = 97; c <= 122; c++) k.push(String.fromCharCode(c));
|
|
363
|
+
return k;
|
|
364
|
+
})();
|
|
365
|
+
|
|
366
|
+
function getVisiblePaletteName() {
|
|
367
|
+
// offsetParent is null when the element (or an ancestor) is hidden.
|
|
368
|
+
var mateActive = document.getElementById('mate-sidebar-tools');
|
|
369
|
+
if (mateActive && mateActive.offsetParent !== null) return 'mate';
|
|
370
|
+
var sessionActive = document.getElementById('session-actions');
|
|
371
|
+
if (sessionActive && sessionActive.offsetParent !== null) return 'session';
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function enterToolPickMode() {
|
|
376
|
+
if (_pickActive) return;
|
|
377
|
+
var name = getVisiblePaletteName();
|
|
378
|
+
if (!name) return;
|
|
379
|
+
var active = document.getElementById(PALETTES[name].activeContainerId);
|
|
380
|
+
if (!active) return;
|
|
381
|
+
var tiles = active.querySelectorAll('[data-tool-id]');
|
|
382
|
+
if (tiles.length === 0) return;
|
|
383
|
+
for (var i = 0; i < tiles.length && i < HOTKEYS.length; i++) {
|
|
384
|
+
var key = HOTKEYS[i];
|
|
385
|
+
tiles[i].dataset.hotkey = key;
|
|
386
|
+
var badge = document.createElement('span');
|
|
387
|
+
badge.className = 'tool-palette-hotkey-badge';
|
|
388
|
+
badge.textContent = key.toUpperCase();
|
|
389
|
+
tiles[i].appendChild(badge);
|
|
390
|
+
}
|
|
391
|
+
_pickActive = true;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function exitToolPickMode() {
|
|
395
|
+
if (!_pickActive) return;
|
|
396
|
+
_pickActive = false;
|
|
397
|
+
var badges = document.querySelectorAll('.tool-palette-hotkey-badge');
|
|
398
|
+
for (var i = 0; i < badges.length; i++) {
|
|
399
|
+
if (badges[i].parentNode) badges[i].parentNode.removeChild(badges[i]);
|
|
400
|
+
}
|
|
401
|
+
var marked = document.querySelectorAll('[data-hotkey]');
|
|
402
|
+
for (var j = 0; j < marked.length; j++) {
|
|
403
|
+
delete marked[j].dataset.hotkey;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Capture phase so we reliably preempt typing and browser shortcuts
|
|
408
|
+
// when focus is in an input.
|
|
409
|
+
document.addEventListener('keydown', function (e) {
|
|
410
|
+
// Pick mode: consume the next printable key.
|
|
411
|
+
if (_pickActive) {
|
|
412
|
+
if (e.key === 'Escape') {
|
|
413
|
+
e.preventDefault();
|
|
414
|
+
exitToolPickMode();
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
// Ignore modifier-only keys; react to single-character keys.
|
|
418
|
+
if (e.key.length !== 1) return;
|
|
419
|
+
if (e.ctrlKey || e.metaKey || e.altKey) return;
|
|
420
|
+
var key = e.key.toLowerCase();
|
|
421
|
+
var tile = document.querySelector('[data-hotkey="' + key + '"]');
|
|
422
|
+
if (tile) {
|
|
423
|
+
e.preventDefault();
|
|
424
|
+
exitToolPickMode();
|
|
425
|
+
tile.click();
|
|
426
|
+
} else {
|
|
427
|
+
// Unknown key exits pick mode without triggering a tile.
|
|
428
|
+
exitToolPickMode();
|
|
429
|
+
}
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Enter pick mode on Cmd/Ctrl + O (letter O). Intercepted globally
|
|
434
|
+
// including inside text inputs — the browser's native Cmd+O is a
|
|
435
|
+
// file-picker dialog Clay never wants, so taking it over costs the
|
|
436
|
+
// user nothing and is especially useful when jumping to a tool from
|
|
437
|
+
// the session search box.
|
|
438
|
+
if ((e.metaKey || e.ctrlKey) && !e.altKey && !e.shiftKey && e.key && e.key.toLowerCase() === 'o') {
|
|
439
|
+
if (!getVisiblePaletteName()) return;
|
|
440
|
+
e.preventDefault();
|
|
441
|
+
enterToolPickMode();
|
|
442
|
+
}
|
|
443
|
+
}, true);
|
|
444
|
+
|
|
445
|
+
// Dismiss the overlay on click-away, blur, or viewport changes so it
|
|
446
|
+
// never lingers invisibly.
|
|
447
|
+
document.addEventListener('click', function () { if (_pickActive) exitToolPickMode(); });
|
|
448
|
+
window.addEventListener('blur', function () { if (_pickActive) exitToolPickMode(); });
|
|
449
|
+
window.addEventListener('resize', function () { if (_pickActive) exitToolPickMode(); });
|
|
450
|
+
|
|
451
|
+
function getDragAfterElement(container, x, y) {
|
|
452
|
+
var tiles = Array.prototype.slice.call(
|
|
453
|
+
container.querySelectorAll('[data-tool-id]:not(.dragging)')
|
|
454
|
+
);
|
|
455
|
+
var closest = null;
|
|
456
|
+
var closestDist = Number.POSITIVE_INFINITY;
|
|
457
|
+
for (var i = 0; i < tiles.length; i++) {
|
|
458
|
+
var rect = tiles[i].getBoundingClientRect();
|
|
459
|
+
// Pointer is "before" the tile if it's left of the tile's horizontal
|
|
460
|
+
// midpoint on the same row (or on a higher row).
|
|
461
|
+
var rowDelta = y - (rect.top + rect.height / 2);
|
|
462
|
+
var colDelta = x - (rect.left + rect.width / 2);
|
|
463
|
+
// Weight row heavier than column so flowing between rows works.
|
|
464
|
+
var dist = Math.abs(rowDelta) * 2 + Math.abs(colDelta);
|
|
465
|
+
var before = rowDelta < -rect.height / 2
|
|
466
|
+
|| (Math.abs(rowDelta) <= rect.height / 2 && colDelta < 0);
|
|
467
|
+
if (before && dist < closestDist) {
|
|
468
|
+
closestDist = dist;
|
|
469
|
+
closest = tiles[i];
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return closest;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// --- Persistence ---
|
|
476
|
+
|
|
477
|
+
function loadPreferences() {
|
|
478
|
+
fetch('/api/user/tool-palettes', { credentials: 'same-origin' })
|
|
479
|
+
.then(function (res) { return res.ok ? res.json() : null; })
|
|
480
|
+
.then(function (data) {
|
|
481
|
+
if (!data) return;
|
|
482
|
+
applyPreferences('session', data.session || null);
|
|
483
|
+
applyPreferences('mate', data.mate || null);
|
|
484
|
+
})
|
|
485
|
+
.catch(function () { /* first-time users have no saved prefs; ignore */ });
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function applyPreferences(name, prefs) {
|
|
489
|
+
if (!prefs) return;
|
|
490
|
+
var palette = PALETTES[name];
|
|
491
|
+
if (!palette) return;
|
|
492
|
+
var active = document.getElementById(palette.activeContainerId);
|
|
493
|
+
var hiddenSection = document.getElementById(palette.hiddenSectionId);
|
|
494
|
+
var hiddenGrid = hiddenSection ? hiddenSection.querySelector('.tool-palette-hidden-grid') : null;
|
|
495
|
+
if (!active || !hiddenGrid) return;
|
|
496
|
+
|
|
497
|
+
var hiddenSet = {};
|
|
498
|
+
var hiddenList = prefs.hidden || [];
|
|
499
|
+
for (var i = 0; i < hiddenList.length; i++) hiddenSet[hiddenList[i]] = true;
|
|
500
|
+
|
|
501
|
+
var orderList = prefs.order || [];
|
|
502
|
+
// Any tools not in the stored order are appended in registry order so
|
|
503
|
+
// newly-added tools surface automatically for existing users.
|
|
504
|
+
var placed = {};
|
|
505
|
+
for (var j = 0; j < orderList.length; j++) {
|
|
506
|
+
var id = orderList[j];
|
|
507
|
+
if (hiddenSet[id]) continue;
|
|
508
|
+
var btn = document.getElementById(id);
|
|
509
|
+
if (btn) {
|
|
510
|
+
active.appendChild(btn);
|
|
511
|
+
placed[id] = true;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
for (var k = 0; k < palette.tools.length; k++) {
|
|
515
|
+
var tid = palette.tools[k].id;
|
|
516
|
+
if (placed[tid] || hiddenSet[tid]) continue;
|
|
517
|
+
var tbtn = document.getElementById(tid);
|
|
518
|
+
if (tbtn) active.appendChild(tbtn);
|
|
519
|
+
}
|
|
520
|
+
for (var h = 0; h < hiddenList.length; h++) {
|
|
521
|
+
var hbtn = document.getElementById(hiddenList[h]);
|
|
522
|
+
if (hbtn) {
|
|
523
|
+
hbtn.draggable = false;
|
|
524
|
+
hiddenGrid.appendChild(hbtn);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function queueSave(name) {
|
|
530
|
+
if (_saveTimers[name]) clearTimeout(_saveTimers[name]);
|
|
531
|
+
_saveTimers[name] = setTimeout(function () {
|
|
532
|
+
_saveTimers[name] = null;
|
|
533
|
+
savePreferences(name);
|
|
534
|
+
}, 250);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function savePreferences(name) {
|
|
538
|
+
var palette = PALETTES[name];
|
|
539
|
+
if (!palette) return;
|
|
540
|
+
var active = document.getElementById(palette.activeContainerId);
|
|
541
|
+
var hiddenSection = document.getElementById(palette.hiddenSectionId);
|
|
542
|
+
var hiddenGrid = hiddenSection ? hiddenSection.querySelector('.tool-palette-hidden-grid') : null;
|
|
543
|
+
if (!active) return;
|
|
544
|
+
|
|
545
|
+
var order = [];
|
|
546
|
+
var activeTiles = active.querySelectorAll('[data-tool-id]');
|
|
547
|
+
for (var i = 0; i < activeTiles.length; i++) order.push(activeTiles[i].dataset.toolId);
|
|
548
|
+
|
|
549
|
+
var hidden = [];
|
|
550
|
+
if (hiddenGrid) {
|
|
551
|
+
var hiddenTiles = hiddenGrid.querySelectorAll('[data-tool-id]');
|
|
552
|
+
for (var j = 0; j < hiddenTiles.length; j++) hidden.push(hiddenTiles[j].dataset.toolId);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
fetch('/api/user/tool-palettes', {
|
|
556
|
+
method: 'PUT',
|
|
557
|
+
credentials: 'same-origin',
|
|
558
|
+
headers: { 'Content-Type': 'application/json' },
|
|
559
|
+
body: JSON.stringify({ palette: name, order: order, hidden: hidden }),
|
|
560
|
+
}).catch(function () { /* save best-effort */ });
|
|
561
|
+
}
|