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
|
@@ -163,7 +163,7 @@
|
|
|
163
163
|
popup.style.bottom = "";
|
|
164
164
|
const popupHeight = popup.offsetHeight;
|
|
165
165
|
let top = barCenter - popupHeight / 2;
|
|
166
|
-
top = Math.max(
|
|
166
|
+
top = Math.max(48, Math.min(top, navRect.height - popupHeight));
|
|
167
167
|
popup.style.top = top + "px";
|
|
168
168
|
|
|
169
169
|
const target = items[idx].popupItem;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Owns billing state (summary / daily / sessions / model list / current period
|
|
4
4
|
// & model filter), currency conversion utilities (localStorage-backed), the
|
|
5
|
-
// data fetch, and clear-data actions. It never renders.
|
|
5
|
+
// data fetch, and clear-data actions (retained for API use). It never renders.
|
|
6
6
|
//
|
|
7
7
|
// Currency utilities are pure and read by both the view and other modules, so
|
|
8
8
|
// they live with the data layer. Emits store events the view reacts to; mirrors
|
|
@@ -103,8 +103,8 @@ const BillingStore = (() => {
|
|
|
103
103
|
if (_summary) _emit("billing:changed");
|
|
104
104
|
},
|
|
105
105
|
|
|
106
|
-
setPeriod(period) { _currentPeriod = period; return Billing.load(); },
|
|
107
|
-
setModel(model) { _currentModel = model; return Billing.load(); },
|
|
106
|
+
setPeriod(period) { _currentPeriod = period; _emit("billing:changed"); return Billing.load(); },
|
|
107
|
+
setModel(model) { _currentModel = model; _emit("billing:changed"); return Billing.load(); },
|
|
108
108
|
|
|
109
109
|
async load() {
|
|
110
110
|
const isFirstLoad = !_summary;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// ── Billing · view — dashboard rendering, charts, tooltips, DOM wiring ─────
|
|
2
2
|
//
|
|
3
3
|
// Owns every render function (stats, heatmap, trend, breakdowns, sessions),
|
|
4
|
-
// tooltip binding
|
|
5
|
-
// and currency helpers; period / model /
|
|
4
|
+
// tooltip binding. Reads through BillingStore.state
|
|
5
|
+
// and currency helpers; period / model / share go through store actions.
|
|
6
6
|
//
|
|
7
7
|
// Augments the `Billing` facade with onPanelShow.
|
|
8
8
|
//
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
// ───────────────────────────────────────────────────────────────────────────
|
|
11
11
|
|
|
12
12
|
const BillingView = (() => {
|
|
13
|
-
let _clearPopupVisible = false;
|
|
14
13
|
|
|
15
14
|
// ── Loading / error ───────────────────────────────────────────────────────
|
|
16
15
|
|
|
@@ -118,7 +117,7 @@ const BillingView = (() => {
|
|
|
118
117
|
|
|
119
118
|
const periods = ["day", "week", "month", "year", "all"];
|
|
120
119
|
const periodBtns = periods.map(p =>
|
|
121
|
-
`<button class="
|
|
120
|
+
`<button class="segmented-control__btn ${p === BillingStore.state.currentPeriod ? 'active' : ''}" data-period="${p}">${_periodLabel(p)}</button>`
|
|
122
121
|
).join("");
|
|
123
122
|
|
|
124
123
|
const allModels = BillingStore.state.allModels;
|
|
@@ -135,21 +134,12 @@ const BillingView = (() => {
|
|
|
135
134
|
<span class="billing-subtitle">${_getSummaryHint()}</span>
|
|
136
135
|
</div>
|
|
137
136
|
<div class="billing-controls">
|
|
138
|
-
<div class="
|
|
137
|
+
<div class="segmented-control">${periodBtns}</div>
|
|
139
138
|
<select id="billing-model-filter" class="billing-model-filter">${modelOptions}</select>
|
|
140
139
|
<button id="billing-share-btn" class="billing-share-btn" title="${I18n.t('billing.share.tooltip') || 'Share scorecard'}">
|
|
141
140
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><polyline points="16 6 12 2 8 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><line x1="12" y1="2" x2="12" y2="15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
142
141
|
${I18n.t('billing.share.btn') || 'Share scorecard'}
|
|
143
142
|
</button>
|
|
144
|
-
<div class="billing-clear-container">
|
|
145
|
-
<button id="billing-clear-btn" class="billing-clear-btn" title="${I18n.t('billing.clearData') || 'Clear Data'}">
|
|
146
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><polyline points="3 6 5 6 21 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M10 11v6M14 11v6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
147
|
-
</button>
|
|
148
|
-
<div id="billing-clear-popup" class="billing-clear-popup" style="display: none;">
|
|
149
|
-
<button id="billing-clear-today" class="billing-clear-option">${I18n.t('billing.clearToday') || 'Clear Today'}</button>
|
|
150
|
-
<button id="billing-clear-all" class="billing-clear-option billing-clear-danger">${I18n.t('billing.clearAll') || 'Clear All'}</button>
|
|
151
|
-
</div>
|
|
152
|
-
</div>
|
|
153
143
|
</div>
|
|
154
144
|
</div>
|
|
155
145
|
|
|
@@ -217,7 +207,7 @@ const BillingView = (() => {
|
|
|
217
207
|
</div>
|
|
218
208
|
`;
|
|
219
209
|
|
|
220
|
-
document.querySelectorAll(".
|
|
210
|
+
document.querySelectorAll(".segmented-control__btn").forEach(btn => {
|
|
221
211
|
btn.addEventListener("click", (e) => Billing.setPeriod(e.target.dataset.period));
|
|
222
212
|
});
|
|
223
213
|
|
|
@@ -225,7 +215,6 @@ const BillingView = (() => {
|
|
|
225
215
|
Billing.setModel(e.target.value);
|
|
226
216
|
});
|
|
227
217
|
|
|
228
|
-
_bindClearHandlers();
|
|
229
218
|
document.getElementById("billing-share-btn")?.addEventListener("click", _openScorecardShare);
|
|
230
219
|
|
|
231
220
|
_bindChartTooltip();
|
|
@@ -416,50 +405,6 @@ const BillingView = (() => {
|
|
|
416
405
|
});
|
|
417
406
|
}
|
|
418
407
|
|
|
419
|
-
// ── Clear popup ────────────────────────────────────────────────────────────
|
|
420
|
-
|
|
421
|
-
function _bindClearHandlers() {
|
|
422
|
-
const clearBtn = document.getElementById("billing-clear-btn");
|
|
423
|
-
const clearPopup = document.getElementById("billing-clear-popup");
|
|
424
|
-
const clearToday = document.getElementById("billing-clear-today");
|
|
425
|
-
const clearAll = document.getElementById("billing-clear-all");
|
|
426
|
-
|
|
427
|
-
if (!clearBtn || !clearPopup) return;
|
|
428
|
-
|
|
429
|
-
clearBtn.addEventListener("click", (e) => {
|
|
430
|
-
e.stopPropagation();
|
|
431
|
-
_clearPopupVisible = !_clearPopupVisible;
|
|
432
|
-
clearPopup.style.display = _clearPopupVisible ? "flex" : "none";
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
clearToday?.addEventListener("click", (e) => {
|
|
436
|
-
e.stopPropagation();
|
|
437
|
-
_hideClearPopup();
|
|
438
|
-
Billing.clearData("today");
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
clearAll?.addEventListener("click", (e) => {
|
|
442
|
-
e.stopPropagation();
|
|
443
|
-
_hideClearPopup();
|
|
444
|
-
Billing.clearData("all");
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
document.addEventListener("click", _closeClearPopup);
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
function _hideClearPopup() {
|
|
451
|
-
const clearPopup = document.getElementById("billing-clear-popup");
|
|
452
|
-
if (clearPopup) clearPopup.style.display = "none";
|
|
453
|
-
_clearPopupVisible = false;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
function _closeClearPopup(e) {
|
|
457
|
-
const clearPopup = document.getElementById("billing-clear-popup");
|
|
458
|
-
const clearBtn = document.getElementById("billing-clear-btn");
|
|
459
|
-
if (!clearPopup || !clearBtn) return;
|
|
460
|
-
if (!clearPopup.contains(e.target) && !clearBtn.contains(e.target)) _hideClearPopup();
|
|
461
|
-
}
|
|
462
|
-
|
|
463
408
|
// ── Section renderers ────────────────────────────────────────────────────────
|
|
464
409
|
|
|
465
410
|
function _getSummaryHint() {
|
|
@@ -678,7 +623,6 @@ const BillingView = (() => {
|
|
|
678
623
|
<div class="billing-chart-card billing-trend-card">
|
|
679
624
|
<div class="billing-chart-header">
|
|
680
625
|
<h4>${I18n.t("billing.costTrend") || "Cost Trend"}</h4>
|
|
681
|
-
<span class="billing-trend-total">${currencySymbol}${_formatCost(costs.reduce((a, b) => a + b, 0))}</span>
|
|
682
626
|
</div>
|
|
683
627
|
<div class="billing-trend-chart">
|
|
684
628
|
<svg viewBox="0 0 ${w} ${h}" preserveAspectRatio="xMidYMid meet" class="billing-trend-svg">
|
|
@@ -32,7 +32,11 @@ const ExtensionsStore = (() => {
|
|
|
32
32
|
let _filterSystem = false; // when true, show system (builtin) extensions
|
|
33
33
|
let _filterMarket = true; // when true, show marketplace extensions
|
|
34
34
|
let _onlyInstalled = (function() { try { return localStorage.getItem(EXT_INSTALLED_KEY) === "installed"; } catch (_) { return false; } })();
|
|
35
|
+
const PAGE_SIZE = 20; // requested page size, sent to the platform as per_page
|
|
36
|
+
let _page = 1; // current marketplace page (1-based)
|
|
37
|
+
let _hasMore = false; // whether the marketplace has another page
|
|
35
38
|
let _loading = false;
|
|
39
|
+
let _loadingMore = false;
|
|
36
40
|
let _error = null; // soft warning when the store is unreachable
|
|
37
41
|
let _detail = null; // currently opened extension detail, or null
|
|
38
42
|
let _detailLoading = false;
|
|
@@ -69,6 +73,8 @@ const ExtensionsStore = (() => {
|
|
|
69
73
|
get filterMarket() { return _filterMarket; },
|
|
70
74
|
get onlyInstalled() { return _onlyInstalled; },
|
|
71
75
|
get loading() { return _loading; },
|
|
76
|
+
get loadingMore(){ return _loadingMore; },
|
|
77
|
+
get hasMore() { return _hasMore; },
|
|
72
78
|
get error() { return _error; },
|
|
73
79
|
get detail() { return _detail; },
|
|
74
80
|
get detailLoading() { return _detailLoading; },
|
|
@@ -86,31 +92,7 @@ const ExtensionsStore = (() => {
|
|
|
86
92
|
if (_filterSystem) return Extensions.loadSystemExtensions();
|
|
87
93
|
if (_filterInstalled) return;
|
|
88
94
|
if (_filterBrand) return Extensions.loadBrandExtensions();
|
|
89
|
-
|
|
90
|
-
_loading = true;
|
|
91
|
-
_error = null;
|
|
92
|
-
_emit("extensions:loading");
|
|
93
|
-
try {
|
|
94
|
-
const params = new URLSearchParams();
|
|
95
|
-
if (_query) params.set("q", _query);
|
|
96
|
-
if (_sort) params.set("sort", _sort);
|
|
97
|
-
const qs = params.toString();
|
|
98
|
-
const res = await fetch("/api/store/extensions" + (qs ? "?" + qs : ""));
|
|
99
|
-
const data = await res.json();
|
|
100
|
-
_allExtensions = data.extensions || [];
|
|
101
|
-
_error = data.warning || null;
|
|
102
|
-
_loading = false;
|
|
103
|
-
_extensions = _allExtensions;
|
|
104
|
-
_emit("extensions:changed", { extensions: _extensions, warning: _error });
|
|
105
|
-
} catch (e) {
|
|
106
|
-
console.error("[Extensions] load failed", e);
|
|
107
|
-
_extensions = [];
|
|
108
|
-
_error = I18n.t("extensions.loadFailed");
|
|
109
|
-
_loading = false;
|
|
110
|
-
_emit("extensions:error", { network: true });
|
|
111
|
-
} finally {
|
|
112
|
-
_loading = false;
|
|
113
|
-
}
|
|
95
|
+
return Extensions.loadMarketExtensions();
|
|
114
96
|
},
|
|
115
97
|
|
|
116
98
|
/** Load builtin system extensions from /api/store/extensions/system. */
|
|
@@ -140,31 +122,61 @@ const ExtensionsStore = (() => {
|
|
|
140
122
|
}
|
|
141
123
|
},
|
|
142
124
|
|
|
143
|
-
/** Load marketplace extensions (all, not filtered by installed). */
|
|
125
|
+
/** Load marketplace extensions (all, not filtered by installed) — first page. */
|
|
144
126
|
async loadMarketExtensions() {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
127
|
+
_page = 1;
|
|
128
|
+
return Extensions._loadMarketPage(true);
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
/** Load the next marketplace page and append it to the current list. */
|
|
132
|
+
async loadMoreMarketExtensions() {
|
|
133
|
+
if (_loading || _loadingMore || !_hasMore) return;
|
|
134
|
+
return Extensions._loadMarketPage(false);
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
async _loadMarketPage(reset) {
|
|
138
|
+
const page = reset ? 1 : _page + 1;
|
|
139
|
+
if (reset) {
|
|
140
|
+
_loading = true;
|
|
141
|
+
_error = null;
|
|
142
|
+
_emit("extensions:loading");
|
|
143
|
+
} else {
|
|
144
|
+
_loadingMore = true;
|
|
145
|
+
_emit("extensions:loadingMore");
|
|
146
|
+
}
|
|
148
147
|
try {
|
|
149
148
|
const params = new URLSearchParams();
|
|
150
149
|
if (_query) params.set("q", _query);
|
|
151
150
|
if (_sort) params.set("sort", _sort);
|
|
151
|
+
if (page > 1) params.set("page", String(page));
|
|
152
|
+
params.set("per_page", String(PAGE_SIZE));
|
|
152
153
|
const qs = params.toString();
|
|
153
154
|
const res = await fetch("/api/store/extensions" + (qs ? "?" + qs : ""));
|
|
154
155
|
const data = await res.json();
|
|
155
|
-
|
|
156
|
-
_error
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
156
|
+
const incoming = data.extensions || [];
|
|
157
|
+
_error = data.warning || null;
|
|
158
|
+
|
|
159
|
+
if (reset) {
|
|
160
|
+
_allExtensions = incoming;
|
|
161
|
+
_extensions = _onlyInstalled ? incoming.filter(e => e.installed) : incoming;
|
|
162
|
+
} else {
|
|
163
|
+
_allExtensions = _allExtensions.concat(incoming);
|
|
164
|
+
const incomingFiltered = _onlyInstalled ? incoming.filter(e => e.installed) : incoming;
|
|
165
|
+
_extensions = _extensions.concat(incomingFiltered);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
_page = page;
|
|
169
|
+
_hasMore = _computeHasMore(data.meta, incoming);
|
|
160
170
|
} catch (e) {
|
|
161
171
|
console.error("[Extensions] loadMarketExtensions failed", e);
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
172
|
+
if (reset) {
|
|
173
|
+
_extensions = [];
|
|
174
|
+
_error = I18n.t("extensions.loadFailed");
|
|
175
|
+
}
|
|
166
176
|
} finally {
|
|
167
|
-
_loading
|
|
177
|
+
_loading = false;
|
|
178
|
+
_loadingMore = false;
|
|
179
|
+
_emit("extensions:changed", { extensions: _extensions, warning: _error, reset: reset });
|
|
168
180
|
}
|
|
169
181
|
},
|
|
170
182
|
|
|
@@ -191,7 +203,8 @@ const ExtensionsStore = (() => {
|
|
|
191
203
|
try { localStorage.setItem(EXT_INSTALLED_KEY, _onlyInstalled ? "installed" : "all"); } catch (_) {}
|
|
192
204
|
if (_filterSystem) return Extensions.loadSystemExtensions();
|
|
193
205
|
if (_filterBrand) return Extensions.loadBrandExtensions();
|
|
194
|
-
|
|
206
|
+
_extensions = _onlyInstalled ? _allExtensions.filter(e => e.installed) : _allExtensions;
|
|
207
|
+
_emit("extensions:changed", { extensions: _extensions, warning: _error, reset: true });
|
|
195
208
|
},
|
|
196
209
|
|
|
197
210
|
/** Switch to the system (builtin) extensions tab. */
|
|
@@ -362,7 +375,7 @@ const ExtensionsStore = (() => {
|
|
|
362
375
|
});
|
|
363
376
|
const data = await res.json();
|
|
364
377
|
if (!res.ok || !data.ok) throw new Error(data.error || "toggle failed");
|
|
365
|
-
const activeTab = _filterSystem ? "system" : _filterBrand ? "brand" : "market";
|
|
378
|
+
const activeTab = _filterSystem ? "system" : _filterBrand ? "brand" : _filterInstalled ? "installed" : "market";
|
|
366
379
|
try { sessionStorage.setItem(EXT_TAB_KEY, activeTab); } catch (_) {}
|
|
367
380
|
location.reload();
|
|
368
381
|
} catch (e) {
|
|
@@ -453,6 +466,17 @@ const ExtensionsStore = (() => {
|
|
|
453
466
|
|
|
454
467
|
};
|
|
455
468
|
|
|
469
|
+
// Whether another marketplace page exists. Prefers the platform's pagination
|
|
470
|
+
// meta; falls back to "a full page came back" for older platform builds that
|
|
471
|
+
// don't return meta yet.
|
|
472
|
+
function _computeHasMore(meta, incoming) {
|
|
473
|
+
if (meta && meta.total_pages != null) {
|
|
474
|
+
const current = meta.current_page != null ? Number(meta.current_page) : _page;
|
|
475
|
+
return current < Number(meta.total_pages);
|
|
476
|
+
}
|
|
477
|
+
return (incoming || []).length >= PAGE_SIZE;
|
|
478
|
+
}
|
|
479
|
+
|
|
456
480
|
// Poll /api/store/extension/install/status until done, updating _installJob each tick.
|
|
457
481
|
// `token` is the _pollToken snapshot taken when the poll started; if the
|
|
458
482
|
// user closes the detail panel, closeDetail() bumps _pollToken and this
|
|
@@ -14,10 +14,12 @@
|
|
|
14
14
|
const ExtensionsView = (() => {
|
|
15
15
|
let _domWired = false;
|
|
16
16
|
let _searchTimer = null;
|
|
17
|
+
let _renderedCount = 0; // cards already appended to the list DOM
|
|
17
18
|
|
|
18
19
|
function _renderLoading() {
|
|
19
20
|
const container = $("extensions-list");
|
|
20
21
|
if (!container) return;
|
|
22
|
+
_renderedCount = 0;
|
|
21
23
|
container.innerHTML = Array.from({ length: 4 }).map(() => `
|
|
22
24
|
<div class="extension-card">
|
|
23
25
|
<div class="extension-card-main">
|
|
@@ -33,25 +35,63 @@ const ExtensionsView = (() => {
|
|
|
33
35
|
function _renderEmpty() {
|
|
34
36
|
const container = $("extensions-list");
|
|
35
37
|
if (!container) return;
|
|
38
|
+
_renderedCount = 0;
|
|
36
39
|
const key = ExtensionsStore.state.query ? "extensions.noResults" : "extensions.empty";
|
|
37
40
|
container.innerHTML = `<div class="extensions-empty">${escapeHtml(I18n.t(key))}</div>`;
|
|
38
41
|
}
|
|
39
42
|
|
|
40
|
-
function _renderList() {
|
|
43
|
+
function _renderList(payload) {
|
|
41
44
|
const container = $("extensions-list");
|
|
42
45
|
if (!container) return;
|
|
43
46
|
|
|
44
|
-
|
|
47
|
+
const st = ExtensionsStore.state;
|
|
48
|
+
const reset = !payload || payload.reset !== false;
|
|
49
|
+
|
|
50
|
+
if (st.loading && reset) { _renderLoading(); return; }
|
|
45
51
|
|
|
46
|
-
const list =
|
|
52
|
+
const list = st.extensions;
|
|
47
53
|
if (!list || list.length === 0) { _renderEmpty(); return; }
|
|
48
54
|
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
if (reset) {
|
|
56
|
+
container.innerHTML = "";
|
|
57
|
+
_renderedCount = 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (let i = _renderedCount; i < list.length; i++) {
|
|
61
|
+
container.appendChild(_renderCard(list[i]));
|
|
62
|
+
}
|
|
63
|
+
_renderedCount = list.length;
|
|
64
|
+
|
|
65
|
+
_renderLoadMore(container, st.hasMore, st.loadingMore);
|
|
51
66
|
|
|
52
67
|
_applyWarning(ExtensionsStore.state.error);
|
|
53
68
|
}
|
|
54
69
|
|
|
70
|
+
function _renderLoadMore(container, hasMore, loadingMore) {
|
|
71
|
+
const existing = container.querySelector(".extensions-load-more");
|
|
72
|
+
if (existing) existing.remove();
|
|
73
|
+
if (!hasMore) return;
|
|
74
|
+
// Only the marketplace tab is paginated; the other tabs are full loads.
|
|
75
|
+
if (!ExtensionsStore.state.filterMarket) return;
|
|
76
|
+
|
|
77
|
+
const btn = document.createElement("button");
|
|
78
|
+
btn.type = "button";
|
|
79
|
+
btn.className = "extensions-load-more";
|
|
80
|
+
btn.textContent = loadingMore ? I18n.t("extensions.loadingMore") : I18n.t("extensions.loadMore");
|
|
81
|
+
btn.disabled = loadingMore;
|
|
82
|
+
btn.addEventListener("click", () => Extensions.loadMoreMarketExtensions());
|
|
83
|
+
container.appendChild(btn);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function _renderLoadingMore() {
|
|
87
|
+
const container = $("extensions-list");
|
|
88
|
+
const btn = container && container.querySelector(".extensions-load-more");
|
|
89
|
+
if (btn) {
|
|
90
|
+
btn.disabled = true;
|
|
91
|
+
btn.textContent = I18n.t("extensions.loadingMore");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
55
95
|
function _applyWarning(warning) {
|
|
56
96
|
const banner = $("extensions-warning");
|
|
57
97
|
if (!banner) return;
|
|
@@ -108,6 +148,8 @@ const ExtensionsView = (() => {
|
|
|
108
148
|
? `<span class="extension-updatable">${escapeHtml(I18n.t("extensions.updatable"))}</span>` : "";
|
|
109
149
|
const unlistedHtml = (ext.unlisted && ext.installed)
|
|
110
150
|
? `<span class="extension-unlisted">${escapeHtml(I18n.t("extensions.unlisted"))}</span>` : "";
|
|
151
|
+
const verifiedHtml = (ext.verified && !ext.installed)
|
|
152
|
+
? `<span class="extension-verified">${escapeHtml(I18n.t("extensions.verified"))}</span>` : "";
|
|
111
153
|
const unitsText = _formatUnits(ext.units);
|
|
112
154
|
const unitsHtml = unitsText
|
|
113
155
|
? `<span class="extension-units">${escapeHtml(unitsText)}</span>` : "";
|
|
@@ -137,6 +179,7 @@ const ExtensionsView = (() => {
|
|
|
137
179
|
${installedHtml}
|
|
138
180
|
${updatableHtml}
|
|
139
181
|
${unlistedHtml}
|
|
182
|
+
${verifiedHtml}
|
|
140
183
|
${unitsHtml}
|
|
141
184
|
${authorHtml}
|
|
142
185
|
${installsHtml}
|
|
@@ -160,6 +203,7 @@ const ExtensionsView = (() => {
|
|
|
160
203
|
} else {
|
|
161
204
|
card.addEventListener("click", (e) => {
|
|
162
205
|
if (e.target.closest("a")) return;
|
|
206
|
+
_saveCurrentTab();
|
|
163
207
|
const router = window.Clacky && window.Clacky.Router;
|
|
164
208
|
const isBrand = card.dataset.extOrigin === "self" || Extensions.state.filterBrand;
|
|
165
209
|
const source = isBrand ? "brand" : null;
|
|
@@ -181,6 +225,17 @@ const ExtensionsView = (() => {
|
|
|
181
225
|
</button>`;
|
|
182
226
|
}
|
|
183
227
|
|
|
228
|
+
function _saveCurrentTab() {
|
|
229
|
+
try {
|
|
230
|
+
var s = ExtensionsStore.state;
|
|
231
|
+
var tab = "market";
|
|
232
|
+
if (s.filterSystem) tab = "system";
|
|
233
|
+
else if (s.filterBrand) tab = "brand";
|
|
234
|
+
else if (s.filterInstalled) tab = "installed";
|
|
235
|
+
sessionStorage.setItem(EXT_TAB_KEY, tab);
|
|
236
|
+
} catch (_) {}
|
|
237
|
+
}
|
|
238
|
+
|
|
184
239
|
// Returns a human-readable button label for a given install job state.
|
|
185
240
|
function _installJobLabel(job) {
|
|
186
241
|
if (!job) return I18n.t("extensions.action.install");
|
|
@@ -300,13 +355,14 @@ const ExtensionsView = (() => {
|
|
|
300
355
|
|
|
301
356
|
const installBtn = document.querySelector("[data-ext-install]");
|
|
302
357
|
if (installBtn) {
|
|
303
|
-
installBtn.addEventListener("click", () => {
|
|
358
|
+
installBtn.addEventListener("click", async () => {
|
|
304
359
|
const id = installBtn.getAttribute("data-ext-install");
|
|
360
|
+
const ext = ExtensionsStore.state.detail;
|
|
361
|
+
if (ext && !ext.verified && !(typeof Brand !== "undefined" && Brand.branded)) {
|
|
362
|
+
const confirmed = await Modal.confirm(I18n.t("extensions.unverifiedInstallWarning"));
|
|
363
|
+
if (!confirmed) return;
|
|
364
|
+
}
|
|
305
365
|
installBtn.disabled = true;
|
|
306
|
-
// Show a "downloading" placeholder rather than a generic "installing" —
|
|
307
|
-
// the next real state is always the download starting, so this avoids
|
|
308
|
-
// a jarring "installing → downloading → installing" flash before any
|
|
309
|
-
// real progress data has arrived.
|
|
310
366
|
installBtn.textContent = _installJobLabel({ stage: "downloading", progress: null });
|
|
311
367
|
Extensions.install(id);
|
|
312
368
|
});
|
|
@@ -350,6 +406,10 @@ const ExtensionsView = (() => {
|
|
|
350
406
|
? `<span class="extension-installed">${installedLabel}</span>` : "";
|
|
351
407
|
const unlistedHtml = ext.unlisted
|
|
352
408
|
? `<span class="extension-unlisted">${escapeHtml(I18n.t("extensions.unlisted"))}</span>` : "";
|
|
409
|
+
const verifiedHtml = ext.verified
|
|
410
|
+
? `<span class="extension-verified">${escapeHtml(I18n.t("extensions.verified"))}</span>` : "";
|
|
411
|
+
const unverifiedBannerHtml = (!ext.verified && !ext.installed && !(typeof Brand !== "undefined" && Brand.branded))
|
|
412
|
+
? `<div class="extension-unverified-banner"><svg class="extension-unverified-icon" width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="7" cy="7" r="6.25" stroke="currentColor" stroke-width="1.2"/><path d="M7 4V7.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/><circle cx="7" cy="10" r="0.7" fill="currentColor"/></svg>${escapeHtml(I18n.t("extensions.unverifiedBanner"))}</div>` : "";
|
|
353
413
|
const unitsText = _formatUnits(ext.units);
|
|
354
414
|
const unitsHtml = unitsText
|
|
355
415
|
? `<span class="extension-units">${escapeHtml(unitsText)}</span>` : "";
|
|
@@ -370,12 +430,14 @@ const ExtensionsView = (() => {
|
|
|
370
430
|
${versionHtml}
|
|
371
431
|
${installedHtml}
|
|
372
432
|
${unlistedHtml}
|
|
433
|
+
${verifiedHtml}
|
|
373
434
|
${unitsHtml}
|
|
374
435
|
${authorHtml}
|
|
375
436
|
${installsHtml}
|
|
376
437
|
</div>
|
|
377
438
|
${description ? `<div class="extension-desc extension-desc-detail">${escapeHtml(description)}</div>` : ""}
|
|
378
439
|
${homepageHtml ? `<div class="extension-meta">${homepageHtml}</div>` : ""}
|
|
440
|
+
${unverifiedBannerHtml}
|
|
379
441
|
${_renderActions(ext)}
|
|
380
442
|
</div>
|
|
381
443
|
</div>
|
|
@@ -584,6 +646,7 @@ const ExtensionsView = (() => {
|
|
|
584
646
|
if (e.target.closest("a")) return;
|
|
585
647
|
const card = e.target.closest(".extension-card-clickable");
|
|
586
648
|
if (card && card.dataset.extId) {
|
|
649
|
+
_saveCurrentTab();
|
|
587
650
|
const router = window.Clacky && window.Clacky.Router;
|
|
588
651
|
// Use origin stored on the card; origin="self" means brand-private extension.
|
|
589
652
|
const isBrand = card.dataset.extOrigin === "self" || Extensions.state.filterBrand;
|
|
@@ -605,6 +668,7 @@ const ExtensionsView = (() => {
|
|
|
605
668
|
|
|
606
669
|
function _subscribe() {
|
|
607
670
|
Extensions.on("extensions:loading", _renderLoading);
|
|
671
|
+
Extensions.on("extensions:loadingMore", _renderLoadingMore);
|
|
608
672
|
Extensions.on("extensions:changed", _renderList);
|
|
609
673
|
Extensions.on("extensions:error", _renderList);
|
|
610
674
|
Extensions.on("extensions:detail", _renderDetail);
|
|
@@ -620,6 +620,27 @@ const NewSessionView = (() => {
|
|
|
620
620
|
_renderContextBar();
|
|
621
621
|
});
|
|
622
622
|
pop.appendChild(clearItem);
|
|
623
|
+
|
|
624
|
+
// "Create new project" action
|
|
625
|
+
const actionSep = document.createElement("div");
|
|
626
|
+
actionSep.style.cssText = "height:1px;background:var(--color-border-primary);margin:4px 8px";
|
|
627
|
+
pop.appendChild(actionSep);
|
|
628
|
+
const createItem = document.createElement("button");
|
|
629
|
+
createItem.type = "button";
|
|
630
|
+
createItem.className = "ns-ctx-popover-item";
|
|
631
|
+
createItem.style.cssText = "display:flex;align-items:center;gap:6px";
|
|
632
|
+
const plusIcon = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0"><path d="M12 5v14M5 12h14"/></svg>';
|
|
633
|
+
createItem.innerHTML = plusIcon + `<span>${zh ? "新建项目" : "New project"}</span>`;
|
|
634
|
+
createItem.addEventListener("click", async () => {
|
|
635
|
+
_dismissCtxPopover();
|
|
636
|
+
const project = await Projects.promptCreate();
|
|
637
|
+
if (project) {
|
|
638
|
+
const dir = project.working_dir || NewSessionStore.state.defaultDir || "";
|
|
639
|
+
NewSessionStore.updateAdvanced({ projectId: project.id, workingDir: dir });
|
|
640
|
+
_renderContextBar();
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
pop.appendChild(createItem);
|
|
623
644
|
});
|
|
624
645
|
}
|
|
625
646
|
|
|
@@ -786,6 +807,9 @@ const NewSessionView = (() => {
|
|
|
786
807
|
const display = hasFiles ? _buildBubbleHtml(finalContent) : null;
|
|
787
808
|
|
|
788
809
|
Sessions.add(session);
|
|
810
|
+
if (session.project_id && typeof Projects !== "undefined" && Projects.expand) {
|
|
811
|
+
Projects.expand(session.project_id);
|
|
812
|
+
}
|
|
789
813
|
Sessions.setPendingMessage(session.id, finalContent, display, files);
|
|
790
814
|
input.value = "";
|
|
791
815
|
_pendingImages.length = 0;
|
|
@@ -41,6 +41,7 @@ const VersionView = (() => {
|
|
|
41
41
|
if (!badge || !text) return;
|
|
42
42
|
|
|
43
43
|
text.textContent = s.current ? `v${s.current}` : "";
|
|
44
|
+
text.href = "https://github.com/clacky-ai/openclacky/releases";
|
|
44
45
|
|
|
45
46
|
if (dot) dot.style.display = "none";
|
|
46
47
|
if (restartDot) restartDot.style.display = "none";
|
|
@@ -142,11 +143,10 @@ const VersionView = (() => {
|
|
|
142
143
|
<p class="vup-versions">v${s.current} <span class="vup-arrow">→</span> v${s.latest}</p>
|
|
143
144
|
<div class="vup-actions">
|
|
144
145
|
<button id="vup-btn-upgrade" class="vup-btn-primary">${I18n.t("upgrade.btn.upgrade")}</button>
|
|
145
|
-
|
|
146
|
+
${(typeof Brand !== "undefined" && Brand.branded) ? "" : `<a id="vup-btn-changelog" class="vup-btn-changelog" href="https://github.com/clacky-ai/openclacky/releases" target="_blank" rel="noopener noreferrer">${I18n.t("upgrade.btn.changelog")}</a>`}
|
|
146
147
|
</div>
|
|
147
148
|
`;
|
|
148
149
|
$("vup-btn-upgrade").addEventListener("click", () => Version.startUpgrade());
|
|
149
|
-
$("vup-btn-cancel").addEventListener("click", _closePopover);
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
function _renderProgressState(pop, s) {
|