openclacky 1.3.7 → 1.3.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 +42 -0
- data/lib/clacky/agent/hook_manager.rb +10 -1
- data/lib/clacky/agent/system_prompt_builder.rb +1 -1
- data/lib/clacky/agent.rb +9 -2
- data/lib/clacky/agent_profile.rb +7 -4
- data/lib/clacky/billing/billing_store.rb +3 -2
- data/lib/clacky/brand_config.rb +2 -1
- data/lib/clacky/default_extensions/coding/agents/coding/avatar.png +0 -0
- data/lib/clacky/default_extensions/coding/ext.yml +4 -4
- data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/avatar.png +0 -0
- data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +0 -1
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +4 -3
- data/lib/clacky/default_extensions/ext-studio/ext.yml +1 -1
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +118 -50
- data/lib/clacky/default_extensions/ext-studio/skills/ext-publish/SKILL.md +1 -2
- data/lib/clacky/default_extensions/general/agents/general/avatar.png +0 -0
- data/lib/clacky/default_extensions/general/ext.yml +4 -4
- data/lib/clacky/default_extensions/git/ext.yml +0 -1
- data/lib/clacky/default_extensions/git/panels/git/view.js +3 -3
- data/lib/clacky/default_extensions/meeting/ext.yml +0 -1
- data/lib/clacky/default_extensions/meeting/panels/meeting/view.js +1 -2
- data/lib/clacky/default_extensions/time_machine/ext.yml +0 -1
- data/lib/clacky/extension/api_extension.rb +11 -2
- data/lib/clacky/extension/dispatcher.rb +3 -2
- data/lib/clacky/extension/loader.rb +6 -0
- data/lib/clacky/extension/packager.rb +0 -6
- data/lib/clacky/extension/verifier.rb +1 -1
- data/lib/clacky/server/http_server.rb +149 -5
- data/lib/clacky/shell_hook_loader.rb +266 -22
- data/lib/clacky/ui2/components/welcome_banner.rb +1 -1
- data/lib/clacky/utils/workspace_rules.rb +2 -2
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +334 -73
- data/lib/clacky/web/core/ext.js +130 -24
- data/lib/clacky/web/features/extensions/store.js +41 -6
- data/lib/clacky/web/features/extensions/view.js +44 -13
- data/lib/clacky/web/features/new-session/store.js +13 -0
- data/lib/clacky/web/features/new-session/view.js +314 -14
- data/lib/clacky/web/features/trash/view.js +4 -5
- data/lib/clacky/web/i18n.js +44 -2
- data/lib/clacky/web/index.html +54 -16
- data/lib/clacky/web/sessions.js +3 -3
- data/lib/clacky/web/skills.js +86 -48
- data/lib/clacky/web/theme.js +3 -0
- data/lib/clacky/web/ws-dispatcher.js +11 -2
- metadata +7 -7
data/lib/clacky/web/core/ext.js
CHANGED
|
@@ -8,10 +8,16 @@
|
|
|
8
8
|
// Clacky.ext.subscribe(event, handler) — listen to store events (read-only)
|
|
9
9
|
// Clacky.ext.ui.mount(slot, spec, opts) — inject UI into a named slot
|
|
10
10
|
//
|
|
11
|
-
// `spec` is either a render function or { create?, render }.
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
11
|
+
// `spec` is either a render function or { create?, render }. The render
|
|
12
|
+
// signature is always `render(container, ctx, runtime)`:
|
|
13
|
+
// - `container` is a host-owned DOM element — append your UI into it,
|
|
14
|
+
// or return a DOM node / HTML string and the host appends it for you.
|
|
15
|
+
// - returning a function registers it as a teardown callback.
|
|
16
|
+
// - returning null is treated as an error (almost always a wrong
|
|
17
|
+
// signature, e.g. `(ctx) => ...`), surfaced as a crashed placeholder.
|
|
18
|
+
// On per-session slots (session.aside/banner/composer), providing
|
|
19
|
+
// `create(ctx)` gives you one runtime per sessionId, isolated across
|
|
20
|
+
// sessions and disposed only when the session leaves the sidebar.
|
|
15
21
|
//
|
|
16
22
|
// Safety guarantees enforced here (the "constitution"):
|
|
17
23
|
// - Every extension callback is wrapped in try/catch. A throwing extension is
|
|
@@ -49,8 +55,6 @@ Clacky.ext = (() => {
|
|
|
49
55
|
// renderer becomes one tab (chrome drawn by the host), and only the active
|
|
50
56
|
// tab's body is shown. Renderers in these slots must carry opts.tab.
|
|
51
57
|
const TABBED_SLOTS = { "session.aside": true };
|
|
52
|
-
// slot => active tab id (remembered across re-renders within a page load).
|
|
53
|
-
const _activeTab = {};
|
|
54
58
|
|
|
55
59
|
// Slots that follow the current session's agent scope. A mount into any
|
|
56
60
|
// of these, made from inside a panel file, is confined to that panel's
|
|
@@ -92,6 +96,7 @@ Clacky.ext = (() => {
|
|
|
92
96
|
// Resolved by the host before the extension scripts run (see loader markers).
|
|
93
97
|
const _extAgents = {};
|
|
94
98
|
const _panelAgents = {};
|
|
99
|
+
const _agentContributions = {}; // agent id => { panels:[{id,title,title_zh}], skills:[...] }
|
|
95
100
|
let _currentPanel = null; // set while an official panel file is loading
|
|
96
101
|
|
|
97
102
|
// Current session context, kept in sync by the host on every session switch.
|
|
@@ -213,6 +218,11 @@ Clacky.ext = (() => {
|
|
|
213
218
|
Object.assign(_panelAgents, map);
|
|
214
219
|
}
|
|
215
220
|
|
|
221
|
+
function registerAgentContributions(map) {
|
|
222
|
+
if (PURE || !map) return;
|
|
223
|
+
Object.assign(_agentContributions, map);
|
|
224
|
+
}
|
|
225
|
+
|
|
216
226
|
const api = {
|
|
217
227
|
// Register a named data source. Host/extensions can later resolve it.
|
|
218
228
|
register(name, fn) {
|
|
@@ -376,26 +386,119 @@ Clacky.ext = (() => {
|
|
|
376
386
|
// as the view teardown, run before the next render or on session exit.
|
|
377
387
|
function _invokeRender(slot, entry, container, ctx) {
|
|
378
388
|
const runtime = _getRuntime(slot, entry, ctx);
|
|
379
|
-
let node, teardown, crashed = false;
|
|
389
|
+
let node, teardown, crashed = false, reason = null;
|
|
380
390
|
try {
|
|
381
391
|
const out = entry.render(container, ctx, runtime);
|
|
382
|
-
if (out === undefined) {
|
|
383
|
-
|
|
392
|
+
if (out === undefined || out === null) {
|
|
393
|
+
// undefined → container mutated in-place; null → renderer opted out of
|
|
394
|
+
// this context (e.g. `if (!ctx.sessionId) return null` on the new-session
|
|
395
|
+
// page). Both render nothing; neither is an error.
|
|
384
396
|
} else if (typeof out === "function") {
|
|
385
397
|
teardown = out;
|
|
386
398
|
} else {
|
|
387
399
|
node = out;
|
|
388
400
|
}
|
|
389
|
-
} catch (
|
|
401
|
+
} catch (err) {
|
|
402
|
+
reason = String((err && err.stack) || err);
|
|
403
|
+
console.error(`[Clacky.ext] extension "${entry.extId || "?"}" threw in render for slot ${JSON.stringify(slot)}:`, err);
|
|
390
404
|
crashed = true;
|
|
391
405
|
}
|
|
392
|
-
if (crashed) return { crashed: true };
|
|
406
|
+
if (crashed) return { crashed: true, reason, extId: entry.extId };
|
|
393
407
|
if (ctx && ctx.sessionId && teardown) {
|
|
394
408
|
_rememberViewTeardown(ctx.sessionId, _mountKey(slot, entry), teardown);
|
|
395
409
|
}
|
|
396
410
|
return { node };
|
|
397
411
|
}
|
|
398
412
|
|
|
413
|
+
// Build a styled, self-contained crashed-panel placeholder: a short headline
|
|
414
|
+
// plus a collapsible <details> exposing the actual reason and a nudge to the
|
|
415
|
+
// console. Never throws; the reason is rendered as plain text.
|
|
416
|
+
function _crashedPlaceholder(extId, reason) {
|
|
417
|
+
_injectCrashStyle();
|
|
418
|
+
const box = document.createElement("div");
|
|
419
|
+
box.className = "ext-slot-crashed";
|
|
420
|
+
box.setAttribute("data-ext-status", "crashed");
|
|
421
|
+
|
|
422
|
+
const head = document.createElement("div");
|
|
423
|
+
head.className = "ext-crashed-head";
|
|
424
|
+
const icon = document.createElement("span");
|
|
425
|
+
icon.className = "ext-crashed-icon";
|
|
426
|
+
icon.textContent = "!";
|
|
427
|
+
const title = document.createElement("span");
|
|
428
|
+
title.className = "ext-crashed-title";
|
|
429
|
+
title.textContent = extId ? `扩展「${extId}」渲染失败` : "扩展渲染失败";
|
|
430
|
+
head.appendChild(icon);
|
|
431
|
+
head.appendChild(title);
|
|
432
|
+
box.appendChild(head);
|
|
433
|
+
|
|
434
|
+
const hint = document.createElement("div");
|
|
435
|
+
hint.className = "ext-crashed-hint";
|
|
436
|
+
hint.textContent = "面板未能加载。展开下方详情,或打开浏览器控制台 (console) 查看完整错误。";
|
|
437
|
+
box.appendChild(hint);
|
|
438
|
+
|
|
439
|
+
if (reason) {
|
|
440
|
+
const details = document.createElement("details");
|
|
441
|
+
details.className = "ext-crashed-details";
|
|
442
|
+
const summary = document.createElement("summary");
|
|
443
|
+
summary.textContent = "错误详情";
|
|
444
|
+
const pre = document.createElement("pre");
|
|
445
|
+
pre.className = "ext-crashed-reason";
|
|
446
|
+
pre.textContent = reason;
|
|
447
|
+
details.appendChild(summary);
|
|
448
|
+
details.appendChild(pre);
|
|
449
|
+
box.appendChild(details);
|
|
450
|
+
}
|
|
451
|
+
return box;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
let _crashStyleInjected = false;
|
|
455
|
+
function _injectCrashStyle() {
|
|
456
|
+
if (_crashStyleInjected) return;
|
|
457
|
+
_crashStyleInjected = true;
|
|
458
|
+
const style = document.createElement("style");
|
|
459
|
+
style.id = "ext-crashed-style";
|
|
460
|
+
style.textContent = `
|
|
461
|
+
.ext-slot-crashed {
|
|
462
|
+
margin: 12px;
|
|
463
|
+
padding: 14px 16px;
|
|
464
|
+
border: 1px solid var(--color-error-border, #fecaca);
|
|
465
|
+
background: var(--color-error-bg, #fef2f2);
|
|
466
|
+
border-radius: var(--radius-md, 8px);
|
|
467
|
+
color: var(--color-text-secondary, #56585e);
|
|
468
|
+
font-size: 13px;
|
|
469
|
+
line-height: 1.5;
|
|
470
|
+
}
|
|
471
|
+
.ext-crashed-head { display: flex; align-items: center; gap: 8px; }
|
|
472
|
+
.ext-crashed-icon {
|
|
473
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
474
|
+
width: 18px; height: 18px; flex: none;
|
|
475
|
+
border-radius: 50%;
|
|
476
|
+
background: var(--color-error, #ef4444);
|
|
477
|
+
color: #fff; font-size: 12px; font-weight: 700; line-height: 1;
|
|
478
|
+
}
|
|
479
|
+
.ext-crashed-title { font-weight: 600; color: var(--color-text-primary, #1a1b1e); }
|
|
480
|
+
.ext-crashed-hint { margin-top: 6px; color: var(--color-text-tertiary, #8a8d94); }
|
|
481
|
+
.ext-crashed-details { margin-top: 10px; }
|
|
482
|
+
.ext-crashed-details > summary {
|
|
483
|
+
cursor: pointer; user-select: none;
|
|
484
|
+
color: var(--color-error, #ef4444); font-weight: 500;
|
|
485
|
+
}
|
|
486
|
+
.ext-crashed-reason {
|
|
487
|
+
margin: 8px 0 0;
|
|
488
|
+
padding: 10px;
|
|
489
|
+
max-height: 220px; overflow: auto;
|
|
490
|
+
background: var(--color-bg-primary, #fbfbfa);
|
|
491
|
+
border: 1px solid var(--color-border-primary, #e8e8e4);
|
|
492
|
+
border-radius: var(--radius-sm, 6px);
|
|
493
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
494
|
+
font-size: 12px; line-height: 1.45;
|
|
495
|
+
white-space: pre-wrap; word-break: break-word;
|
|
496
|
+
color: var(--color-text-secondary, #56585e);
|
|
497
|
+
}
|
|
498
|
+
`;
|
|
499
|
+
(document.head || document.documentElement).appendChild(style);
|
|
500
|
+
}
|
|
501
|
+
|
|
399
502
|
|
|
400
503
|
// null agents AND null panel => global (always visible). Otherwise the
|
|
401
504
|
// current profile must be in the renderer's agent list, or in the set of
|
|
@@ -407,6 +510,13 @@ Clacky.ext = (() => {
|
|
|
407
510
|
return false;
|
|
408
511
|
}
|
|
409
512
|
|
|
513
|
+
// What a third-party agent's extension contributes (panels + skills), from
|
|
514
|
+
// ext.yml — so the new-session page can advertise it before a session exists.
|
|
515
|
+
// Returns { panels:[{id,title,title_zh}], skills:[...] } or null for built-ins.
|
|
516
|
+
function contributionsForAgent(agentId) {
|
|
517
|
+
return (agentId && _agentContributions[agentId]) || null;
|
|
518
|
+
}
|
|
519
|
+
|
|
410
520
|
// Render every extension registered for `slot` into `container`, scoped to the
|
|
411
521
|
// current agent profile. Called by the host's view layer wherever it exposes a
|
|
412
522
|
// slot, and re-called on session switch (the container is cleared first so a
|
|
@@ -436,11 +546,9 @@ Clacky.ext = (() => {
|
|
|
436
546
|
// Nav mounts that open a workspace declare it via opts.workspace; the
|
|
437
547
|
// host stamps it so the Router alone owns the active highlight.
|
|
438
548
|
if (entry.workspace) wrap.setAttribute("data-ext-workspace", entry.workspace);
|
|
439
|
-
const { node, crashed } = _invokeRender(slot, entry, wrap, renderCtx);
|
|
549
|
+
const { node, crashed, reason, extId } = _invokeRender(slot, entry, wrap, renderCtx);
|
|
440
550
|
if (crashed) {
|
|
441
|
-
wrap.
|
|
442
|
-
wrap.className = "ext-slot-crashed";
|
|
443
|
-
wrap.textContent = "Extension failed to render.";
|
|
551
|
+
wrap.appendChild(_crashedPlaceholder(extId || entry.extId, reason));
|
|
444
552
|
container.appendChild(wrap);
|
|
445
553
|
return;
|
|
446
554
|
}
|
|
@@ -478,17 +586,16 @@ Clacky.ext = (() => {
|
|
|
478
586
|
const bodies = document.createElement("div");
|
|
479
587
|
bodies.className = "aside-bodies";
|
|
480
588
|
|
|
481
|
-
//
|
|
589
|
+
// Always default to the first (highest-priority) tab. Tabs may register in
|
|
590
|
+
// several async passes; whichever pass runs last simply lands on tab #1.
|
|
482
591
|
const ids = renderers.map((e) => e.tab.id);
|
|
483
|
-
|
|
484
|
-
if (!active || !ids.includes(active)) active = ids[0];
|
|
592
|
+
const active = ids[0];
|
|
485
593
|
|
|
486
594
|
const tabBtns = {};
|
|
487
595
|
const bodyEls = {};
|
|
488
596
|
const rendered = {};
|
|
489
597
|
|
|
490
598
|
function activate(id) {
|
|
491
|
-
_activeTab[slot] = id;
|
|
492
599
|
Object.keys(tabBtns).forEach((k) => tabBtns[k].classList.toggle("active", k === id));
|
|
493
600
|
Object.keys(bodyEls).forEach((k) => bodyEls[k].classList.toggle("active", k === id));
|
|
494
601
|
if (!rendered[id]) {
|
|
@@ -498,12 +605,9 @@ Clacky.ext = (() => {
|
|
|
498
605
|
const localCtx = Object.assign({}, renderCtx, {
|
|
499
606
|
setBadge: (n) => _setTabBadge(tabBtns[id], n),
|
|
500
607
|
});
|
|
501
|
-
const { node, crashed } = _invokeRender(slot, entry, body, localCtx);
|
|
608
|
+
const { node, crashed, reason, extId } = _invokeRender(slot, entry, body, localCtx);
|
|
502
609
|
if (crashed) {
|
|
503
|
-
|
|
504
|
-
ph.className = "ext-slot-crashed";
|
|
505
|
-
ph.textContent = "Extension failed to render.";
|
|
506
|
-
body.appendChild(ph);
|
|
610
|
+
body.appendChild(_crashedPlaceholder(extId || entry.extId, reason));
|
|
507
611
|
} else if (node != null) {
|
|
508
612
|
if (typeof node === "string") body.innerHTML = node;
|
|
509
613
|
else body.appendChild(node);
|
|
@@ -608,6 +712,8 @@ Clacky.ext = (() => {
|
|
|
608
712
|
notifySessionRemoved,
|
|
609
713
|
refreshSlots,
|
|
610
714
|
registerPanelAgents,
|
|
715
|
+
registerAgentContributions,
|
|
716
|
+
contributionsForAgent,
|
|
611
717
|
api,
|
|
612
718
|
ui,
|
|
613
719
|
subscribe,
|
|
@@ -19,9 +19,11 @@
|
|
|
19
19
|
|
|
20
20
|
const ExtensionsStore = (() => {
|
|
21
21
|
// ── State (single source of truth) ─────────────────────────────────────
|
|
22
|
-
let _extensions
|
|
23
|
-
let
|
|
24
|
-
let
|
|
22
|
+
let _extensions = []; // [{ id, name, name_zh, description, ..., units }]
|
|
23
|
+
let _allExtensions = []; // unfiltered result from server
|
|
24
|
+
let _query = ""; // current search text
|
|
25
|
+
let _sort = "newest"; // "newest" | "updated" | "downloads"
|
|
26
|
+
let _filterInstalled = false; // when true, show only installed extensions
|
|
25
27
|
let _loading = false;
|
|
26
28
|
let _error = null; // soft warning when the store is unreachable
|
|
27
29
|
let _detail = null; // currently opened extension detail, or null
|
|
@@ -50,6 +52,7 @@ const ExtensionsStore = (() => {
|
|
|
50
52
|
get extensions() { return _extensions; },
|
|
51
53
|
get query() { return _query; },
|
|
52
54
|
get sort() { return _sort; },
|
|
55
|
+
get filterInstalled() { return _filterInstalled; },
|
|
53
56
|
get loading() { return _loading; },
|
|
54
57
|
get error() { return _error; },
|
|
55
58
|
get detail() { return _detail; },
|
|
@@ -73,7 +76,8 @@ const ExtensionsStore = (() => {
|
|
|
73
76
|
const qs = params.toString();
|
|
74
77
|
const res = await fetch("/api/store/extensions" + (qs ? "?" + qs : ""));
|
|
75
78
|
const data = await res.json();
|
|
76
|
-
|
|
79
|
+
_allExtensions = data.extensions || [];
|
|
80
|
+
_extensions = _filterInstalled ? _allExtensions.filter(e => e.installed) : _allExtensions;
|
|
77
81
|
_error = data.warning || null;
|
|
78
82
|
_loading = false;
|
|
79
83
|
_emit("extensions:changed", { extensions: _extensions, warning: _error });
|
|
@@ -100,6 +104,13 @@ const ExtensionsStore = (() => {
|
|
|
100
104
|
return Extensions.load();
|
|
101
105
|
},
|
|
102
106
|
|
|
107
|
+
/** Toggle the "installed only" filter (client-side, no new network request). */
|
|
108
|
+
setFilterInstalled(onlyInstalled) {
|
|
109
|
+
_filterInstalled = !!onlyInstalled;
|
|
110
|
+
_extensions = _filterInstalled ? _allExtensions.filter(e => e.installed) : _allExtensions;
|
|
111
|
+
_emit("extensions:changed", { extensions: _extensions, warning: _error });
|
|
112
|
+
},
|
|
113
|
+
|
|
103
114
|
/** Open the detail view for one extension (fetches contributes + versions). */
|
|
104
115
|
async loadDetail(id) {
|
|
105
116
|
if (!id) return;
|
|
@@ -156,6 +167,31 @@ const ExtensionsStore = (() => {
|
|
|
156
167
|
}
|
|
157
168
|
},
|
|
158
169
|
|
|
170
|
+
/** Install a marketplace extension by fetching its download_url then posting to the local server. */
|
|
171
|
+
async install(id) {
|
|
172
|
+
if (!id) return;
|
|
173
|
+
try {
|
|
174
|
+
const detailRes = await fetch("/api/store/extension?id=" + encodeURIComponent(id));
|
|
175
|
+
const detailData = await detailRes.json();
|
|
176
|
+
if (!detailRes.ok || !detailData.ok) throw new Error(detailData.error || "fetch detail failed");
|
|
177
|
+
const ext = detailData.extension;
|
|
178
|
+
const download_url = ext.download_url;
|
|
179
|
+
if (!download_url) throw new Error("No download URL available");
|
|
180
|
+
const res = await fetch("/api/store/extension/install", {
|
|
181
|
+
method: "POST",
|
|
182
|
+
headers: { "Content-Type": "application/json" },
|
|
183
|
+
body: JSON.stringify({ download_url, name: ext.name }),
|
|
184
|
+
});
|
|
185
|
+
const data = await res.json();
|
|
186
|
+
if (!res.ok || !data.ok) throw new Error(data.error || "install failed");
|
|
187
|
+
await Extensions.loadDetail(id);
|
|
188
|
+
} catch (e) {
|
|
189
|
+
console.error("[Extensions] install failed", e);
|
|
190
|
+
_detailError = e.message;
|
|
191
|
+
_emit("extensions:detail");
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
|
|
159
195
|
/** Remove an installed extension, then return to the list. */
|
|
160
196
|
async uninstall(id) {
|
|
161
197
|
if (!id) return;
|
|
@@ -167,8 +203,7 @@ const ExtensionsStore = (() => {
|
|
|
167
203
|
});
|
|
168
204
|
const data = await res.json();
|
|
169
205
|
if (!res.ok || !data.ok) throw new Error(data.error || "uninstall failed");
|
|
170
|
-
Extensions.
|
|
171
|
-
Extensions.load();
|
|
206
|
+
await Extensions.loadDetail(id);
|
|
172
207
|
} catch (e) {
|
|
173
208
|
console.error("[Extensions] uninstall failed", e);
|
|
174
209
|
_detailError = e.message;
|
|
@@ -21,8 +21,8 @@ const ExtensionsView = (() => {
|
|
|
21
21
|
container.innerHTML = Array.from({ length: 4 }).map(() => `
|
|
22
22
|
<div class="extension-card">
|
|
23
23
|
<div class="extension-card-main">
|
|
24
|
-
<span class="skel" style="height:2rem;width:2rem;border-radius:8px;"></span>
|
|
25
|
-
<div class="extension-info">
|
|
24
|
+
<span class="skel" style="height:2rem;width:2rem;border-radius:8px;flex-shrink:0"></span>
|
|
25
|
+
<div class="extension-info" style="display:flex;flex-direction:column;gap:0.375rem">
|
|
26
26
|
<span class="skel skel-title"></span>
|
|
27
27
|
<span class="skel skel-subtitle"></span>
|
|
28
28
|
</div>
|
|
@@ -161,7 +161,8 @@ const ExtensionsView = (() => {
|
|
|
161
161
|
return `
|
|
162
162
|
<div class="extension-detail-head">
|
|
163
163
|
<button type="button" class="extension-detail-back" id="extension-detail-back">
|
|
164
|
-
|
|
164
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9 2L4 7L9 12" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
165
|
+
${escapeHtml(I18n.t("extensions.detail.back"))}
|
|
165
166
|
</button>
|
|
166
167
|
</div>
|
|
167
168
|
${inner}`;
|
|
@@ -188,6 +189,16 @@ const ExtensionsView = (() => {
|
|
|
188
189
|
if (ok) Extensions.uninstall(id);
|
|
189
190
|
});
|
|
190
191
|
}
|
|
192
|
+
|
|
193
|
+
const installBtn = document.querySelector("[data-ext-install]");
|
|
194
|
+
if (installBtn) {
|
|
195
|
+
installBtn.addEventListener("click", () => {
|
|
196
|
+
const id = installBtn.getAttribute("data-ext-install");
|
|
197
|
+
installBtn.disabled = true;
|
|
198
|
+
installBtn.textContent = I18n.t("extensions.action.installing");
|
|
199
|
+
Extensions.install(id);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
191
202
|
}
|
|
192
203
|
|
|
193
204
|
function _backToList() {
|
|
@@ -214,6 +225,8 @@ const ExtensionsView = (() => {
|
|
|
214
225
|
const homepageHtml = ext.homepage
|
|
215
226
|
? `<a class="extension-homepage" href="${escapeHtml(ext.homepage)}" target="_blank" rel="noopener noreferrer">${I18n.t("extensions.homepage")}</a>`
|
|
216
227
|
: "";
|
|
228
|
+
const authorHtml = ext.author
|
|
229
|
+
? `<span class="extension-detail-author">${escapeHtml(ext.author)}</span>` : "";
|
|
217
230
|
|
|
218
231
|
return `
|
|
219
232
|
<div class="extension-detail-hero">
|
|
@@ -225,10 +238,8 @@ const ExtensionsView = (() => {
|
|
|
225
238
|
${installedHtml}
|
|
226
239
|
${unitsHtml}
|
|
227
240
|
</div>
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
${homepageHtml}
|
|
231
|
-
</div>
|
|
241
|
+
${description ? `<div class="extension-desc extension-desc-detail">${escapeHtml(description)}</div>` : ""}
|
|
242
|
+
${(authorHtml || homepageHtml) ? `<div class="extension-meta">${authorHtml}${homepageHtml}</div>` : ""}
|
|
232
243
|
${_renderActions(ext)}
|
|
233
244
|
</div>
|
|
234
245
|
</div>
|
|
@@ -239,8 +250,17 @@ const ExtensionsView = (() => {
|
|
|
239
250
|
// Manage buttons for a locally installed extension: enable/disable toggle
|
|
240
251
|
// (always available when installed) plus remove (installed layer only).
|
|
241
252
|
function _renderActions(ext) {
|
|
242
|
-
|
|
243
|
-
|
|
253
|
+
const id = ext.id != null ? String(ext.id) : (ext.name || ext.slug || "");
|
|
254
|
+
if (!ext.installed) {
|
|
255
|
+
if (ext.origin === "marketplace" && ext.download_url) {
|
|
256
|
+
return `
|
|
257
|
+
<div class="extension-detail-actions">
|
|
258
|
+
<button type="button" class="extension-action extension-action-install" data-ext-install="${escapeHtml(id)}">${escapeHtml(I18n.t("extensions.action.install"))}</button>
|
|
259
|
+
</div>`;
|
|
260
|
+
}
|
|
261
|
+
return "";
|
|
262
|
+
}
|
|
263
|
+
const slug = ext.name || ext.slug || id;
|
|
244
264
|
const toggleKey = ext.disabled ? "extensions.action.enable" : "extensions.action.disable";
|
|
245
265
|
const toggleCls = ext.disabled ? "extension-action-enable" : "extension-action-disable";
|
|
246
266
|
const disabledBadge = ext.disabled
|
|
@@ -273,10 +293,13 @@ const ExtensionsView = (() => {
|
|
|
273
293
|
const singular = type.replace(/s$/, "");
|
|
274
294
|
const heading = _sectionHeading(type);
|
|
275
295
|
const rows = items.map((it) => {
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
296
|
+
const isStr = typeof it === "string";
|
|
297
|
+
const title = isStr ? it
|
|
298
|
+
: ((currentLang === "zh" && it.title_zh) ? it.title_zh
|
|
299
|
+
: (it.title || it.name || it.id || singular));
|
|
300
|
+
const desc = isStr ? ""
|
|
301
|
+
: ((currentLang === "zh" && it.description_zh) ? it.description_zh
|
|
302
|
+
: (it.description || ""));
|
|
280
303
|
return `
|
|
281
304
|
<li class="extension-contrib-item">
|
|
282
305
|
<span class="extension-contrib-title">${escapeHtml(String(title))}</span>
|
|
@@ -339,6 +362,14 @@ const ExtensionsView = (() => {
|
|
|
339
362
|
sortSelect.addEventListener("change", () => Extensions.setSort(sortSelect.value));
|
|
340
363
|
}
|
|
341
364
|
|
|
365
|
+
document.querySelectorAll(".extensions-filter-tab").forEach(btn => {
|
|
366
|
+
btn.addEventListener("click", () => {
|
|
367
|
+
document.querySelectorAll(".extensions-filter-tab").forEach(b => b.classList.remove("extensions-filter-tab-active"));
|
|
368
|
+
btn.classList.add("extensions-filter-tab-active");
|
|
369
|
+
Extensions.setFilterInstalled(btn.dataset.filter === "installed");
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
342
373
|
const list = $("extensions-list");
|
|
343
374
|
if (list) {
|
|
344
375
|
list.addEventListener("click", (e) => {
|
|
@@ -168,6 +168,18 @@ const NewSessionStore = (() => {
|
|
|
168
168
|
_emit("newSession:reset", {});
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
async function fetchSkillsForAgent(agentId) {
|
|
172
|
+
try {
|
|
173
|
+
const res = await fetch(`/api/agents/${encodeURIComponent(agentId)}/skills`);
|
|
174
|
+
if (!res.ok) return [];
|
|
175
|
+
const data = await res.json();
|
|
176
|
+
return data.skills || [];
|
|
177
|
+
} catch (e) {
|
|
178
|
+
console.error("Failed to load skills:", e);
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
171
183
|
return {
|
|
172
184
|
get state() { return _state; },
|
|
173
185
|
on: _on,
|
|
@@ -179,6 +191,7 @@ const NewSessionStore = (() => {
|
|
|
179
191
|
loadModels,
|
|
180
192
|
loadDefaultDirectory,
|
|
181
193
|
createSession,
|
|
194
|
+
fetchSkillsForAgent,
|
|
182
195
|
reset,
|
|
183
196
|
};
|
|
184
197
|
})();
|