openclacky 1.5.7 → 1.5.9
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +71 -0
- data/README.md +3 -3
- data/README_CN.md +2 -2
- data/README_JA.md +2 -2
- data/lib/clacky/agent.rb +2 -3
- data/lib/clacky/agent_config.rb +40 -18
- data/lib/clacky/brand_config.rb +12 -7
- data/lib/clacky/cli.rb +0 -1
- data/lib/clacky/default_extensions/ext-studio/ext.yml +1 -1
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +27 -14
- data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +22 -5
- data/lib/clacky/default_extensions/general/ext.yml +1 -1
- data/lib/clacky/extension/api_loader.rb +4 -11
- data/lib/clacky/extension/packager.rb +1 -1
- data/lib/clacky/extension/scaffold/templates/hello/test/handler_test.rb.erb +22 -0
- data/lib/clacky/extension/verifier.rb +1 -1
- data/lib/clacky/media/generator.rb +3 -3
- data/lib/clacky/message_format/anthropic.rb +19 -2
- data/lib/clacky/message_format/bedrock.rb +8 -3
- data/lib/clacky/message_history.rb +1 -1
- data/lib/clacky/providers.rb +159 -105
- data/lib/clacky/server/channel/adapters/feishu/ws_client.rb +6 -0
- data/lib/clacky/server/http_server.rb +67 -79
- data/lib/clacky/session_manager.rb +7 -0
- data/lib/clacky/skill_loader.rb +4 -0
- data/lib/clacky/tools/base.rb +11 -1
- data/lib/clacky/tools/edit.rb +28 -0
- data/lib/clacky/tools/terminal.rb +5 -0
- data/lib/clacky/tools/trash_manager.rb +1 -1
- data/lib/clacky/ui2/components/inline_input.rb +1 -0
- data/lib/clacky/ui2/components/input_area.rb +1 -0
- data/lib/clacky/ui2/components/modal_component.rb +0 -1
- data/lib/clacky/ui2/components/todo_area.rb +1 -0
- data/lib/clacky/ui2/line_editor.rb +1 -0
- data/lib/clacky/ui2/markdown_renderer.rb +5 -1
- data/lib/clacky/ui2/ui_controller.rb +1 -0
- data/lib/clacky/utils/model_pricing.rb +62 -27
- data/lib/clacky/utils/string_matcher.rb +29 -2
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +278 -221
- data/lib/clacky/web/components/chat-navigator.js +1 -1
- data/lib/clacky/web/features/billing/store.js +3 -3
- data/lib/clacky/web/features/billing/view.js +5 -61
- data/lib/clacky/web/features/extensions/store.js +65 -41
- data/lib/clacky/web/features/extensions/view.js +74 -10
- data/lib/clacky/web/features/new-session/view.js +24 -0
- data/lib/clacky/web/features/version/view.js +2 -2
- data/lib/clacky/web/i18n.js +36 -22
- data/lib/clacky/web/index.html +17 -17
- data/lib/clacky/web/projects.js +83 -285
- data/lib/clacky/web/sessions.js +151 -17
- data/lib/clacky/web/settings.js +6 -1
- metadata +2 -1
data/lib/clacky/web/projects.js
CHANGED
|
@@ -48,36 +48,47 @@ const Projects = (() => {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
// ── Shared icon helper ────────────────────────────────────────────────────
|
|
51
|
-
// Reads SVG path from the picker popup DOM and forces a consistent size.
|
|
52
|
-
// Falls back to a hard-coded folder icon when the popup isn't in the DOM yet.
|
|
53
51
|
const _FOLDER_PATH = `<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>`;
|
|
54
52
|
const _FOLDER_OPEN_PATH = `<path d="m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2"/>`;
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
|
|
54
|
+
const _ICON_PATHS = {
|
|
55
|
+
folder: _FOLDER_PATH,
|
|
56
|
+
code: `<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>`,
|
|
57
|
+
book: `<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>`,
|
|
58
|
+
globe: `<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>`,
|
|
59
|
+
zap: `<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>`,
|
|
60
|
+
star: `<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>`,
|
|
61
|
+
heart: `<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>`,
|
|
62
|
+
tool: `<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>`,
|
|
63
|
+
database: `<ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/>`,
|
|
64
|
+
image: `<rect x="3" y="3" width="16" height="16" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/>`,
|
|
65
|
+
music: `<path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/>`,
|
|
66
|
+
coffee: `<path d="M18 8h1a4 4 0 0 1 0 8h-1"/><path d="M2 8h16v9a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V8z"/><line x1="6" y1="1" x2="6" y2="4"/><line x1="10" y1="1" x2="10" y2="4"/><line x1="14" y1="1" x2="14" y2="4"/>`,
|
|
67
|
+
briefcase: `<rect x="2" y="7" width="20" height="14" rx="2" ry="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/>`,
|
|
68
|
+
flask: `<path d="M9 3h6"/><path d="M10 3v6l-4 8a1 1 0 0 0 .9 1.5h10.2a1 1 0 0 0 .9-1.5L14 9V3"/>`,
|
|
69
|
+
chart: `<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/>`,
|
|
70
|
+
rocket: `<path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/>`,
|
|
71
|
+
dollar: `<line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>`,
|
|
72
|
+
cpu: `<rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><line x1="9" y1="1" x2="9" y2="4"/><line x1="15" y1="1" x2="15" y2="4"/><line x1="9" y1="20" x2="9" y2="23"/><line x1="15" y1="20" x2="15" y2="23"/><line x1="20" y1="9" x2="23" y2="9"/><line x1="20" y1="14" x2="23" y2="14"/><line x1="1" y1="9" x2="4" y2="9"/><line x1="1" y1="14" x2="4" y2="14"/>`,
|
|
73
|
+
paint: `<circle cx="13.5" cy="6.5" r=".5"/><circle cx="17.5" cy="10.5" r=".5"/><circle cx="8.5" cy="7.5" r=".5"/><circle cx="6.5" cy="12.5" r=".5"/><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z"/>`,
|
|
74
|
+
leaf: `<path d="M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10z"/><path d="M2 21c0-3 1.85-5.36 5.08-6C9.5 14.52 12 13 13 12"/>`,
|
|
75
|
+
trophy: `<polyline points="8 21 8 14 4 9 4 4 20 4 20 9 16 14 16 21"/><line x1="8" y1="21" x2="16" y2="21"/><path d="M4 9H2a1 1 0 0 0-1 1 6 6 0 0 0 6 6"/><path d="M20 9h2a1 1 0 0 1 1 1 6 6 0 0 1-6 6"/>`,
|
|
76
|
+
smile: `<circle cx="12" cy="12" r="10"/><path d="M8 14s1.5 2 4 2 4-2 4-2"/><line x1="9" y1="9" x2="9.01" y2="9"/><line x1="15" y1="9" x2="15.01" y2="9"/>`,
|
|
77
|
+
map: `<polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/><line x1="8" y1="2" x2="8" y2="18"/><line x1="16" y1="6" x2="16" y2="22"/>`,
|
|
78
|
+
flower: `<path d="M12 7.5a4.5 4.5 0 1 1 4.5 4.5M12 7.5A4.5 4.5 0 1 0 7.5 12M12 7.5V9m-4.5 3a4.5 4.5 0 1 0 4.5 4.5M7.5 12H9m7.5 0a4.5 4.5 0 1 1-4.5 4.5m4.5-4.5H15m-3 4.5V15"/><circle cx="12" cy="12" r="3"/>`,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
function _getIconSvg(iconName, size = 15.5) {
|
|
57
82
|
if (iconName === "folder-open") {
|
|
58
83
|
return `<svg viewBox="0 0 24 24" width="${size}" height="${size}" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${_FOLDER_OPEN_PATH}</svg>`;
|
|
59
84
|
}
|
|
60
|
-
const
|
|
61
|
-
let inner = null;
|
|
62
|
-
if (popup) {
|
|
63
|
-
const btn = popup.querySelector(`[data-icon="${iconName}"]`);
|
|
64
|
-
if (btn) {
|
|
65
|
-
// Clone the SVG and override size attrs
|
|
66
|
-
const svg = btn.querySelector("svg");
|
|
67
|
-
if (svg) inner = svg.innerHTML;
|
|
68
|
-
}
|
|
69
|
-
if (!inner) {
|
|
70
|
-
const first = popup.querySelector("[data-icon] svg");
|
|
71
|
-
if (first) inner = first.innerHTML;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
if (!inner) inner = _FOLDER_PATH;
|
|
85
|
+
const inner = _ICON_PATHS[iconName] || _FOLDER_PATH;
|
|
75
86
|
return `<svg viewBox="0 0 24 24" width="${size}" height="${size}" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${inner}</svg>`;
|
|
76
87
|
}
|
|
77
88
|
|
|
78
89
|
// ── State accessors ────────────────────────────────────────────────────────
|
|
79
90
|
|
|
80
|
-
function all() { return _projects; }
|
|
91
|
+
function all() { return _sortProjects(_projects); }
|
|
81
92
|
|
|
82
93
|
function find(id) { return id != null ? (_projects.find(p => p.id === String(id)) || null) : null; }
|
|
83
94
|
|
|
@@ -196,7 +207,9 @@ const Projects = (() => {
|
|
|
196
207
|
const bi = sortedProjects.findIndex(p => p.id === b.project_id);
|
|
197
208
|
return ai - bi;
|
|
198
209
|
}
|
|
199
|
-
// Secondary: within project,
|
|
210
|
+
// Secondary: within project, pinned first then newest
|
|
211
|
+
if (a.pinned && !b.pinned) return -1;
|
|
212
|
+
if (!a.pinned && b.pinned) return 1;
|
|
200
213
|
const ta = a.updated_at || a.created_at || "";
|
|
201
214
|
const tb = b.updated_at || b.created_at || "";
|
|
202
215
|
return tb.localeCompare(ta);
|
|
@@ -230,7 +243,9 @@ const Projects = (() => {
|
|
|
230
243
|
const sessions = Sessions.all
|
|
231
244
|
.filter(s => s.project_id === project.id)
|
|
232
245
|
.sort((a, b) => {
|
|
233
|
-
//
|
|
246
|
+
// Pinned sessions first, then newest first
|
|
247
|
+
if (a.pinned && !b.pinned) return -1;
|
|
248
|
+
if (!a.pinned && b.pinned) return 1;
|
|
234
249
|
const ta = a.updated_at || a.created_at || "";
|
|
235
250
|
const tb = b.updated_at || b.created_at || "";
|
|
236
251
|
return tb.localeCompare(ta);
|
|
@@ -271,29 +286,11 @@ const Projects = (() => {
|
|
|
271
286
|
<path d="M12 8v8"/>
|
|
272
287
|
<path d="M8 12h8"/>
|
|
273
288
|
</svg>`;
|
|
274
|
-
newSessionBtn.addEventListener("click",
|
|
289
|
+
newSessionBtn.addEventListener("click", e => {
|
|
275
290
|
e.stopPropagation();
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
method: "POST",
|
|
280
|
-
headers: { "Content-Type": "application/json" },
|
|
281
|
-
body: JSON.stringify({
|
|
282
|
-
name,
|
|
283
|
-
agent_profile: "general",
|
|
284
|
-
source: "manual",
|
|
285
|
-
project_id: project.id,
|
|
286
|
-
}),
|
|
287
|
-
});
|
|
288
|
-
const data = await res.json();
|
|
289
|
-
if (res.ok && data.session) {
|
|
290
|
-
Sessions.add(data.session);
|
|
291
|
-
renderSection();
|
|
292
|
-
Sessions.select(data.session.id);
|
|
293
|
-
}
|
|
294
|
-
} catch (err) {
|
|
295
|
-
console.error("Failed to create session:", err);
|
|
296
|
-
}
|
|
291
|
+
const dir = project.working_dir || (NewSessionStore.state && NewSessionStore.state.defaultDir) || "";
|
|
292
|
+
NewSessionStore.updateAdvanced({ projectId: project.id, workingDir: dir });
|
|
293
|
+
Router.navigate("welcome");
|
|
297
294
|
});
|
|
298
295
|
// Actions button (⋯)
|
|
299
296
|
const actionsBtn = document.createElement("button");
|
|
@@ -304,28 +301,13 @@ const Projects = (() => {
|
|
|
304
301
|
</svg>`;
|
|
305
302
|
actionsBtn.addEventListener("click", e => {
|
|
306
303
|
e.stopPropagation();
|
|
307
|
-
_clearTooltip();
|
|
308
304
|
_showProjectMenu(project.id, actionsBtn);
|
|
309
305
|
});
|
|
310
306
|
header.appendChild(actionsBtn);
|
|
311
307
|
header.appendChild(newSessionBtn);
|
|
312
308
|
|
|
313
|
-
// Tooltip on hover — show after short delay; hide via shared _hideTimer
|
|
314
|
-
header.addEventListener("mouseenter", () => {
|
|
315
|
-
_cancelHide();
|
|
316
|
-
clearTimeout(_tooltipTimer);
|
|
317
|
-
_tooltipTimer = setTimeout(() => {
|
|
318
|
-
_showProjectTooltip(project, sessions, header);
|
|
319
|
-
}, 300);
|
|
320
|
-
});
|
|
321
|
-
header.addEventListener("mouseleave", () => {
|
|
322
|
-
clearTimeout(_tooltipTimer);
|
|
323
|
-
_scheduleHide();
|
|
324
|
-
});
|
|
325
|
-
|
|
326
309
|
// Toggle collapse on header click — in-place DOM toggle, no full re-render
|
|
327
310
|
header.addEventListener("click", () => {
|
|
328
|
-
_clearTooltip();
|
|
329
311
|
const nowCollapsed = _collapsed.has(project.id);
|
|
330
312
|
if (nowCollapsed) {
|
|
331
313
|
_collapsed.delete(project.id);
|
|
@@ -372,156 +354,9 @@ const Projects = (() => {
|
|
|
372
354
|
});
|
|
373
355
|
}
|
|
374
356
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
let _tooltipEl = null;
|
|
378
|
-
let _tooltipTimer = null; // show delay
|
|
379
|
-
let _hideTimer = null; // hide delay — shared by header + tooltip
|
|
380
|
-
|
|
381
|
-
/** Start hide countdown. Cancelled if mouse re-enters header or tooltip. */
|
|
382
|
-
function _scheduleHide() {
|
|
383
|
-
clearTimeout(_hideTimer);
|
|
384
|
-
_hideTimer = setTimeout(_clearTooltip, 150);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
/** Cancel pending hide (mouse is back on header or tooltip). */
|
|
388
|
-
function _cancelHide() {
|
|
389
|
-
clearTimeout(_hideTimer);
|
|
390
|
-
_hideTimer = null;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
/** Create and position the hover tooltip for a project. */
|
|
394
|
-
function _showProjectTooltip(project, sessions, anchorEl) {
|
|
395
|
-
_clearTooltip();
|
|
396
|
-
|
|
397
|
-
const tip = document.createElement("div");
|
|
398
|
-
tip.className = "project-tooltip";
|
|
399
|
-
|
|
400
|
-
// ── Header: icon + name + session count ──────────────────────────
|
|
401
|
-
const header = document.createElement("div");
|
|
402
|
-
header.className = "project-tooltip-header";
|
|
403
|
-
|
|
404
|
-
// Project icon — use shared helper (12px, matches text size)
|
|
405
|
-
const iconWrap = document.createElement("span");
|
|
406
|
-
iconWrap.className = "project-tooltip-icon";
|
|
407
|
-
iconWrap.innerHTML = _getIconSvg(project.icon || "folder", 12);
|
|
408
|
-
if (project.color) iconWrap.style.color = project.color;
|
|
409
|
-
header.appendChild(iconWrap);
|
|
410
|
-
|
|
411
|
-
const nameEl = document.createElement("span");
|
|
412
|
-
nameEl.className = "project-tooltip-name";
|
|
413
|
-
nameEl.textContent = project.name;
|
|
414
|
-
header.appendChild(nameEl);
|
|
415
|
-
|
|
416
|
-
tip.appendChild(header);
|
|
417
|
-
|
|
418
|
-
// ── Info rows ─────────────────────────────────────────────────────
|
|
419
|
-
const rows = document.createElement("div");
|
|
420
|
-
rows.className = "project-tooltip-rows";
|
|
421
|
-
|
|
422
|
-
// Helper: build one row with icon (12px) + text
|
|
423
|
-
const addRow = (iconSvgHtml, text, titleText) => {
|
|
424
|
-
const row = document.createElement("div");
|
|
425
|
-
row.className = "project-tooltip-row";
|
|
426
|
-
|
|
427
|
-
const iconSpan = document.createElement("span");
|
|
428
|
-
iconSpan.className = "project-tooltip-row-icon";
|
|
429
|
-
iconSpan.innerHTML = iconSvgHtml;
|
|
430
|
-
row.appendChild(iconSpan);
|
|
431
|
-
|
|
432
|
-
const textSpan = document.createElement("span");
|
|
433
|
-
textSpan.className = "project-tooltip-row-text";
|
|
434
|
-
textSpan.textContent = text;
|
|
435
|
-
if (titleText) textSpan.title = titleText;
|
|
436
|
-
row.appendChild(textSpan);
|
|
437
|
-
|
|
438
|
-
rows.appendChild(row);
|
|
439
|
-
};
|
|
440
|
-
|
|
441
|
-
// message-circle icon — simple chat bubble, no "+" (used for session count display)
|
|
442
|
-
const chatIcon = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22z"/></svg>`;
|
|
443
|
-
|
|
444
|
-
// Sessions row
|
|
445
|
-
const sessionText = sessions.length === 0
|
|
446
|
-
? I18n.t("projects.noSessions", "No sessions")
|
|
447
|
-
: I18n.t("projects.tooltip.sessions", "{n} sessions").replace("{n}", sessions.length);
|
|
448
|
-
addRow(chatIcon, sessionText);
|
|
449
|
-
|
|
450
|
-
// Working dir row (only if set) — reuse picker folder icon at 12px
|
|
451
|
-
if (project.working_dir) {
|
|
452
|
-
const divider = document.createElement("div");
|
|
453
|
-
divider.className = "project-tooltip-divider";
|
|
454
|
-
rows.appendChild(divider);
|
|
455
|
-
|
|
456
|
-
const dirRow = document.createElement("div");
|
|
457
|
-
dirRow.className = "project-tooltip-row project-tooltip-row--clickable";
|
|
458
|
-
dirRow.title = project.working_dir;
|
|
459
|
-
|
|
460
|
-
const iconSpan = document.createElement("span");
|
|
461
|
-
iconSpan.className = "project-tooltip-row-icon";
|
|
462
|
-
iconSpan.innerHTML = _getIconSvg("folder", 12);
|
|
463
|
-
dirRow.appendChild(iconSpan);
|
|
464
|
-
|
|
465
|
-
const textSpan = document.createElement("span");
|
|
466
|
-
textSpan.className = "project-tooltip-row-text";
|
|
467
|
-
textSpan.textContent = project.working_dir;
|
|
468
|
-
dirRow.appendChild(textSpan);
|
|
469
|
-
|
|
470
|
-
// Click: reveal in Finder/Explorer
|
|
471
|
-
dirRow.addEventListener("click", () => {
|
|
472
|
-
fetch("/api/file-action", {
|
|
473
|
-
method: "POST",
|
|
474
|
-
headers: { "Content-Type": "application/json" },
|
|
475
|
-
body: JSON.stringify({ path: project.working_dir, action: "reveal" }),
|
|
476
|
-
}).catch(err => console.error("[Projects] reveal error:", err));
|
|
477
|
-
_clearTooltip();
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
rows.appendChild(dirRow);
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
tip.appendChild(rows);
|
|
484
|
-
|
|
485
|
-
// ── Positioning: right side of the sidebar ───────────────────────
|
|
486
|
-
document.body.appendChild(tip);
|
|
487
|
-
_tooltipEl = tip;
|
|
488
|
-
|
|
489
|
-
// Hide when mouse leaves tooltip — use shared timer so gap crossing works
|
|
490
|
-
tip.addEventListener("mouseenter", () => _cancelHide());
|
|
491
|
-
tip.addEventListener("mouseleave", () => _scheduleHide());
|
|
492
|
-
|
|
493
|
-
const rect = anchorEl.getBoundingClientRect();
|
|
494
|
-
const tipRect = tip.getBoundingClientRect();
|
|
495
|
-
const margin = 8; // gap between sidebar and tooltip card
|
|
496
|
-
|
|
497
|
-
// Position tooltip 8px right of anchor; ::before pseudo fills the physical gap
|
|
498
|
-
let left = rect.right + margin;
|
|
499
|
-
if (left + tipRect.width > window.innerWidth - margin) {
|
|
500
|
-
left = rect.left - tipRect.width - margin;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// Vertically align top with anchor row, but keep inside viewport
|
|
504
|
-
let top = rect.top;
|
|
505
|
-
if (top + tipRect.height > window.innerHeight - margin) {
|
|
506
|
-
top = window.innerHeight - tipRect.height - margin;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
tip.style.left = `${Math.max(margin, left)}px`;
|
|
510
|
-
tip.style.top = `${Math.max(margin, top)}px`;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
function _clearTooltip() {
|
|
514
|
-
clearTimeout(_tooltipTimer);
|
|
515
|
-
_tooltipTimer = null;
|
|
516
|
-
clearTimeout(_hideTimer);
|
|
517
|
-
_hideTimer = null;
|
|
518
|
-
if (_tooltipEl) {
|
|
519
|
-
_tooltipEl.remove();
|
|
520
|
-
_tooltipEl = null;
|
|
521
|
-
}
|
|
522
|
-
}
|
|
357
|
+
|
|
523
358
|
|
|
524
|
-
/** Show a small context menu for project actions
|
|
359
|
+
/** Show a small context menu for project actions. */
|
|
525
360
|
function _showProjectMenu(projectId, anchor) {
|
|
526
361
|
// Remove any existing project menu
|
|
527
362
|
document.querySelectorAll(".project-actions-menu").forEach(m => m.remove());
|
|
@@ -529,10 +364,24 @@ const Projects = (() => {
|
|
|
529
364
|
const menu = document.createElement("div");
|
|
530
365
|
menu.className = "project-actions-menu";
|
|
531
366
|
|
|
532
|
-
|
|
367
|
+
// Lucide icons, embedded to avoid extra asset requests.
|
|
368
|
+
const iconEdit = `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 1 1 3 3L7 19l-4 1 1-4z"/></svg>`;
|
|
369
|
+
const iconClearSessions = `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m16 22-1-4"/><path d="M19 14a1 1 0 0 0 1-1v-1a2 2 0 0 0-2-2h-3a1 1 0 0 1-1-1V4a2 2 0 0 0-4 0v5a1 1 0 0 1-1 1H6a2 2 0 0 0-2 2v1a1 1 0 0 0 1 1"/><path d="M19 14H5l-1.973 6.767A1 1 0 0 0 4 22h16a1 1 0 0 0 .973-1.233z"/><path d="m8 22 1-4"/></svg>`;
|
|
370
|
+
const iconTrash = `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>`;
|
|
371
|
+
|
|
372
|
+
const addItem = (labelKey, defaultLabel, iconSvg, danger, onClick) => {
|
|
533
373
|
const item = document.createElement("button");
|
|
534
374
|
item.className = "project-actions-menu-item" + (danger ? " danger" : "");
|
|
535
|
-
|
|
375
|
+
|
|
376
|
+
const icon = document.createElement("span");
|
|
377
|
+
icon.className = "project-actions-menu-icon";
|
|
378
|
+
icon.innerHTML = iconSvg;
|
|
379
|
+
|
|
380
|
+
const label = document.createElement("span");
|
|
381
|
+
label.className = "project-actions-menu-label";
|
|
382
|
+
label.textContent = I18n.t(labelKey, defaultLabel);
|
|
383
|
+
|
|
384
|
+
item.append(icon, label);
|
|
536
385
|
item.addEventListener("click", e => {
|
|
537
386
|
e.stopPropagation();
|
|
538
387
|
menu.remove();
|
|
@@ -541,9 +390,9 @@ const Projects = (() => {
|
|
|
541
390
|
menu.appendChild(item);
|
|
542
391
|
};
|
|
543
392
|
|
|
544
|
-
addItem("projects.menu.edit", "Edit", false, () => promptEdit(projectId));
|
|
545
|
-
addItem("projects.menu.deleteSessions", "Delete Sessions", false, () => promptDeleteSessions(projectId));
|
|
546
|
-
addItem("projects.menu.delete", "Delete",
|
|
393
|
+
addItem("projects.menu.edit", "Edit Project", iconEdit, false, () => promptEdit(projectId));
|
|
394
|
+
addItem("projects.menu.deleteSessions", "Delete Sessions", iconClearSessions, false, () => promptDeleteSessions(projectId));
|
|
395
|
+
addItem("projects.menu.delete", "Delete", iconTrash, true, () => promptDelete(projectId));
|
|
547
396
|
|
|
548
397
|
// Position near anchor
|
|
549
398
|
const rect = anchor.getBoundingClientRect();
|
|
@@ -647,6 +496,13 @@ const Projects = (() => {
|
|
|
647
496
|
*/
|
|
648
497
|
function _ensureModalDOM() {
|
|
649
498
|
if (document.getElementById("project-picker-popup")) return;
|
|
499
|
+
|
|
500
|
+
let iconsHTML = "";
|
|
501
|
+
for (const [name, path] of Object.entries(_ICON_PATHS)) {
|
|
502
|
+
const sel = name === "folder" ? " project-picker-icon-option--selected" : "";
|
|
503
|
+
iconsHTML += `<button class="project-picker-icon-option${sel}" data-icon="${name}" title="${name[0].toUpperCase()}${name.slice(1)}"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${path}</svg></button>`;
|
|
504
|
+
}
|
|
505
|
+
|
|
650
506
|
const tpl = document.createElement("div");
|
|
651
507
|
tpl.innerHTML = `
|
|
652
508
|
<div id="project-picker-popup" class="project-picker-popup" style="display:none">
|
|
@@ -665,78 +521,7 @@ const Projects = (() => {
|
|
|
665
521
|
<div class="project-picker-divider"></div>
|
|
666
522
|
<!-- Icon grid -->
|
|
667
523
|
<div class="project-picker-icons" id="project-picker-icons">
|
|
668
|
-
|
|
669
|
-
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
|
|
670
|
-
</button>
|
|
671
|
-
<button class="project-picker-icon-option" data-icon="code" title="Code">
|
|
672
|
-
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
|
|
673
|
-
</button>
|
|
674
|
-
<button class="project-picker-icon-option" data-icon="book" title="Book">
|
|
675
|
-
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
|
|
676
|
-
</button>
|
|
677
|
-
<button class="project-picker-icon-option" data-icon="globe" title="Globe">
|
|
678
|
-
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
|
679
|
-
</button>
|
|
680
|
-
<button class="project-picker-icon-option" data-icon="zap" title="Zap">
|
|
681
|
-
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
|
|
682
|
-
</button>
|
|
683
|
-
<button class="project-picker-icon-option" data-icon="star" title="Star">
|
|
684
|
-
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
|
|
685
|
-
</button>
|
|
686
|
-
<button class="project-picker-icon-option" data-icon="heart" title="Heart">
|
|
687
|
-
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
|
|
688
|
-
</button>
|
|
689
|
-
<button class="project-picker-icon-option" data-icon="tool" title="Tool">
|
|
690
|
-
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>
|
|
691
|
-
</button>
|
|
692
|
-
<button class="project-picker-icon-option" data-icon="database" title="Database">
|
|
693
|
-
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg>
|
|
694
|
-
</button>
|
|
695
|
-
<button class="project-picker-icon-option" data-icon="image" title="Image">
|
|
696
|
-
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="16" height="16" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
|
|
697
|
-
</button>
|
|
698
|
-
<button class="project-picker-icon-option" data-icon="music" title="Music">
|
|
699
|
-
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>
|
|
700
|
-
</button>
|
|
701
|
-
<button class="project-picker-icon-option" data-icon="coffee" title="Coffee">
|
|
702
|
-
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8h1a4 4 0 0 1 0 8h-1"/><path d="M2 8h16v9a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V8z"/><line x1="6" y1="1" x2="6" y2="4"/><line x1="10" y1="1" x2="10" y2="4"/><line x1="14" y1="1" x2="14" y2="4"/></svg>
|
|
703
|
-
</button>
|
|
704
|
-
<button class="project-picker-icon-option" data-icon="briefcase" title="Briefcase">
|
|
705
|
-
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="20" height="14" rx="2" ry="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/></svg>
|
|
706
|
-
</button>
|
|
707
|
-
<button class="project-picker-icon-option" data-icon="flask" title="Flask">
|
|
708
|
-
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 3h6"/><path d="M10 3v6l-4 8a1 1 0 0 0 .9 1.5h10.2a1 1 0 0 0 .9-1.5L14 9V3"/></svg>
|
|
709
|
-
</button>
|
|
710
|
-
<button class="project-picker-icon-option" data-icon="chart" title="Chart">
|
|
711
|
-
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>
|
|
712
|
-
</button>
|
|
713
|
-
<button class="project-picker-icon-option" data-icon="rocket" title="Rocket">
|
|
714
|
-
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/></svg>
|
|
715
|
-
</button>
|
|
716
|
-
<button class="project-picker-icon-option" data-icon="dollar" title="Dollar">
|
|
717
|
-
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
|
|
718
|
-
</button>
|
|
719
|
-
<button class="project-picker-icon-option" data-icon="cpu" title="CPU">
|
|
720
|
-
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><line x1="9" y1="1" x2="9" y2="4"/><line x1="15" y1="1" x2="15" y2="4"/><line x1="9" y1="20" x2="9" y2="23"/><line x1="15" y1="20" x2="15" y2="23"/><line x1="20" y1="9" x2="23" y2="9"/><line x1="20" y1="14" x2="23" y2="14"/><line x1="1" y1="9" x2="4" y2="9"/><line x1="1" y1="14" x2="4" y2="14"/></svg>
|
|
721
|
-
</button>
|
|
722
|
-
<button class="project-picker-icon-option" data-icon="paint" title="Paint">
|
|
723
|
-
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="13.5" cy="6.5" r=".5"/><circle cx="17.5" cy="10.5" r=".5"/><circle cx="8.5" cy="7.5" r=".5"/><circle cx="6.5" cy="12.5" r=".5"/><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z"/></svg>
|
|
724
|
-
</button>
|
|
725
|
-
<button class="project-picker-icon-option" data-icon="leaf" title="Leaf">
|
|
726
|
-
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10z"/><path d="M2 21c0-3 1.85-5.36 5.08-6C9.5 14.52 12 13 13 12"/></svg>
|
|
727
|
-
</button>
|
|
728
|
-
<button class="project-picker-icon-option" data-icon="trophy" title="Trophy">
|
|
729
|
-
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="8 21 8 14 4 9 4 4 20 4 20 9 16 14 16 21"/><line x1="8" y1="21" x2="16" y2="21"/><path d="M4 9H2a1 1 0 0 0-1 1 6 6 0 0 0 6 6"/><path d="M20 9h2a1 1 0 0 1 1 1 6 6 0 0 1-6 6"/></svg>
|
|
730
|
-
</button>
|
|
731
|
-
<button class="project-picker-icon-option" data-icon="smile" title="Smile">
|
|
732
|
-
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M8 14s1.5 2 4 2 4-2 4-2"/><line x1="9" y1="9" x2="9.01" y2="9"/><line x1="15" y1="9" x2="15.01" y2="9"/></svg>
|
|
733
|
-
</button>
|
|
734
|
-
<button class="project-picker-icon-option" data-icon="map" title="Map">
|
|
735
|
-
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/><line x1="8" y1="2" x2="8" y2="18"/><line x1="16" y1="6" x2="16" y2="22"/></svg>
|
|
736
|
-
</button>
|
|
737
|
-
<button class="project-picker-icon-option" data-icon="flower" title="Flower">
|
|
738
|
-
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 7.5a4.5 4.5 0 1 1 4.5 4.5M12 7.5A4.5 4.5 0 1 0 7.5 12M12 7.5V9m-4.5 3a4.5 4.5 0 1 0 4.5 4.5M7.5 12H9m7.5 0a4.5 4.5 0 1 1-4.5 4.5m4.5-4.5H15m-3 4.5V15"/><circle cx="12" cy="12" r="3"/></svg>
|
|
739
|
-
</button>
|
|
524
|
+
${iconsHTML}
|
|
740
525
|
</div>
|
|
741
526
|
<!-- Done button -->
|
|
742
527
|
<div class="project-picker-footer">
|
|
@@ -775,6 +560,7 @@ const Projects = (() => {
|
|
|
775
560
|
</button>
|
|
776
561
|
</div>
|
|
777
562
|
</div>
|
|
563
|
+
<div id="create-project-folder-hint" class="create-project-folder-hint" data-i18n="projects.create.folderDefaultHint">If not set, the default working directory ~/clacky_workspace will be used</div>
|
|
778
564
|
</div>
|
|
779
565
|
</div>
|
|
780
566
|
<div class="modal-footer">
|
|
@@ -810,6 +596,7 @@ const Projects = (() => {
|
|
|
810
596
|
const folderCard = $("create-project-folder-card");
|
|
811
597
|
const folderName = $("create-project-folder-name");
|
|
812
598
|
const folderRemoveBtn = $("create-project-folder-remove");
|
|
599
|
+
const folderHint = $("create-project-folder-hint");
|
|
813
600
|
|
|
814
601
|
// State
|
|
815
602
|
let selectedColor = ""; // "" = default (accent)
|
|
@@ -859,6 +646,7 @@ const Projects = (() => {
|
|
|
859
646
|
folderCard.style.display = "none";
|
|
860
647
|
folderArea.style.cursor = "pointer";
|
|
861
648
|
}
|
|
649
|
+
folderHint.style.display = selectedDir ? "none" : "";
|
|
862
650
|
};
|
|
863
651
|
|
|
864
652
|
// Reset all state (prefill when editing an existing project)
|
|
@@ -1123,6 +911,12 @@ const Projects = (() => {
|
|
|
1123
911
|
}
|
|
1124
912
|
}
|
|
1125
913
|
|
|
914
|
+
function expand(projectId) {
|
|
915
|
+
if (!projectId) return;
|
|
916
|
+
_collapsed.delete(String(projectId));
|
|
917
|
+
_saveCollapsed();
|
|
918
|
+
}
|
|
919
|
+
|
|
1126
920
|
// ── Init ───────────────────────────────────────────────────────────────────
|
|
1127
921
|
|
|
1128
922
|
function init() {
|
|
@@ -1158,6 +952,8 @@ const Projects = (() => {
|
|
|
1158
952
|
_showOrganizeMenu(btnOrganize);
|
|
1159
953
|
});
|
|
1160
954
|
}
|
|
955
|
+
|
|
956
|
+
document.addEventListener("langchange", () => renderSection());
|
|
1161
957
|
}
|
|
1162
958
|
|
|
1163
959
|
// ── Public API ─────────────────────────────────────────────────────────────
|
|
@@ -1174,6 +970,8 @@ const Projects = (() => {
|
|
|
1174
970
|
promptEdit,
|
|
1175
971
|
promptDelete,
|
|
1176
972
|
renderSection,
|
|
973
|
+
expand,
|
|
974
|
+
getIconSvg: _getIconSvg,
|
|
1177
975
|
init,
|
|
1178
976
|
};
|
|
1179
977
|
})();
|