openclacky 1.4.0 → 1.5.0
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 +55 -0
- data/lib/clacky/agent/llm_caller.rb +3 -3
- data/lib/clacky/agent/session_serializer.rb +50 -9
- data/lib/clacky/agent.rb +4 -5
- data/lib/clacky/brand_config.rb +102 -10
- data/lib/clacky/client.rb +22 -16
- data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +135 -84
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +144 -1
- data/lib/clacky/default_extensions/ext-studio/ext.yml +2 -4
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +412 -173
- data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +522 -0
- data/lib/clacky/extension/api_extension.rb +5 -6
- data/lib/clacky/extension/loader.rb +16 -2
- data/lib/clacky/extension/packager.rb +10 -1
- data/lib/clacky/extension/scaffold/templates/full/api/handler.rb.erb +16 -0
- data/lib/clacky/mcp/http_transport.rb +1 -1
- data/lib/clacky/platform_http_client.rb +9 -7
- data/lib/clacky/providers.rb +113 -5
- data/lib/clacky/server/http_server.rb +131 -38
- data/lib/clacky/server/session_registry.rb +9 -0
- data/lib/clacky/tools/grep.rb +6 -1
- data/lib/clacky/utils/environment_detector.rb +16 -9
- data/lib/clacky/utils/file_processor.rb +21 -24
- data/lib/clacky/utils/model_pricing.rb +85 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +441 -48
- data/lib/clacky/web/app.js +29 -6
- data/lib/clacky/web/components/onboard.js +21 -7
- data/lib/clacky/web/features/backup/view.js +4 -3
- data/lib/clacky/web/features/extensions/store.js +56 -4
- data/lib/clacky/web/features/extensions/view.js +54 -24
- data/lib/clacky/web/features/mcp/view.js +5 -2
- data/lib/clacky/web/features/new-session/store.js +8 -4
- data/lib/clacky/web/features/version/view.js +5 -1
- data/lib/clacky/web/features/workspace/store.js +11 -0
- data/lib/clacky/web/features/workspace/view.js +10 -4
- data/lib/clacky/web/i18n.js +58 -36
- data/lib/clacky/web/index.html +74 -7
- data/lib/clacky/web/sessions.js +72 -15
- data/lib/clacky/web/settings.js +22 -3
- data/lib/clacky/web/theme.js +27 -58
- data/scripts/uninstall.sh +1 -1
- metadata +2 -5
- data/lib/clacky/default_extensions/ext-studio/skills/ext-debug/SKILL.md +0 -71
- data/lib/clacky/default_extensions/ext-studio/skills/ext-publish/SKILL.md +0 -73
- data/lib/clacky/default_extensions/ext-studio/skills/ext-scaffold/SKILL.md +0 -65
- data/lib/clacky/default_skills/extend-openclacky/SKILL.md +0 -106
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
const Onboard = (() => {
|
|
18
18
|
let _providers = [];
|
|
19
19
|
let _selectedLang = I18n.lang(); // language chosen during setup
|
|
20
|
+
let _branded = false; // true when running under a brand license
|
|
20
21
|
|
|
21
22
|
// ── Public API ──────────────────────────────────────────────────────────────
|
|
22
23
|
|
|
@@ -27,6 +28,7 @@ const Onboard = (() => {
|
|
|
27
28
|
if (!data.needs_onboard) return { needsOnboard: false, phase: null };
|
|
28
29
|
|
|
29
30
|
const phase = data.phase;
|
|
31
|
+
_branded = !!data.branded;
|
|
30
32
|
|
|
31
33
|
if (phase === "key_setup") {
|
|
32
34
|
// Mandatory: show full-screen setup panel, block boot.
|
|
@@ -111,9 +113,16 @@ const Onboard = (() => {
|
|
|
111
113
|
$("setup-dot-1").className = "setup-step" + (step === "lang" ? " active" : " done");
|
|
112
114
|
$("setup-dot-2").className = "setup-step" + (step === "key" ? " active" : "");
|
|
113
115
|
if (step === "key") {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
116
|
+
if (_branded) {
|
|
117
|
+
// Brand mode: skip the OpenClacky AI Keys card, go straight to manual config
|
|
118
|
+
$("setup-device-block").style.display = "none";
|
|
119
|
+
$("setup-manual-toggle").style.display = "none";
|
|
120
|
+
$("setup-manual-section").style.display = "";
|
|
121
|
+
} else {
|
|
122
|
+
$("setup-device-block").style.display = "";
|
|
123
|
+
$("setup-manual-toggle").style.display = "";
|
|
124
|
+
$("setup-manual-section").style.display = "none";
|
|
125
|
+
}
|
|
117
126
|
}
|
|
118
127
|
}
|
|
119
128
|
|
|
@@ -153,16 +162,20 @@ const Onboard = (() => {
|
|
|
153
162
|
dropdown.appendChild(placeholder);
|
|
154
163
|
|
|
155
164
|
_providers.forEach(p => {
|
|
165
|
+
// Prefer i18n key (localised); fall back to shipped English `name`.
|
|
166
|
+
const translated = p.name_key ? I18n.t(p.name_key) : null;
|
|
167
|
+
const displayName = (translated && translated !== p.name_key) ? translated : p.name;
|
|
168
|
+
|
|
156
169
|
const opt = document.createElement("div");
|
|
157
170
|
opt.className = "custom-select-option";
|
|
158
171
|
opt.dataset.value = p.id;
|
|
159
|
-
opt.dataset.label =
|
|
172
|
+
opt.dataset.label = displayName;
|
|
160
173
|
if (p.id === "openclacky") {
|
|
161
174
|
const nameSpan = document.createElement("span");
|
|
162
|
-
nameSpan.textContent =
|
|
175
|
+
nameSpan.textContent = displayName;
|
|
163
176
|
opt.innerHTML = nameSpan.outerHTML + ` <span class="provider-badge-recommended">${I18n.t("provider.recommended")}</span>`;
|
|
164
177
|
} else {
|
|
165
|
-
opt.textContent =
|
|
178
|
+
opt.textContent = displayName;
|
|
166
179
|
}
|
|
167
180
|
dropdown.appendChild(opt);
|
|
168
181
|
});
|
|
@@ -379,7 +392,8 @@ const Onboard = (() => {
|
|
|
379
392
|
});
|
|
380
393
|
|
|
381
394
|
$("setup-btn-back").addEventListener("click", () => {
|
|
382
|
-
if ($("setup-manual-section").style.display !== "none") {
|
|
395
|
+
if (!_branded && $("setup-manual-section").style.display !== "none") {
|
|
396
|
+
// Non-brand: collapse manual section back to device card
|
|
383
397
|
$("setup-device-block").style.display = "";
|
|
384
398
|
$("setup-manual-toggle").style.display = "";
|
|
385
399
|
$("setup-manual-section").style.display = "none";
|
|
@@ -58,7 +58,8 @@ const BackupView = (() => {
|
|
|
58
58
|
a.click();
|
|
59
59
|
a.remove();
|
|
60
60
|
URL.revokeObjectURL(url);
|
|
61
|
-
if (statusEl) { statusEl.textContent =
|
|
61
|
+
if (statusEl) { statusEl.textContent = ""; statusEl.className = "model-test-result"; }
|
|
62
|
+
Modal.toast(I18n.t("settings.backup.downloaded"), "success");
|
|
62
63
|
} else if (statusEl) {
|
|
63
64
|
statusEl.textContent = I18n.t("settings.backup.lastError", { msg: res.error });
|
|
64
65
|
statusEl.className = "model-test-result error";
|
|
@@ -108,8 +109,8 @@ const BackupView = (() => {
|
|
|
108
109
|
if (ev.type !== "_ws_connected" || !_restoreStatusEl) return;
|
|
109
110
|
const el = _restoreStatusEl;
|
|
110
111
|
_restoreStatusEl = null;
|
|
111
|
-
el.textContent =
|
|
112
|
-
|
|
112
|
+
if (el) { el.textContent = ""; el.className = "model-test-result"; }
|
|
113
|
+
Modal.toast(I18n.t("settings.backup.restartOk"), "success");
|
|
113
114
|
if (typeof Settings !== "undefined") Settings.open();
|
|
114
115
|
});
|
|
115
116
|
}
|
|
@@ -22,8 +22,9 @@ const ExtensionsStore = (() => {
|
|
|
22
22
|
let _extensions = []; // [{ id, name, name_zh, description, ..., units }]
|
|
23
23
|
let _allExtensions = []; // unfiltered result from server
|
|
24
24
|
let _query = ""; // current search text
|
|
25
|
-
let _sort = "
|
|
25
|
+
let _sort = "downloads"; // "newest" | "updated" | "downloads"
|
|
26
26
|
let _filterInstalled = false; // when true, show only installed extensions
|
|
27
|
+
let _filterBrand = false; // when true, show only brand-private extensions
|
|
27
28
|
let _loading = false;
|
|
28
29
|
let _error = null; // soft warning when the store is unreachable
|
|
29
30
|
let _detail = null; // currently opened extension detail, or null
|
|
@@ -53,6 +54,7 @@ const ExtensionsStore = (() => {
|
|
|
53
54
|
get query() { return _query; },
|
|
54
55
|
get sort() { return _sort; },
|
|
55
56
|
get filterInstalled() { return _filterInstalled; },
|
|
57
|
+
get filterBrand() { return _filterBrand; },
|
|
56
58
|
get loading() { return _loading; },
|
|
57
59
|
get error() { return _error; },
|
|
58
60
|
get detail() { return _detail; },
|
|
@@ -67,6 +69,7 @@ const ExtensionsStore = (() => {
|
|
|
67
69
|
/** Fetch the catalog from the server for the current query + sort. */
|
|
68
70
|
async load() {
|
|
69
71
|
if (_filterInstalled) return;
|
|
72
|
+
if (_filterBrand) return Extensions.loadBrandExtensions();
|
|
70
73
|
_loading = true;
|
|
71
74
|
_error = null;
|
|
72
75
|
_emit("extensions:loading");
|
|
@@ -97,18 +100,22 @@ const ExtensionsStore = (() => {
|
|
|
97
100
|
setQuery(query) {
|
|
98
101
|
_query = (query || "").trim();
|
|
99
102
|
if (_filterInstalled) return Extensions.setFilterInstalled(true);
|
|
103
|
+
if (_filterBrand) return Extensions.loadBrandExtensions();
|
|
100
104
|
return Extensions.load();
|
|
101
105
|
},
|
|
102
106
|
|
|
103
107
|
/** Set the sort order and reload. */
|
|
104
108
|
setSort(sort) {
|
|
105
|
-
_sort = sort || "
|
|
109
|
+
_sort = sort || "downloads";
|
|
110
|
+
if (_filterBrand) return Extensions.loadBrandExtensions();
|
|
111
|
+
if (_filterInstalled) return Extensions.setFilterInstalled(true);
|
|
106
112
|
return Extensions.load();
|
|
107
113
|
},
|
|
108
114
|
|
|
109
115
|
/** Toggle the "installed only" filter — fetches from local store when enabled. */
|
|
110
116
|
async setFilterInstalled(onlyInstalled) {
|
|
111
117
|
_filterInstalled = !!onlyInstalled;
|
|
118
|
+
_filterBrand = false;
|
|
112
119
|
if (_filterInstalled) {
|
|
113
120
|
_loading = true;
|
|
114
121
|
_emit("extensions:loading");
|
|
@@ -132,6 +139,40 @@ const ExtensionsStore = (() => {
|
|
|
132
139
|
}
|
|
133
140
|
},
|
|
134
141
|
|
|
142
|
+
/** Toggle the "brand only" filter — fetches brand-private extensions. */
|
|
143
|
+
async setFilterBrand(onlyBrand) {
|
|
144
|
+
_filterBrand = !!onlyBrand;
|
|
145
|
+
_filterInstalled = false;
|
|
146
|
+
if (_filterBrand) {
|
|
147
|
+
return Extensions.loadBrandExtensions();
|
|
148
|
+
} else {
|
|
149
|
+
return Extensions.load();
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
/** Fetch brand-private extensions from /api/store/extensions/brand. */
|
|
154
|
+
async loadBrandExtensions() {
|
|
155
|
+
_loading = true;
|
|
156
|
+
_error = null;
|
|
157
|
+
_emit("extensions:loading");
|
|
158
|
+
try {
|
|
159
|
+
const res = await fetch("/api/store/extensions/brand");
|
|
160
|
+
const data = await res.json();
|
|
161
|
+
_extensions = data.extensions || [];
|
|
162
|
+
_error = data.warning || null;
|
|
163
|
+
_loading = false;
|
|
164
|
+
_emit("extensions:changed", { extensions: _extensions, warning: _error });
|
|
165
|
+
} catch (e) {
|
|
166
|
+
console.error("[Extensions] loadBrandExtensions failed", e);
|
|
167
|
+
_extensions = [];
|
|
168
|
+
_error = I18n.t("extensions.loadFailed");
|
|
169
|
+
_loading = false;
|
|
170
|
+
_emit("extensions:error", { network: true });
|
|
171
|
+
} finally {
|
|
172
|
+
_loading = false;
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
|
|
135
176
|
/** Open the detail view for one extension (fetches contributes + versions). */
|
|
136
177
|
async loadDetail(id) {
|
|
137
178
|
if (!id) return;
|
|
@@ -159,6 +200,17 @@ const ExtensionsStore = (() => {
|
|
|
159
200
|
}
|
|
160
201
|
},
|
|
161
202
|
|
|
203
|
+
/** Fetch /api/brand/status and return { branded: bool }. */
|
|
204
|
+
async fetchBrandStatus() {
|
|
205
|
+
try {
|
|
206
|
+
const res = await fetch("/api/brand/status");
|
|
207
|
+
const data = await res.json();
|
|
208
|
+
return data;
|
|
209
|
+
} catch (_e) {
|
|
210
|
+
return { branded: false };
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
|
|
162
214
|
/** Close the detail view. */
|
|
163
215
|
closeDetail() {
|
|
164
216
|
_detail = null;
|
|
@@ -218,13 +270,13 @@ const ExtensionsStore = (() => {
|
|
|
218
270
|
},
|
|
219
271
|
|
|
220
272
|
/** Remove an installed extension, then return to the list. */
|
|
221
|
-
async uninstall(id) {
|
|
273
|
+
async uninstall(id, purgeData = false) {
|
|
222
274
|
if (!id) return;
|
|
223
275
|
try {
|
|
224
276
|
const res = await fetch("/api/store/extension", {
|
|
225
277
|
method: "DELETE",
|
|
226
278
|
headers: { "Content-Type": "application/json" },
|
|
227
|
-
body: JSON.stringify({ id }),
|
|
279
|
+
body: JSON.stringify({ id, purge_data: purgeData }),
|
|
228
280
|
});
|
|
229
281
|
const data = await res.json();
|
|
230
282
|
if (!res.ok || !data.ok) throw new Error(data.error || "uninstall failed");
|
|
@@ -209,8 +209,11 @@ const ExtensionsView = (() => {
|
|
|
209
209
|
if (remove) {
|
|
210
210
|
remove.addEventListener("click", async () => {
|
|
211
211
|
const id = remove.getAttribute("data-ext-remove");
|
|
212
|
-
const ok = await Modal.
|
|
213
|
-
|
|
212
|
+
const { ok, checked } = await Modal.confirmWithCheckbox(
|
|
213
|
+
I18n.t("extensions.action.removeConfirm"),
|
|
214
|
+
I18n.t("extensions.action.removePurgeData")
|
|
215
|
+
);
|
|
216
|
+
if (ok) Extensions.uninstall(id, checked);
|
|
214
217
|
});
|
|
215
218
|
}
|
|
216
219
|
|
|
@@ -291,16 +294,31 @@ const ExtensionsView = (() => {
|
|
|
291
294
|
${_renderActions(ext)}
|
|
292
295
|
</div>
|
|
293
296
|
</div>
|
|
294
|
-
${
|
|
297
|
+
${_renderReadme(ext.readme)}
|
|
295
298
|
${_renderVersions(ext.versions)}`;
|
|
296
299
|
}
|
|
297
300
|
|
|
301
|
+
// Renders Markdown readme content. Uses marked.js if available, falls back to plain text.
|
|
302
|
+
function _renderReadme(readme) {
|
|
303
|
+
if (!readme || !readme.trim()) return "";
|
|
304
|
+
const html = typeof marked !== "undefined"
|
|
305
|
+
? marked.parse(readme, { breaks: true, gfm: true })
|
|
306
|
+
: `<pre style="white-space:pre-wrap">${escapeHtml(readme)}</pre>`;
|
|
307
|
+
return `
|
|
308
|
+
<div class="extension-detail-block extension-readme">
|
|
309
|
+
<h3 class="extension-detail-block-title">${escapeHtml(I18n.t("extensions.detail.readme"))}</h3>
|
|
310
|
+
<div class="extension-readme-body">${html}</div>
|
|
311
|
+
</div>`;
|
|
312
|
+
}
|
|
313
|
+
|
|
298
314
|
// Manage buttons for a locally installed extension: enable/disable toggle
|
|
299
315
|
// (always available when installed) plus remove (installed layer only).
|
|
300
316
|
function _renderActions(ext) {
|
|
301
317
|
const id = ext.id != null ? String(ext.id) : (ext.name || ext.slug || "");
|
|
302
318
|
if (!ext.installed) {
|
|
303
|
-
|
|
319
|
+
// Show install button for both marketplace and brand-private (origin=self) extensions,
|
|
320
|
+
// as long as a download_url is available.
|
|
321
|
+
if (ext.download_url) {
|
|
304
322
|
return `
|
|
305
323
|
<div class="extension-detail-actions">
|
|
306
324
|
<button type="button" class="extension-action extension-action-install" data-ext-install="${escapeHtml(id)}">${escapeHtml(I18n.t("extensions.action.install"))}</button>
|
|
@@ -374,24 +392,6 @@ const ExtensionsView = (() => {
|
|
|
374
392
|
</div>`;
|
|
375
393
|
}
|
|
376
394
|
|
|
377
|
-
function _renderEntryPoints(contributes) {
|
|
378
|
-
if (!contributes || !Array.isArray(contributes.panels)) return "";
|
|
379
|
-
const eps = contributes.panels.flatMap((p) => Array.isArray(p.entry_points) ? p.entry_points : []);
|
|
380
|
-
if (eps.length === 0) return "";
|
|
381
|
-
const rows = eps.map((ep) => {
|
|
382
|
-
const slotKey = "extensions.slot." + ep.slot;
|
|
383
|
-
const label = I18n.t(slotKey) !== slotKey ? I18n.t(slotKey) : ep.slot;
|
|
384
|
-
return `<li class="extension-contrib-item"><span class="extension-contrib-title">${escapeHtml(label)}</span></li>`;
|
|
385
|
-
}).join("");
|
|
386
|
-
return `
|
|
387
|
-
<div class="extension-detail-block">
|
|
388
|
-
<h3 class="extension-detail-block-title">${escapeHtml(I18n.t("extensions.detail.entry_points"))}</h3>
|
|
389
|
-
<div class="extension-detail-section">
|
|
390
|
-
<ul class="extension-contrib-list">${rows}</ul>
|
|
391
|
-
</div>
|
|
392
|
-
</div>`;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
395
|
function _renderVersions(versions) {
|
|
396
396
|
if (!Array.isArray(versions) || versions.length === 0) return "";
|
|
397
397
|
const rows = versions.map((v) => {
|
|
@@ -412,6 +412,17 @@ const ExtensionsView = (() => {
|
|
|
412
412
|
</div>`;
|
|
413
413
|
}
|
|
414
414
|
|
|
415
|
+
// Show/hide the brand filter tab based on brand status from the store.
|
|
416
|
+
async function _applyBrandTab() {
|
|
417
|
+
try {
|
|
418
|
+
const data = await Extensions.fetchBrandStatus();
|
|
419
|
+
const brandTab = $("tab-extensions-brand");
|
|
420
|
+
if (brandTab) brandTab.style.display = data.branded ? "" : "none";
|
|
421
|
+
} catch (_e) {
|
|
422
|
+
// On error, keep tab hidden.
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
415
426
|
function _wireDom() {
|
|
416
427
|
if (_domWired) return;
|
|
417
428
|
|
|
@@ -437,10 +448,25 @@ const ExtensionsView = (() => {
|
|
|
437
448
|
btn.addEventListener("click", () => {
|
|
438
449
|
document.querySelectorAll(".extensions-filter-tab").forEach(b => b.classList.remove("extensions-filter-tab-active"));
|
|
439
450
|
btn.classList.add("extensions-filter-tab-active");
|
|
440
|
-
|
|
451
|
+
const filter = btn.dataset.filter;
|
|
452
|
+
if (filter === "installed") {
|
|
453
|
+
Extensions.setFilterInstalled(true);
|
|
454
|
+
} else if (filter === "brand") {
|
|
455
|
+
Extensions.setFilterBrand(true);
|
|
456
|
+
} else {
|
|
457
|
+
Extensions.setFilterInstalled(false);
|
|
458
|
+
}
|
|
441
459
|
});
|
|
442
460
|
});
|
|
443
461
|
|
|
462
|
+
// Subscribe to brand status changes to show/hide the brand tab.
|
|
463
|
+
if (window.Skills && Skills.on) {
|
|
464
|
+
Skills.on("brandStatus:changed", (p) => {
|
|
465
|
+
const brandTab = $("tab-extensions-brand");
|
|
466
|
+
if (brandTab) brandTab.style.display = p.branded ? "" : "none";
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
444
470
|
const list = $("extensions-list");
|
|
445
471
|
if (list) {
|
|
446
472
|
list.addEventListener("click", (e) => {
|
|
@@ -471,8 +497,12 @@ const ExtensionsView = (() => {
|
|
|
471
497
|
}
|
|
472
498
|
|
|
473
499
|
const viewApi = {
|
|
474
|
-
onPanelShow(opts) {
|
|
500
|
+
async onPanelShow(opts) {
|
|
475
501
|
_wireDom();
|
|
502
|
+
// Apply brand tab visibility based on current status (fast path),
|
|
503
|
+
// then also refresh in background so it stays up-to-date.
|
|
504
|
+
_applyBrandTab();
|
|
505
|
+
if (window.Skills && Skills.refreshBrandStatus) Skills.refreshBrandStatus();
|
|
476
506
|
const detailId = opts && opts.detailId;
|
|
477
507
|
if (detailId) {
|
|
478
508
|
Extensions.load();
|
|
@@ -84,6 +84,11 @@ const McpView = (() => {
|
|
|
84
84
|
|
|
85
85
|
data.servers.forEach(server => {
|
|
86
86
|
list.appendChild(_renderCard(server));
|
|
87
|
+
// _renderTools uses getElementById which requires the card to already be
|
|
88
|
+
// in the DOM, so we call it after appendChild (not inside _renderCard).
|
|
89
|
+
if (McpStore.state.isExpanded(server.name) && !server.disabled) {
|
|
90
|
+
_renderTools(server.name);
|
|
91
|
+
}
|
|
87
92
|
});
|
|
88
93
|
}
|
|
89
94
|
|
|
@@ -170,8 +175,6 @@ const McpView = (() => {
|
|
|
170
175
|
card.querySelector(`#toggle-mcp-${CSS.escape(server.name)}`)
|
|
171
176
|
?.addEventListener("change", (ev) => _toggle(server.name, ev.target.checked));
|
|
172
177
|
|
|
173
|
-
if (isExpanded && !server.disabled) _renderTools(server.name);
|
|
174
|
-
|
|
175
178
|
return card;
|
|
176
179
|
}
|
|
177
180
|
|
|
@@ -122,19 +122,23 @@ const NewSessionStore = (() => {
|
|
|
122
122
|
return "Session " + (maxN + 1);
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
async function createSession({ existingSessions } = {}) {
|
|
125
|
+
async function createSession({ existingSessions, useDefaults = false } = {}) {
|
|
126
126
|
if (_state.creating) return null;
|
|
127
127
|
_state.creating = true;
|
|
128
128
|
_emit("newSession:creating", { creating: true });
|
|
129
129
|
|
|
130
130
|
try {
|
|
131
|
-
|
|
131
|
+
// useDefaults=true: quick-create from sidebar — use the default agent and
|
|
132
|
+
// skip advanced options (model / dir) so the session always starts clean.
|
|
133
|
+
const agentId = useDefaults ? "general" : (_state.selectedAgentId || "general");
|
|
132
134
|
const adv = _state.advanced;
|
|
133
135
|
const name = adv.name.trim() || _autoName(existingSessions);
|
|
134
136
|
|
|
135
137
|
const payload = { name, agent_profile: agentId, source: "manual" };
|
|
136
|
-
if (
|
|
137
|
-
|
|
138
|
+
if (!useDefaults) {
|
|
139
|
+
if (adv.workingDir.trim()) payload.working_dir = adv.workingDir.trim();
|
|
140
|
+
if (adv.modelId) payload.model_id = adv.modelId;
|
|
141
|
+
}
|
|
138
142
|
|
|
139
143
|
const res = await fetch("/api/sessions", {
|
|
140
144
|
method: "POST",
|
|
@@ -237,7 +237,11 @@ const VersionView = (() => {
|
|
|
237
237
|
_closePopover();
|
|
238
238
|
if (!_autoReloaded) {
|
|
239
239
|
_autoReloaded = true;
|
|
240
|
-
|
|
240
|
+
// Delay reload to ensure the server is fully ready after restart.
|
|
241
|
+
// /api/version responding OK does not guarantee the full server stack
|
|
242
|
+
// (static assets, routes) is ready; a short extra wait avoids a blank
|
|
243
|
+
// page caused by the browser hitting a not-yet-ready server.
|
|
244
|
+
setTimeout(() => window.location.reload(), 3000);
|
|
241
245
|
}
|
|
242
246
|
return;
|
|
243
247
|
}
|
|
@@ -72,6 +72,17 @@ const WorkspaceStore = (() => {
|
|
|
72
72
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
73
73
|
},
|
|
74
74
|
|
|
75
|
+
async displayPath(entry) {
|
|
76
|
+
const resp = await fetch("/api/file-action", {
|
|
77
|
+
method: "POST",
|
|
78
|
+
headers: { "Content-Type": "application/json" },
|
|
79
|
+
body: JSON.stringify({ path: _absPath(entry.path), action: "display-path" })
|
|
80
|
+
});
|
|
81
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
82
|
+
const data = await resp.json();
|
|
83
|
+
return data.path;
|
|
84
|
+
},
|
|
85
|
+
|
|
75
86
|
async fetchFileBlob(entry) {
|
|
76
87
|
const resp = await fetch("/api/file-action", {
|
|
77
88
|
method: "POST",
|
|
@@ -156,10 +156,16 @@ const WorkspaceView = (() => {
|
|
|
156
156
|
if (existing) existing.remove();
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
function copyPath(entry) {
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
159
|
+
async function copyPath(entry) {
|
|
160
|
+
const fallback = Workspace.state.workingDir.replace(/\/+$/, "") + "/" + entry.path.replace(/^\/+/, "");
|
|
161
|
+
let target = fallback;
|
|
162
|
+
try {
|
|
163
|
+
target = await Workspace.displayPath(entry);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
target = fallback;
|
|
166
|
+
}
|
|
167
|
+
navigator.clipboard.writeText(target).then(() => {
|
|
168
|
+
Modal.toast(target, "info");
|
|
163
169
|
});
|
|
164
170
|
}
|
|
165
171
|
|