openclacky 1.5.12 → 1.5.13
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 +48 -0
- data/lib/clacky/access_key.rb +59 -0
- data/lib/clacky/agent/message_compressor_helper.rb +52 -1
- data/lib/clacky/agent/session_serializer.rb +35 -2
- data/lib/clacky/agent/time_machine.rb +9 -1
- data/lib/clacky/agent.rb +11 -7
- data/lib/clacky/agent_config.rb +40 -0
- data/lib/clacky/agent_profile.rb +5 -5
- data/lib/clacky/billing/platform_billing.rb +84 -1
- data/lib/clacky/brand_config.rb +4 -3
- data/lib/clacky/cli.rb +4 -2
- data/lib/clacky/client.rb +107 -0
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +3 -0
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +40 -21
- data/lib/clacky/default_extensions/git/ext.yml +1 -1
- data/lib/clacky/default_extensions/git/panels/git/view.js +483 -77
- data/lib/clacky/default_extensions/meeting/ext.yml +2 -1
- data/lib/clacky/default_extensions/time_machine/ext.yml +1 -1
- data/lib/clacky/default_extensions/time_machine/panels/time_machine/view.js +186 -208
- data/lib/clacky/extension/packager.rb +4 -2
- data/lib/clacky/extension/verifier.rb +14 -1
- data/lib/clacky/message_format/open_ai.rb +5 -1
- data/lib/clacky/message_format/open_ai_responses.rb +409 -0
- data/lib/clacky/message_history.rb +6 -2
- data/lib/clacky/openai_responses_stream_aggregator.rb +294 -0
- data/lib/clacky/providers.rb +18 -6
- data/lib/clacky/server/dir_picker.rb +154 -0
- data/lib/clacky/server/git_panel.rb +61 -9
- data/lib/clacky/server/http_server.rb +147 -103
- data/lib/clacky/tools/terminal.rb +27 -3
- data/lib/clacky/ui2/markdown_renderer.rb +15 -12
- data/lib/clacky/ui2/strings_cjk_patch.rb +132 -0
- data/lib/clacky/ui2/themes/hacker_theme.rb +1 -1
- data/lib/clacky/ui2/themes/minimal_theme.rb +1 -1
- data/lib/clacky/utils/model_pricing.rb +70 -4
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +412 -108
- data/lib/clacky/web/components/composer.js +45 -7
- data/lib/clacky/web/components/custom-select.js +136 -0
- data/lib/clacky/web/components/mentions.js +58 -2
- data/lib/clacky/web/components/model-picker.js +527 -0
- data/lib/clacky/web/components/onboard.js +29 -55
- data/lib/clacky/web/core/ext.js +38 -0
- data/lib/clacky/web/features/billing/view.js +20 -6
- data/lib/clacky/web/features/extensions/view.js +22 -10
- data/lib/clacky/web/features/new-session/store.js +15 -1
- data/lib/clacky/web/features/new-session/view.js +31 -22
- data/lib/clacky/web/features/trash/view.js +31 -19
- data/lib/clacky/web/features/workspace/store.js +1 -0
- data/lib/clacky/web/features/workspace/view.js +54 -3
- data/lib/clacky/web/i18n.js +73 -11
- data/lib/clacky/web/index.html +92 -37
- data/lib/clacky/web/sessions.js +550 -592
- data/lib/clacky/web/settings.js +163 -190
- data/lib/clacky.rb +3 -0
- metadata +8 -1
data/lib/clacky/web/core/ext.js
CHANGED
|
@@ -379,6 +379,19 @@ Clacky.ext = (() => {
|
|
|
379
379
|
map.set(key, teardown);
|
|
380
380
|
}
|
|
381
381
|
|
|
382
|
+
// Run and forget a single teardown by key. Used when a tab re-renders within
|
|
383
|
+
// the same session (e.g. an eager onAttach re-running) so its previous
|
|
384
|
+
// subscriptions are released before a new one is registered.
|
|
385
|
+
function _runViewTeardown(sessionId, key) {
|
|
386
|
+
if (!sessionId) return;
|
|
387
|
+
const map = _viewTeardowns.get(sessionId);
|
|
388
|
+
if (!map) return;
|
|
389
|
+
const fn = map.get(key);
|
|
390
|
+
if (typeof fn !== "function") return;
|
|
391
|
+
map.delete(key);
|
|
392
|
+
try { fn(); } catch (_e) { /* already guarded by caller */ }
|
|
393
|
+
}
|
|
394
|
+
|
|
382
395
|
// Invoke a renderer's render function. Handles the (container, ctx, runtime)
|
|
383
396
|
// signature used everywhere; if the render returns a function it is stored
|
|
384
397
|
// as the view teardown, run before the next render or on session exit.
|
|
@@ -632,6 +645,31 @@ Clacky.ext = (() => {
|
|
|
632
645
|
body.setAttribute("data-panel", id);
|
|
633
646
|
bodyEls[id] = body;
|
|
634
647
|
bodies.appendChild(body);
|
|
648
|
+
|
|
649
|
+
// Eager init: a tab can run setup (surface a badge, pre-fetch data,
|
|
650
|
+
// start a subscription) before the user ever opens it. onAttach runs
|
|
651
|
+
// immediately with a ctx carrying sessionId + setBadge; returning a
|
|
652
|
+
// function registers it as a teardown run on the next render pass /
|
|
653
|
+
// session switch. Any tab may opt in — no per-extension wiring needed.
|
|
654
|
+
if (typeof entry.tab.onAttach === "function") {
|
|
655
|
+
const attachKey = _mountKey(slot, entry) + "::attach";
|
|
656
|
+
// Re-running within the same session (rare, e.g. a host re-render)
|
|
657
|
+
// must release the previous teardown first to avoid leaks.
|
|
658
|
+
_runViewTeardown(renderCtx.sessionId, attachKey);
|
|
659
|
+
const attachCtx = Object.assign({}, renderCtx, {
|
|
660
|
+
setBadge: (n) => _setTabBadge(tabBtns[id], n),
|
|
661
|
+
});
|
|
662
|
+
let teardown;
|
|
663
|
+
try {
|
|
664
|
+
teardown = entry.tab.onAttach(attachCtx);
|
|
665
|
+
} catch (err) {
|
|
666
|
+
console.error(`[Clacky.ext] onAttach for tab "${id}" threw:`, err);
|
|
667
|
+
teardown = null;
|
|
668
|
+
}
|
|
669
|
+
if (typeof teardown === "function" && renderCtx.sessionId) {
|
|
670
|
+
_rememberViewTeardown(renderCtx.sessionId, attachKey, teardown);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
635
673
|
});
|
|
636
674
|
|
|
637
675
|
container.appendChild(tabBar);
|
|
@@ -122,8 +122,8 @@ const BillingView = (() => {
|
|
|
122
122
|
|
|
123
123
|
const allModels = BillingStore.state.allModels;
|
|
124
124
|
const models = allModels.length > 0 ? allModels : (summary.by_model ? Object.keys(summary.by_model) : []);
|
|
125
|
-
const modelOptions = [`<option value="all">${I18n.t("billing.allModels") || "All Models"}</
|
|
126
|
-
.concat(models.filter(m => m).map(m => `<option value="${_esc(m)}"
|
|
125
|
+
const modelOptions = [`<div class="custom-select-option" data-value="all">${I18n.t("billing.allModels") || "All Models"}</div>`]
|
|
126
|
+
.concat(models.filter(m => m).map(m => `<div class="custom-select-option" data-value="${_esc(m)}">${_esc(m)}</div>`))
|
|
127
127
|
.join("");
|
|
128
128
|
|
|
129
129
|
container.innerHTML = `
|
|
@@ -135,7 +135,15 @@ const BillingView = (() => {
|
|
|
135
135
|
</div>
|
|
136
136
|
<div class="billing-controls">
|
|
137
137
|
<div class="segmented-control">${periodBtns}</div>
|
|
138
|
-
<
|
|
138
|
+
<div class="custom-select-wrapper billing-model-filter" id="billing-model-filter">
|
|
139
|
+
<div class="custom-select-trigger" tabindex="0">
|
|
140
|
+
<span class="custom-select-value">${I18n.t("billing.allModels") || "All Models"}</span>
|
|
141
|
+
<svg class="custom-select-arrow" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
142
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
|
143
|
+
</svg>
|
|
144
|
+
</div>
|
|
145
|
+
<div class="custom-select-dropdown">${modelOptions}</div>
|
|
146
|
+
</div>
|
|
139
147
|
<button id="billing-share-btn" class="billing-share-btn" title="${I18n.t('billing.share.tooltip') || 'Share scorecard'}">
|
|
140
148
|
<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>
|
|
141
149
|
${I18n.t('billing.share.btn') || 'Share scorecard'}
|
|
@@ -207,9 +215,15 @@ const BillingView = (() => {
|
|
|
207
215
|
btn.addEventListener("click", (e) => Billing.setPeriod(e.target.dataset.period));
|
|
208
216
|
});
|
|
209
217
|
|
|
210
|
-
document.getElementById("billing-model-filter")
|
|
211
|
-
|
|
212
|
-
|
|
218
|
+
const billingModelWrap = document.getElementById("billing-model-filter");
|
|
219
|
+
if (billingModelWrap && window.CustomSelect) {
|
|
220
|
+
const sel = window.CustomSelect.init({
|
|
221
|
+
trigger: billingModelWrap.querySelector(".custom-select-trigger"),
|
|
222
|
+
dropdown: billingModelWrap.querySelector(".custom-select-dropdown"),
|
|
223
|
+
onSelect: (value) => Billing.setModel(value)
|
|
224
|
+
});
|
|
225
|
+
sel.setValue(BillingStore.state.currentModel || "all");
|
|
226
|
+
}
|
|
213
227
|
|
|
214
228
|
document.getElementById("billing-share-btn")?.addEventListener("click", _openScorecardShare);
|
|
215
229
|
|
|
@@ -587,18 +587,30 @@ const ExtensionsView = (() => {
|
|
|
587
587
|
});
|
|
588
588
|
}
|
|
589
589
|
|
|
590
|
-
const
|
|
591
|
-
if (
|
|
592
|
-
|
|
593
|
-
|
|
590
|
+
const sortWrap = $("extensions-sort");
|
|
591
|
+
if (sortWrap && window.CustomSelect) {
|
|
592
|
+
const sortDropdown = sortWrap.querySelector(".custom-select-dropdown");
|
|
593
|
+
if (!sortDropdown.__customSelect) {
|
|
594
|
+
window.CustomSelect.init({
|
|
595
|
+
trigger: sortWrap.querySelector(".custom-select-trigger"),
|
|
596
|
+
dropdown: sortDropdown,
|
|
597
|
+
onSelect: (value) => Extensions.setSort(value)
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
sortDropdown.__customSelect.setValue(ExtensionsStore.state.sort);
|
|
594
601
|
}
|
|
595
602
|
|
|
596
|
-
const
|
|
597
|
-
if (
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
603
|
+
const installedWrap = $("extensions-installed-filter");
|
|
604
|
+
if (installedWrap && window.CustomSelect) {
|
|
605
|
+
const installedDropdown = installedWrap.querySelector(".custom-select-dropdown");
|
|
606
|
+
if (!installedDropdown.__customSelect) {
|
|
607
|
+
window.CustomSelect.init({
|
|
608
|
+
trigger: installedWrap.querySelector(".custom-select-trigger"),
|
|
609
|
+
dropdown: installedDropdown,
|
|
610
|
+
onSelect: (value) => Extensions.setOnlyInstalled(value === "installed")
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
installedDropdown.__customSelect.setValue(ExtensionsStore.state.onlyInstalled ? "installed" : "all");
|
|
602
614
|
}
|
|
603
615
|
document.querySelectorAll(".extensions-filter-tab").forEach(btn => {
|
|
604
616
|
btn.addEventListener("click", () => {
|
|
@@ -53,7 +53,9 @@ const NewSessionStore = (() => {
|
|
|
53
53
|
const res = await fetch("/api/agents");
|
|
54
54
|
if (!res.ok) return;
|
|
55
55
|
const data = await res.json();
|
|
56
|
-
|
|
56
|
+
// Hidden agents are still returned (so existing sessions can resolve
|
|
57
|
+
// their display names) but must not appear in the new-session picker.
|
|
58
|
+
_state.agents = ((data && data.agents) || []).filter((a) => !a.hidden);
|
|
57
59
|
|
|
58
60
|
// Restore last-used agent from localStorage; fall back to the first
|
|
59
61
|
// available one (typically "general").
|
|
@@ -206,6 +208,17 @@ const NewSessionStore = (() => {
|
|
|
206
208
|
}
|
|
207
209
|
}
|
|
208
210
|
|
|
211
|
+
async function loadAgentName() {
|
|
212
|
+
try {
|
|
213
|
+
const res = await fetch("/api/profile");
|
|
214
|
+
if (!res.ok) return "";
|
|
215
|
+
const data = await res.json();
|
|
216
|
+
return (data.soul && data.soul.name) || "";
|
|
217
|
+
} catch (e) {
|
|
218
|
+
return "";
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
209
222
|
return {
|
|
210
223
|
get state() { return _state; },
|
|
211
224
|
on: _on,
|
|
@@ -218,6 +231,7 @@ const NewSessionStore = (() => {
|
|
|
218
231
|
loadDefaultDirectory,
|
|
219
232
|
createSession,
|
|
220
233
|
fetchSkillsForAgent,
|
|
234
|
+
loadAgentName,
|
|
221
235
|
reset,
|
|
222
236
|
};
|
|
223
237
|
})();
|
|
@@ -15,10 +15,33 @@ const NewSessionView = (() => {
|
|
|
15
15
|
let _initialized = false;
|
|
16
16
|
let _modelsLoaded = false;
|
|
17
17
|
|
|
18
|
+
let _agentName = "";
|
|
19
|
+
let _agentNamePromise = null;
|
|
20
|
+
|
|
18
21
|
function _isZh() {
|
|
19
22
|
return I18n.lang && I18n.lang().startsWith("zh");
|
|
20
23
|
}
|
|
21
24
|
|
|
25
|
+
function _loadAgentName() {
|
|
26
|
+
if (_agentName) return Promise.resolve(_agentName);
|
|
27
|
+
if (_agentNamePromise) return _agentNamePromise;
|
|
28
|
+
_agentNamePromise = NewSessionStore.loadAgentName()
|
|
29
|
+
.then(name => {
|
|
30
|
+
_agentName = name || "";
|
|
31
|
+
return _agentName;
|
|
32
|
+
})
|
|
33
|
+
.finally(() => { _agentNamePromise = null; });
|
|
34
|
+
return _agentNamePromise;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function _renderTitle() {
|
|
38
|
+
const el = document.querySelector(".new-session-title");
|
|
39
|
+
if (!el) return;
|
|
40
|
+
el.textContent = _agentName
|
|
41
|
+
? I18n.t("sessions.new.title.named", { name: _agentName })
|
|
42
|
+
: I18n.t("sessions.new.title");
|
|
43
|
+
}
|
|
44
|
+
|
|
22
45
|
function _agentLabel(a) {
|
|
23
46
|
if (_isZh() && a.title_zh) return a.title_zh;
|
|
24
47
|
return a.title || a.id;
|
|
@@ -672,32 +695,15 @@ const NewSessionView = (() => {
|
|
|
672
695
|
pop.appendChild(empty);
|
|
673
696
|
return;
|
|
674
697
|
}
|
|
675
|
-
models.forEach((m) => {
|
|
676
|
-
const isDefault = m.type === "default";
|
|
677
|
-
const modelName = m.model || m.id || "";
|
|
678
|
-
|
|
679
|
-
const item = document.createElement("button");
|
|
680
|
-
item.type = "button";
|
|
681
|
-
item.className = "ns-ctx-popover-item sib-model-option" + ((m.id || "") === currentId ? " active" : "");
|
|
682
|
-
item.style.cssText = "display:flex;align-items:center;justify-content:space-between;gap:0.5rem;";
|
|
683
|
-
|
|
684
|
-
const nameSpan = document.createElement("span");
|
|
685
|
-
nameSpan.textContent = modelName;
|
|
686
|
-
item.appendChild(nameSpan);
|
|
687
|
-
|
|
688
|
-
if (isDefault) {
|
|
689
|
-
const badge = document.createElement("span");
|
|
690
|
-
badge.className = "model-badge default";
|
|
691
|
-
badge.textContent = "default";
|
|
692
|
-
item.appendChild(badge);
|
|
693
|
-
}
|
|
694
698
|
|
|
695
|
-
|
|
699
|
+
ModelPicker.populate(pop, {
|
|
700
|
+
models,
|
|
701
|
+
currentId,
|
|
702
|
+
onSelect: (m) => {
|
|
696
703
|
NewSessionStore.updateAdvanced({ modelId: m.id || "" });
|
|
697
704
|
_dismissCtxPopover();
|
|
698
705
|
_renderContextBar();
|
|
699
|
-
}
|
|
700
|
-
pop.appendChild(item);
|
|
706
|
+
},
|
|
701
707
|
});
|
|
702
708
|
});
|
|
703
709
|
}
|
|
@@ -853,6 +859,8 @@ const NewSessionView = (() => {
|
|
|
853
859
|
if (_initialized) return;
|
|
854
860
|
_initialized = true;
|
|
855
861
|
|
|
862
|
+
document.addEventListener("langchange", _renderTitle);
|
|
863
|
+
|
|
856
864
|
NewSessionStore.on("newSession:agents-loaded", _renderAgents);
|
|
857
865
|
NewSessionStore.on("newSession:selection-changed", () => {
|
|
858
866
|
_renderAgents();
|
|
@@ -938,6 +946,7 @@ const NewSessionView = (() => {
|
|
|
938
946
|
|
|
939
947
|
async function onPanelShow() {
|
|
940
948
|
_bindOnce();
|
|
949
|
+
_loadAgentName().then(_renderTitle);
|
|
941
950
|
await NewSessionStore.loadAgents();
|
|
942
951
|
// Must run after loadAgents(): that call restores the remembered
|
|
943
952
|
// projectId from localStorage, which decides whether the working dir is
|
|
@@ -52,26 +52,33 @@ const TrashView = (() => {
|
|
|
52
52
|
// ── Project filter select helpers ────────────────────────────────────
|
|
53
53
|
|
|
54
54
|
function _refreshProjectOptions() {
|
|
55
|
-
const
|
|
56
|
-
if (!
|
|
55
|
+
const wrap = $("trash-filter-project");
|
|
56
|
+
if (!wrap) return;
|
|
57
|
+
const dropdown = wrap.querySelector(".custom-select-dropdown");
|
|
58
|
+
if (!dropdown) return;
|
|
59
|
+
const ctl = dropdown.__customSelect;
|
|
60
|
+
const currentVal = ctl ? ctl.value : "";
|
|
57
61
|
|
|
58
62
|
const projects = (typeof Projects !== "undefined") ? Projects.all() : [];
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
const allLabel = I18n.t ? I18n.t("trash.filterAllProjects") : "所有项目";
|
|
64
|
+
|
|
65
|
+
dropdown.innerHTML = "";
|
|
66
|
+
const all = document.createElement("div");
|
|
67
|
+
all.className = "custom-select-option";
|
|
68
|
+
all.dataset.value = "";
|
|
69
|
+
all.textContent = allLabel;
|
|
70
|
+
dropdown.appendChild(all);
|
|
63
71
|
projects.forEach(p => {
|
|
64
|
-
const opt = document.createElement("
|
|
65
|
-
opt.
|
|
72
|
+
const opt = document.createElement("div");
|
|
73
|
+
opt.className = "custom-select-option";
|
|
74
|
+
opt.dataset.value = p.id;
|
|
66
75
|
opt.textContent = p.name || p.id;
|
|
67
|
-
|
|
76
|
+
dropdown.appendChild(opt);
|
|
68
77
|
});
|
|
69
78
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
} else {
|
|
74
|
-
sel.value = "";
|
|
79
|
+
if (ctl) {
|
|
80
|
+
const restore = (currentVal && projects.some(p => String(p.id) === currentVal)) ? currentVal : "";
|
|
81
|
+
ctl.setValue(restore);
|
|
75
82
|
}
|
|
76
83
|
}
|
|
77
84
|
|
|
@@ -464,11 +471,16 @@ const TrashView = (() => {
|
|
|
464
471
|
});
|
|
465
472
|
|
|
466
473
|
// Project filter select
|
|
467
|
-
const
|
|
468
|
-
if (
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
474
|
+
const projWrap = $("trash-filter-project");
|
|
475
|
+
if (projWrap && window.CustomSelect) {
|
|
476
|
+
const dropdown = projWrap.querySelector(".custom-select-dropdown");
|
|
477
|
+
if (!dropdown.__customSelect) {
|
|
478
|
+
window.CustomSelect.init({
|
|
479
|
+
trigger: projWrap.querySelector(".custom-select-trigger"),
|
|
480
|
+
dropdown: dropdown,
|
|
481
|
+
onSelect: (value) => Trash.setFilterProject(value || null)
|
|
482
|
+
});
|
|
483
|
+
}
|
|
472
484
|
}
|
|
473
485
|
}
|
|
474
486
|
|
|
@@ -110,6 +110,7 @@ const WorkspaceStore = (() => {
|
|
|
110
110
|
body: JSON.stringify({ path: _absPath(entry.path), action: "save", content })
|
|
111
111
|
});
|
|
112
112
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
113
|
+
_emit("workspace:fileSaved", { sessionId: _sessionId, path: entry.path, name: entry.name });
|
|
113
114
|
},
|
|
114
115
|
};
|
|
115
116
|
|
|
@@ -740,6 +740,53 @@ const WorkspaceView = (() => {
|
|
|
740
740
|
return { root, openTab, destroy };
|
|
741
741
|
}
|
|
742
742
|
|
|
743
|
+
// Draggable split between the file tree and the viewer. The tree width is a
|
|
744
|
+
// CSS variable so a drag just rewrites it; the handle only shows once a file
|
|
745
|
+
// is open (mirrors the has-open-files class on .wv-root). Width persists.
|
|
746
|
+
const TREE_MIN_W = 168;
|
|
747
|
+
const TREE_MAX_W = 460;
|
|
748
|
+
const TREE_WIDTH_KEY = "clacky.workspace.treeWidth";
|
|
749
|
+
|
|
750
|
+
function initTreeResize(treePane, handle) {
|
|
751
|
+
try {
|
|
752
|
+
const saved = parseFloat(localStorage.getItem(TREE_WIDTH_KEY));
|
|
753
|
+
if (saved >= TREE_MIN_W && saved <= TREE_MAX_W) {
|
|
754
|
+
treePane.style.setProperty("--wv-tree-width", saved + "px");
|
|
755
|
+
}
|
|
756
|
+
} catch (_e) { /* ignore */ }
|
|
757
|
+
|
|
758
|
+
let dragging = false;
|
|
759
|
+
let startX = 0;
|
|
760
|
+
let startW = 0;
|
|
761
|
+
|
|
762
|
+
const onMove = (e) => {
|
|
763
|
+
if (!dragging) return;
|
|
764
|
+
const w = Math.min(TREE_MAX_W, Math.max(TREE_MIN_W, startW + (e.clientX - startX)));
|
|
765
|
+
treePane.style.setProperty("--wv-tree-width", w + "px");
|
|
766
|
+
};
|
|
767
|
+
const onUp = () => {
|
|
768
|
+
if (!dragging) return;
|
|
769
|
+
dragging = false;
|
|
770
|
+
document.body.style.cursor = "";
|
|
771
|
+
document.body.style.userSelect = "";
|
|
772
|
+
document.removeEventListener("mousemove", onMove);
|
|
773
|
+
document.removeEventListener("mouseup", onUp);
|
|
774
|
+
const w = treePane.getBoundingClientRect().width;
|
|
775
|
+
try { localStorage.setItem(TREE_WIDTH_KEY, String(Math.round(w))); } catch (_e) { /* ignore */ }
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
handle.addEventListener("mousedown", (e) => {
|
|
779
|
+
e.preventDefault();
|
|
780
|
+
dragging = true;
|
|
781
|
+
startX = e.clientX;
|
|
782
|
+
startW = treePane.getBoundingClientRect().width;
|
|
783
|
+
document.body.style.cursor = "col-resize";
|
|
784
|
+
document.body.style.userSelect = "none";
|
|
785
|
+
document.addEventListener("mousemove", onMove);
|
|
786
|
+
document.addEventListener("mouseup", onUp);
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
|
|
743
790
|
// Build the Files tab body for the current session: file tree on the left,
|
|
744
791
|
// tabbed viewer on the right. Mutates `container` and returns a destroy()
|
|
745
792
|
// that frees CodeMirror instances and blob URLs when the tab is re-rendered
|
|
@@ -914,7 +961,11 @@ const WorkspaceView = (() => {
|
|
|
914
961
|
treePane.appendChild(tree);
|
|
915
962
|
treePane.appendChild(searchList);
|
|
916
963
|
|
|
964
|
+
const resizeHandle = el("div", "wv-tree-resize");
|
|
965
|
+
initTreeResize(treePane, resizeHandle);
|
|
966
|
+
|
|
917
967
|
root.appendChild(treePane);
|
|
968
|
+
root.appendChild(resizeHandle);
|
|
918
969
|
root.appendChild(viewer.root);
|
|
919
970
|
container.appendChild(root);
|
|
920
971
|
|
|
@@ -934,14 +985,14 @@ const WorkspaceView = (() => {
|
|
|
934
985
|
return { mount };
|
|
935
986
|
})();
|
|
936
987
|
|
|
937
|
-
// Files is a built-in tab: visible for every session, after
|
|
938
|
-
//
|
|
988
|
+
// Files is a built-in tab: visible for every session, after ext-studio
|
|
989
|
+
// publish (20) and before git (30) / time-machine (40).
|
|
939
990
|
if (window.Clacky && Clacky.ext) {
|
|
940
991
|
Clacky.ext.ui.mountBuiltin("session.aside", (container, ctx) => {
|
|
941
992
|
const viewer = WorkspaceView.mount(container, ctx);
|
|
942
993
|
return () => viewer.destroy();
|
|
943
994
|
}, {
|
|
944
|
-
order:
|
|
995
|
+
order: 25,
|
|
945
996
|
tab: { id: "files", label: () => (typeof I18n !== "undefined" ? I18n.t("workspace.title") : "Files") },
|
|
946
997
|
});
|
|
947
998
|
}
|
data/lib/clacky/web/i18n.js
CHANGED
|
@@ -66,6 +66,7 @@ const I18n = (() => {
|
|
|
66
66
|
|
|
67
67
|
// ── Welcome screen ──
|
|
68
68
|
"sessions.new.title": "Start a new session",
|
|
69
|
+
"sessions.new.title.named": "Hi, {{name}}. Ready to get started?",
|
|
69
70
|
"sessions.new.subtitle": "Pick an agent to chat with, then tell it what you'd like to do.",
|
|
70
71
|
"sessions.new.placeholder": "Type your first message to {{agent}}…",
|
|
71
72
|
"sessions.new.moreOptions": "More options",
|
|
@@ -119,8 +120,9 @@ const I18n = (() => {
|
|
|
119
120
|
"chat.send": "Send",
|
|
120
121
|
"chat.edit.warn": "Editing this message will discard the conversation history, file snapshots, and token costs after it. Continue?",
|
|
121
122
|
"chat.edit.warnSkip": "Don't show this again",
|
|
122
|
-
"chat.empty.title":
|
|
123
|
-
"chat.empty.
|
|
123
|
+
"chat.empty.title": "How can I help you today?",
|
|
124
|
+
"chat.empty.title.named": "Hi, {{name}}. Ready to get started?",
|
|
125
|
+
"chat.empty.subtitle": "Ask a question, or start with a skill.",
|
|
124
126
|
"chat.empty.tip1": "Research, write, and translate",
|
|
125
127
|
"chat.empty.tip2": "Summarize and organize information",
|
|
126
128
|
"chat.empty.tip3": "Upload an image or file for me to analyze",
|
|
@@ -155,6 +157,12 @@ const I18n = (() => {
|
|
|
155
157
|
"sib.dir.default": "Default workspace",
|
|
156
158
|
"sib.dir.current": "Current workspace",
|
|
157
159
|
"sib.dir.inputPlaceholder": "Type or select a directory path",
|
|
160
|
+
"sib.dir.back": "Back",
|
|
161
|
+
"sib.dir.forward": "Forward",
|
|
162
|
+
"sib.dir.refresh": "Refresh",
|
|
163
|
+
"sib.dir.addFavorite": "Add to favorites",
|
|
164
|
+
"sib.dir.removeFavorite": "Remove from favorites",
|
|
165
|
+
"sib.dir.userFavorites": "My favorites",
|
|
158
166
|
"sib.dir.loading": "Loading…",
|
|
159
167
|
"sib.dir.empty": "Empty directory",
|
|
160
168
|
"sib.dir.loadError": "Failed to load",
|
|
@@ -169,6 +177,8 @@ const I18n = (() => {
|
|
|
169
177
|
"sib.dir.places.desktop": "Desktop",
|
|
170
178
|
"sib.dir.places.downloads": "Downloads",
|
|
171
179
|
"sib.dir.places.documents": "Documents",
|
|
180
|
+
"sib.dir.drives": "Drives",
|
|
181
|
+
"sib.dir.places.drive": "{{letter}}:",
|
|
172
182
|
"workspace.title": "Files",
|
|
173
183
|
"workspace.expand": "Open workspace",
|
|
174
184
|
"workspace.collapse": "Collapse workspace",
|
|
@@ -211,9 +221,23 @@ const I18n = (() => {
|
|
|
211
221
|
"changes.save.prefix": "Manual snapshot",
|
|
212
222
|
"changes.save.btn": "Save snapshot",
|
|
213
223
|
"changes.save.saving": "Saving…",
|
|
214
|
-
"changes.save.done": "Saved
|
|
224
|
+
"changes.save.done": "Saved",
|
|
215
225
|
"changes.save.failed": "Save failed",
|
|
216
|
-
"
|
|
226
|
+
"changes.select.all": "All",
|
|
227
|
+
"changes.select.count": "{{n}}/{{total}} selected",
|
|
228
|
+
"changes.save.btnN": "Save {{n}} selected file(s)",
|
|
229
|
+
"changes.save.promptMsg": "Describe this snapshot",
|
|
230
|
+
"changes.diff.loading": "Loading diff…",
|
|
231
|
+
"changes.diff.fail": "Failed to load diff",
|
|
232
|
+
"changes.diff.same": "No content change in this file.",
|
|
233
|
+
"changes.diff.binary": "Binary file - line diff not available.",
|
|
234
|
+
"changes.diff.truncated": "Diff too long - showing first {{n}} lines",
|
|
235
|
+
"changes.restore.title": "Revert this file's changes",
|
|
236
|
+
"changes.restore.confirm": "Discard?",
|
|
237
|
+
"changes.restore.diffBtn": "Discard changes",
|
|
238
|
+
"changes.restore.doing": "Reverting…",
|
|
239
|
+
"changes.restore.done": "Reverted: {{path}}",
|
|
240
|
+
"changes.restore.failed": "Revert failed", "tm.tab": "History",
|
|
217
241
|
"sib.model.tooltip": "Click to switch model",
|
|
218
242
|
"sib.model.tooltip.busy": "Model switching is disabled while the agent is responding",
|
|
219
243
|
"sib.variant.header": "Quick switch",
|
|
@@ -253,6 +277,8 @@ const I18n = (() => {
|
|
|
253
277
|
"sessions.badge.channel": "Channel",
|
|
254
278
|
"sessions.badge.coding": "Coding",
|
|
255
279
|
"sessions.badge.setup": "Setup",
|
|
280
|
+
"sessions.setup.name": "Onboard",
|
|
281
|
+
"sessions.setup.browser.name": "🌐 Browser Setup",
|
|
256
282
|
"sessions.newSession": "New Session",
|
|
257
283
|
"sessions.advancedOptions": "Advanced options",
|
|
258
284
|
"sessions.actions.pin": "Pin",
|
|
@@ -763,6 +789,7 @@ const I18n = (() => {
|
|
|
763
789
|
"settings.models.apiFormat.auto": "Auto (detect from provider & model)",
|
|
764
790
|
"settings.models.apiFormat.anthropicMessages": "Anthropic Messages API",
|
|
765
791
|
"settings.models.apiFormat.openaiCompletions": "OpenAI Chat Completions",
|
|
792
|
+
"settings.models.apiFormat.openaiResponses": "OpenAI Responses API",
|
|
766
793
|
"settings.models.field.getApiKey": "How to get →",
|
|
767
794
|
"settings.models.field.docsGuide.question": "New to AI keys?",
|
|
768
795
|
"settings.models.field.docsGuide.cta": "See the guide →",
|
|
@@ -969,7 +996,7 @@ const I18n = (() => {
|
|
|
969
996
|
"onboard.device.success.credit.label": "Free trial credit:",
|
|
970
997
|
"onboard.device.success.credit.value": "$1.00 (one-time, on us)",
|
|
971
998
|
"onboard.device.success.model.label": "Trial model:",
|
|
972
|
-
"onboard.device.success.note": "During the trial only Gemini 3.
|
|
999
|
+
"onboard.device.success.note": "During the trial only Gemini 3.8 Flash is available — top up any amount to unlock all frontier models on this same key.",
|
|
973
1000
|
"onboard.device.success.btn": "Start using Clacky →",
|
|
974
1001
|
"onboard.manual.toggle": "Configure manually with your own API key",
|
|
975
1002
|
"onboard.provider.custom": "Custom",
|
|
@@ -1212,6 +1239,7 @@ const I18n = (() => {
|
|
|
1212
1239
|
|
|
1213
1240
|
// ── Welcome screen ──
|
|
1214
1241
|
"sessions.new.title": "开始新会话",
|
|
1242
|
+
"sessions.new.title.named": "你好,{{name}}。准备好开始了吗?",
|
|
1215
1243
|
"sessions.new.subtitle": "选一个想聊的 Agent,说说你想做什么。",
|
|
1216
1244
|
"sessions.new.placeholder": "输入第一条消息发给 {{agent}}…",
|
|
1217
1245
|
"sessions.new.moreOptions": "更多选项",
|
|
@@ -1263,8 +1291,9 @@ const I18n = (() => {
|
|
|
1263
1291
|
"chat.send": "发送",
|
|
1264
1292
|
"chat.edit.warn": "编辑此消息将丢弃后续对话历史、文件快照及已产生的 token 费用,是否继续?",
|
|
1265
1293
|
"chat.edit.warnSkip": "不再提示",
|
|
1266
|
-
"chat.empty.title":
|
|
1267
|
-
"chat.empty.
|
|
1294
|
+
"chat.empty.title": "有什么可以帮你?",
|
|
1295
|
+
"chat.empty.title.named": "你好,{{name}}。准备好开始了吗?",
|
|
1296
|
+
"chat.empty.subtitle": "直接提问,或用一个技能开始。",
|
|
1268
1297
|
"chat.empty.tip1": "查资料、写文案、做翻译",
|
|
1269
1298
|
"chat.empty.tip2": "归纳总结、整理信息",
|
|
1270
1299
|
"chat.empty.tip3": "上传图片或文件,让我帮你分析",
|
|
@@ -1299,6 +1328,12 @@ const I18n = (() => {
|
|
|
1299
1328
|
"sib.dir.default": "默认工作目录",
|
|
1300
1329
|
"sib.dir.current": "当前工作目录",
|
|
1301
1330
|
"sib.dir.inputPlaceholder": "输入或选择目录路径",
|
|
1331
|
+
"sib.dir.back": "后退",
|
|
1332
|
+
"sib.dir.forward": "前进",
|
|
1333
|
+
"sib.dir.refresh": "刷新",
|
|
1334
|
+
"sib.dir.addFavorite": "添加到收藏",
|
|
1335
|
+
"sib.dir.removeFavorite": "从收藏中移除",
|
|
1336
|
+
"sib.dir.userFavorites": "我的收藏",
|
|
1302
1337
|
"sib.dir.loading": "加载中…",
|
|
1303
1338
|
"sib.dir.empty": "空目录",
|
|
1304
1339
|
"sib.dir.loadError": "加载失败",
|
|
@@ -1313,6 +1348,8 @@ const I18n = (() => {
|
|
|
1313
1348
|
"sib.dir.places.desktop": "桌面",
|
|
1314
1349
|
"sib.dir.places.downloads": "下载",
|
|
1315
1350
|
"sib.dir.places.documents": "文稿",
|
|
1351
|
+
"sib.dir.drives": "驱动器",
|
|
1352
|
+
"sib.dir.places.drive": "{{letter}}盘",
|
|
1316
1353
|
"workspace.title": "文件",
|
|
1317
1354
|
"workspace.expand": "打开工作区",
|
|
1318
1355
|
"workspace.collapse": "收起工作区",
|
|
@@ -1355,8 +1392,23 @@ const I18n = (() => {
|
|
|
1355
1392
|
"changes.save.prefix": "手动存档",
|
|
1356
1393
|
"changes.save.btn": "存档当前版本",
|
|
1357
1394
|
"changes.save.saving": "正在存档…",
|
|
1358
|
-
"changes.save.done": "
|
|
1395
|
+
"changes.save.done": "已存档",
|
|
1359
1396
|
"changes.save.failed": "存档失败",
|
|
1397
|
+
"changes.select.all": "全选",
|
|
1398
|
+
"changes.select.count": "已选 {{n}}/{{total}}",
|
|
1399
|
+
"changes.save.btnN": "存档选中的 {{n}} 个文件",
|
|
1400
|
+
"changes.save.promptMsg": "给这个版本写句说明",
|
|
1401
|
+
"changes.diff.loading": "正在读取差异…",
|
|
1402
|
+
"changes.diff.fail": "读取差异失败",
|
|
1403
|
+
"changes.diff.same": "这个文件的内容没有变化。",
|
|
1404
|
+
"changes.diff.binary": "二进制文件,无法逐行对比。",
|
|
1405
|
+
"changes.diff.truncated": "差异过大,仅显示前 {{n}} 行",
|
|
1406
|
+
"changes.restore.title": "撤销此文件的改动",
|
|
1407
|
+
"changes.restore.confirm": "确认丢弃?",
|
|
1408
|
+
"changes.restore.diffBtn": "撤销改动",
|
|
1409
|
+
"changes.restore.doing": "正在撤销…",
|
|
1410
|
+
"changes.restore.done": "已撤销:{{path}}",
|
|
1411
|
+
"changes.restore.failed": "撤销失败",
|
|
1360
1412
|
"tm.tab": "时光机",
|
|
1361
1413
|
"sib.model.tooltip": "点击切换模型",
|
|
1362
1414
|
"sib.model.tooltip.busy": "Agent 回复中,暂时无法切换模型",
|
|
@@ -1381,7 +1433,7 @@ const I18n = (() => {
|
|
|
1381
1433
|
"sib.reasoning.medium": "中",
|
|
1382
1434
|
"sib.reasoning.high": "高",
|
|
1383
1435
|
"sib.reasoning.xhigh": "超高",
|
|
1384
|
-
"sib.reasoning.max": "
|
|
1436
|
+
"sib.reasoning.max": "极高",
|
|
1385
1437
|
"sessions.thinking": "思考中…",
|
|
1386
1438
|
"sessions.default_name": "会话 {{n}}",
|
|
1387
1439
|
"sessions.group.cron": "定时任务",
|
|
@@ -1397,6 +1449,8 @@ const I18n = (() => {
|
|
|
1397
1449
|
"sessions.badge.channel": "频道",
|
|
1398
1450
|
"sessions.badge.coding": "Coding",
|
|
1399
1451
|
"sessions.badge.setup": "配置",
|
|
1452
|
+
"sessions.setup.name": "个性化引导",
|
|
1453
|
+
"sessions.setup.browser.name": "🌐 浏览器配置",
|
|
1400
1454
|
"sessions.newSession": "新会话",
|
|
1401
1455
|
"sessions.advancedOptions": "高级选项",
|
|
1402
1456
|
"sessions.loadMore": "加载更多会话",
|
|
@@ -1913,6 +1967,7 @@ const I18n = (() => {
|
|
|
1913
1967
|
"settings.models.apiFormat.auto": "自动(根据服务商和模型检测)",
|
|
1914
1968
|
"settings.models.apiFormat.anthropicMessages": "Anthropic Messages API",
|
|
1915
1969
|
"settings.models.apiFormat.openaiCompletions": "OpenAI Chat Completions",
|
|
1970
|
+
"settings.models.apiFormat.openaiResponses": "OpenAI Responses API",
|
|
1916
1971
|
"settings.models.field.getApiKey": "如何获取 →",
|
|
1917
1972
|
"settings.models.field.docsGuide.question": "不会配置 AI 模型?",
|
|
1918
1973
|
"settings.models.field.docsGuide.cta": "查看指南 →",
|
|
@@ -2119,7 +2174,7 @@ const I18n = (() => {
|
|
|
2119
2174
|
"onboard.device.success.credit.label": "免费试用额度:",
|
|
2120
2175
|
"onboard.device.success.credit.value": "$1.00(一次性赠送)",
|
|
2121
2176
|
"onboard.device.success.model.label": "试用模型:",
|
|
2122
|
-
"onboard.device.success.note": "试用期内仅可使用 Gemini 3.
|
|
2177
|
+
"onboard.device.success.note": "试用期内仅可使用 Gemini 3.8 Flash —— 任意金额充值后,同一 Key 立即解锁全部前沿模型。",
|
|
2123
2178
|
"onboard.device.success.btn": "开始使用",
|
|
2124
2179
|
"onboard.manual.toggle": "使用自己的 API Key 手动配置",
|
|
2125
2180
|
"onboard.provider.custom": "自定义",
|
|
@@ -2311,7 +2366,14 @@ const I18n = (() => {
|
|
|
2311
2366
|
}
|
|
2312
2367
|
};
|
|
2313
2368
|
// ── State ──────────────────────────────────────────────────────────────────
|
|
2314
|
-
|
|
2369
|
+
// On first visit (no saved preference), match the OS/browser language.
|
|
2370
|
+
function _detectLang() {
|
|
2371
|
+
const saved = localStorage.getItem(STORAGE_KEY);
|
|
2372
|
+
if (saved) return TRANSLATIONS[saved] ? saved : DEFAULT_LANG;
|
|
2373
|
+
const nav = (navigator.language || "").toLowerCase();
|
|
2374
|
+
return nav.startsWith("zh") ? "zh" : DEFAULT_LANG;
|
|
2375
|
+
}
|
|
2376
|
+
let _lang = _detectLang();
|
|
2315
2377
|
|
|
2316
2378
|
// ── Core functions ─────────────────────────────────────────────────────────
|
|
2317
2379
|
|