openclacky 1.5.11 → 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 +82 -0
- data/lib/clacky/access_key.rb +59 -0
- data/lib/clacky/agent/cost_tracker.rb +2 -0
- data/lib/clacky/agent/message_compressor_helper.rb +53 -2
- data/lib/clacky/agent/session_serializer.rb +72 -19
- data/lib/clacky/agent/time_machine.rb +10 -1
- data/lib/clacky/agent.rb +107 -26
- data/lib/clacky/agent_config.rb +40 -0
- data/lib/clacky/agent_profile.rb +5 -5
- data/lib/clacky/billing/billing_record.rb +3 -0
- data/lib/clacky/billing/billing_store.rb +31 -5
- data/lib/clacky/billing/platform_billing.rb +231 -0
- data/lib/clacky/brand_config.rb +4 -3
- data/lib/clacky/cancel_flag.rb +23 -0
- data/lib/clacky/cli.rb +4 -2
- data/lib/clacky/client.rb +107 -0
- data/lib/clacky/default_extensions/advisor/advisors/general.md +23 -17
- data/lib/clacky/default_extensions/advisor/hooks/advisor.rb +104 -5
- data/lib/clacky/default_extensions/advisor/panels/advisor/view.js +92 -75
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +31 -0
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +74 -24
- data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +16 -37
- data/lib/clacky/default_extensions/git/ext.yml +1 -1
- data/lib/clacky/default_extensions/git/panels/git/view.js +483 -67
- 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/scaffold/templates/full/panels/dashboard/view.js.erb +2 -3
- data/lib/clacky/extension/verifier.rb +14 -1
- data/lib/clacky/fanout.rb +49 -2
- data/lib/clacky/json_ui_controller.rb +3 -2
- 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 +43 -1
- data/lib/clacky/openai_responses_stream_aggregator.rb +294 -0
- data/lib/clacky/plain_ui_controller.rb +15 -5
- data/lib/clacky/providers.rb +24 -7
- data/lib/clacky/rich_ui/rich_ui_controller.rb +2 -2
- data/lib/clacky/server/channel/channel_manager.rb +93 -18
- data/lib/clacky/server/channel/channel_ui_controller.rb +16 -3
- 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 +366 -42
- data/lib/clacky/server/web_ui_controller.rb +7 -5
- data/lib/clacky/session_manager.rb +74 -17
- data/lib/clacky/tools/ask_user.rb +3 -2
- data/lib/clacky/tools/terminal.rb +30 -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/ui2/ui_controller.rb +2 -2
- data/lib/clacky/ui_interface.rb +2 -2
- data/lib/clacky/utils/model_pricing.rb +92 -4
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +915 -142
- data/lib/clacky/web/components/code-editor.js +33 -1
- data/lib/clacky/web/components/composer.js +315 -0
- data/lib/clacky/web/components/custom-select.js +136 -0
- data/lib/clacky/web/components/mentions.js +655 -0
- data/lib/clacky/web/components/model-picker.js +527 -0
- data/lib/clacky/web/components/onboard.js +29 -55
- data/lib/clacky/web/core/aside.js +3 -3
- data/lib/clacky/web/core/ext.js +38 -0
- data/lib/clacky/web/features/billing/store.js +2 -6
- data/lib/clacky/web/features/billing/view.js +32 -16
- 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 +80 -34
- 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 +766 -80
- data/lib/clacky/web/i18n.js +107 -17
- data/lib/clacky/web/index.html +125 -45
- data/lib/clacky/web/sessions.js +704 -635
- data/lib/clacky/web/settings.js +164 -190
- data/lib/clacky/web/skills.js +11 -17
- data/lib/clacky/web/ws-dispatcher.js +11 -33
- data/lib/clacky.rb +4 -0
- metadata +12 -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);
|
|
@@ -112,19 +112,15 @@ const BillingStore = (() => {
|
|
|
112
112
|
|
|
113
113
|
try {
|
|
114
114
|
const modelParam = (_currentModel && _currentModel !== "all") ? `&model=${encodeURIComponent(_currentModel)}` : "";
|
|
115
|
-
const [summaryRes, dailyRes
|
|
115
|
+
const [summaryRes, dailyRes] = await Promise.all([
|
|
116
116
|
fetch(`/api/billing/summary?period=${_currentPeriod}${modelParam}`),
|
|
117
|
-
fetch(`/api/billing/daily?days=30${modelParam}`)
|
|
118
|
-
fetch(`/api/billing/sessions?period=${_currentPeriod}${modelParam}&limit=100`)
|
|
117
|
+
fetch(`/api/billing/daily?days=30${modelParam}`)
|
|
119
118
|
]);
|
|
120
119
|
|
|
121
120
|
_summary = await summaryRes.json();
|
|
122
121
|
const dailyData = await dailyRes.json();
|
|
123
122
|
_daily = dailyData.days || [];
|
|
124
123
|
|
|
125
|
-
const sessionsData = await sessionsRes.json();
|
|
126
|
-
_sessions = sessionsData.sessions || [];
|
|
127
|
-
|
|
128
124
|
if (!_currentModel || _currentModel === "all") {
|
|
129
125
|
_allModels = _summary.by_model ? Object.keys(_summary.by_model) : [];
|
|
130
126
|
}
|
|
@@ -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'}
|
|
@@ -200,10 +208,6 @@ const BillingView = (() => {
|
|
|
200
208
|
<div class="billing-chart-row">
|
|
201
209
|
${_renderCombinedChart()}
|
|
202
210
|
</div>
|
|
203
|
-
|
|
204
|
-
<div class="billing-sessions-row">
|
|
205
|
-
${_renderSessionList()}
|
|
206
|
-
</div>
|
|
207
211
|
</div>
|
|
208
212
|
`;
|
|
209
213
|
|
|
@@ -211,9 +215,15 @@ const BillingView = (() => {
|
|
|
211
215
|
btn.addEventListener("click", (e) => Billing.setPeriod(e.target.dataset.period));
|
|
212
216
|
});
|
|
213
217
|
|
|
214
|
-
document.getElementById("billing-model-filter")
|
|
215
|
-
|
|
216
|
-
|
|
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
|
+
}
|
|
217
227
|
|
|
218
228
|
document.getElementById("billing-share-btn")?.addEventListener("click", _openScorecardShare);
|
|
219
229
|
|
|
@@ -238,7 +248,7 @@ const BillingView = (() => {
|
|
|
238
248
|
function _cacheHitRateOf(summary) {
|
|
239
249
|
const prompt = summary.prompt_tokens || 0;
|
|
240
250
|
const cacheRead = summary.cache_read_tokens || 0;
|
|
241
|
-
return prompt === 0 ? "0" : ((cacheRead / prompt) * 100).toFixed(1);
|
|
251
|
+
return prompt === 0 ? "0" : Math.min((cacheRead / prompt) * 100, 100).toFixed(1);
|
|
242
252
|
}
|
|
243
253
|
|
|
244
254
|
function _heatmapDays() {
|
|
@@ -325,12 +335,18 @@ const BillingView = (() => {
|
|
|
325
335
|
</div>
|
|
326
336
|
`;
|
|
327
337
|
tooltip.style.display = "block";
|
|
328
|
-
tooltip.style.
|
|
329
|
-
|
|
338
|
+
tooltip.style.visibility = "hidden";
|
|
339
|
+
const rect = tooltip.getBoundingClientRect();
|
|
340
|
+
const ovfX = e.clientX + 15 + rect.width - window.innerWidth;
|
|
341
|
+
const ovfY = e.clientY - 10 + rect.height - window.innerHeight;
|
|
342
|
+
tooltip.style.left = ovfX > 0 ? `${e.clientX - 15 - rect.width}px` : `${e.clientX + 15}px`;
|
|
343
|
+
tooltip.style.top = ovfY > 0 ? `${e.clientY - 10 - rect.height}px` : `${e.clientY - 10}px`;
|
|
344
|
+
tooltip.style.visibility = "visible";
|
|
330
345
|
});
|
|
331
346
|
|
|
332
347
|
container.addEventListener("mouseleave", () => {
|
|
333
348
|
tooltip.style.display = "none";
|
|
349
|
+
tooltip.style.visibility = "";
|
|
334
350
|
});
|
|
335
351
|
}
|
|
336
352
|
|
|
@@ -431,7 +447,7 @@ const BillingView = (() => {
|
|
|
431
447
|
const promptTokens = summary.prompt_tokens || 0;
|
|
432
448
|
const completionTokens = summary.completion_tokens || 0;
|
|
433
449
|
const cacheReadTokens = summary.cache_read_tokens || 0;
|
|
434
|
-
const cacheMissTokens = promptTokens - cacheReadTokens;
|
|
450
|
+
const cacheMissTokens = Math.max(promptTokens - cacheReadTokens, 0);
|
|
435
451
|
|
|
436
452
|
return `
|
|
437
453
|
<div class="billing-section billing-token-section">
|
|
@@ -668,7 +684,7 @@ const BillingView = (() => {
|
|
|
668
684
|
const chartBars = recentDays.map((d, i) => {
|
|
669
685
|
const cacheHit = d.cache_read_tokens || 0;
|
|
670
686
|
const totalPrompt = d.prompt_tokens || 0;
|
|
671
|
-
const cacheMiss = totalPrompt - cacheHit;
|
|
687
|
+
const cacheMiss = Math.max(totalPrompt - cacheHit, 0);
|
|
672
688
|
const output = d.completion_tokens || 0;
|
|
673
689
|
const totalInput = totalPrompt;
|
|
674
690
|
const totalTokens = totalInput + output;
|
|
@@ -746,7 +762,7 @@ const BillingView = (() => {
|
|
|
746
762
|
const totalTokens = s.total_tokens || 0;
|
|
747
763
|
const promptTokens = s.prompt_tokens || 0;
|
|
748
764
|
const cacheHit = s.cache_read_tokens || 0;
|
|
749
|
-
const cacheMiss = promptTokens - cacheHit;
|
|
765
|
+
const cacheMiss = Math.max(promptTokens - cacheHit, 0);
|
|
750
766
|
const completionTokens = s.completion_tokens || 0;
|
|
751
767
|
const requests = s.requests || 0;
|
|
752
768
|
const models = (s.models || []).join(", ");
|
|
@@ -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;
|
|
@@ -191,8 +214,7 @@ const NewSessionView = (() => {
|
|
|
191
214
|
btn.addEventListener("click", () => {
|
|
192
215
|
const input = $("new-session-input");
|
|
193
216
|
if (!input) return;
|
|
194
|
-
input
|
|
195
|
-
input.focus();
|
|
217
|
+
Composer.setText(input, zh ? p.zh : p.en);
|
|
196
218
|
_updateSendButton();
|
|
197
219
|
});
|
|
198
220
|
list.appendChild(btn);
|
|
@@ -204,7 +226,7 @@ const NewSessionView = (() => {
|
|
|
204
226
|
function _updatePlaceholder() {
|
|
205
227
|
const input = $("new-session-input");
|
|
206
228
|
if (!input) return;
|
|
207
|
-
input
|
|
229
|
+
Composer.setPlaceholder(input, I18n.t("chat.input.placeholder"));
|
|
208
230
|
}
|
|
209
231
|
|
|
210
232
|
async function _populateModels() {
|
|
@@ -245,10 +267,10 @@ const NewSessionView = (() => {
|
|
|
245
267
|
const btn = $("new-session-send");
|
|
246
268
|
const input = $("new-session-input");
|
|
247
269
|
if (!btn || !input) return;
|
|
248
|
-
const
|
|
270
|
+
const hasContent = Composer.hasContent(input);
|
|
249
271
|
const hasFiles = _pendingImages.length > 0 || _pendingFiles.length > 0;
|
|
250
272
|
const initProject = NewSessionStore.state.advanced.initProject === true;
|
|
251
|
-
btn.disabled = (!
|
|
273
|
+
btn.disabled = (!hasContent && !hasFiles && !initProject) || NewSessionStore.state.creating;
|
|
252
274
|
}
|
|
253
275
|
|
|
254
276
|
// ── Attachments (image compression + generic file upload) ───────────────
|
|
@@ -428,8 +450,18 @@ const NewSessionView = (() => {
|
|
|
428
450
|
];
|
|
429
451
|
}
|
|
430
452
|
|
|
431
|
-
function _buildBubbleHtml(content) {
|
|
453
|
+
function _buildBubbleHtml(content, mentionChips = []) {
|
|
432
454
|
let html = content ? escapeHtml(content) : "";
|
|
455
|
+
if (mentionChips.length > 0) {
|
|
456
|
+
const badges = mentionChips.map((ch) => {
|
|
457
|
+
const path = ch.type === "session"
|
|
458
|
+
? MentionAC.icons.message
|
|
459
|
+
: (ch.type === "directory" ? MentionAC.icons.folder : MentionAC.icons.file);
|
|
460
|
+
return `<span class="msg-mention-badge"><span class="msg-mention-badge-icon">${MentionAC.svgHtml(path, 13)}</span>` +
|
|
461
|
+
`<span class="msg-mention-badge-name">${escapeHtml(ch.name)}</span></span>`;
|
|
462
|
+
}).join(" ");
|
|
463
|
+
html = badges + (html ? "<br>" + html : "");
|
|
464
|
+
}
|
|
433
465
|
if (_pendingImages.length > 0) {
|
|
434
466
|
const thumbs = _pendingImages
|
|
435
467
|
.map((img) => `<img src="${img.dataUrl}" alt="${escapeHtml(img.name)}" class="msg-image-thumb">`)
|
|
@@ -663,32 +695,15 @@ const NewSessionView = (() => {
|
|
|
663
695
|
pop.appendChild(empty);
|
|
664
696
|
return;
|
|
665
697
|
}
|
|
666
|
-
models.forEach((m) => {
|
|
667
|
-
const isDefault = m.type === "default";
|
|
668
|
-
const modelName = m.model || m.id || "";
|
|
669
|
-
|
|
670
|
-
const item = document.createElement("button");
|
|
671
|
-
item.type = "button";
|
|
672
|
-
item.className = "ns-ctx-popover-item sib-model-option" + ((m.id || "") === currentId ? " active" : "");
|
|
673
|
-
item.style.cssText = "display:flex;align-items:center;justify-content:space-between;gap:0.5rem;";
|
|
674
|
-
|
|
675
|
-
const nameSpan = document.createElement("span");
|
|
676
|
-
nameSpan.textContent = modelName;
|
|
677
|
-
item.appendChild(nameSpan);
|
|
678
|
-
|
|
679
|
-
if (isDefault) {
|
|
680
|
-
const badge = document.createElement("span");
|
|
681
|
-
badge.className = "model-badge default";
|
|
682
|
-
badge.textContent = "default";
|
|
683
|
-
item.appendChild(badge);
|
|
684
|
-
}
|
|
685
698
|
|
|
686
|
-
|
|
699
|
+
ModelPicker.populate(pop, {
|
|
700
|
+
models,
|
|
701
|
+
currentId,
|
|
702
|
+
onSelect: (m) => {
|
|
687
703
|
NewSessionStore.updateAdvanced({ modelId: m.id || "" });
|
|
688
704
|
_dismissCtxPopover();
|
|
689
705
|
_renderContextBar();
|
|
690
|
-
}
|
|
691
|
-
pop.appendChild(item);
|
|
706
|
+
},
|
|
692
707
|
});
|
|
693
708
|
});
|
|
694
709
|
}
|
|
@@ -780,11 +795,12 @@ const NewSessionView = (() => {
|
|
|
780
795
|
async function _submit() {
|
|
781
796
|
const input = $("new-session-input");
|
|
782
797
|
if (!input) return;
|
|
783
|
-
const content = input.
|
|
798
|
+
const content = Composer.text(input).trim();
|
|
799
|
+
const mentionChips = Composer.chips(input);
|
|
784
800
|
const hasFiles = _pendingImages.length > 0 || _pendingFiles.length > 0;
|
|
785
801
|
const initProject = NewSessionStore.state.advanced.initProject === true;
|
|
786
802
|
// Allow empty input when initProject is checked — /new will be auto-sent.
|
|
787
|
-
if (!content && !hasFiles && !initProject) return;
|
|
803
|
+
if (!content && mentionChips.length === 0 && !hasFiles && !initProject) return;
|
|
788
804
|
|
|
789
805
|
const btn = $("new-session-send");
|
|
790
806
|
if (btn) btn.disabled = true;
|
|
@@ -803,15 +819,30 @@ const NewSessionView = (() => {
|
|
|
803
819
|
const finalContent = initProject
|
|
804
820
|
? (content ? `/new\n${content}` : "/new")
|
|
805
821
|
: content;
|
|
806
|
-
|
|
807
|
-
|
|
822
|
+
|
|
823
|
+
// Mention chips: file/directory references ride the files payload (agent.run
|
|
824
|
+
// lists directory contents), session references ride references.
|
|
825
|
+
const fileDirChips = mentionChips.filter(c => c.type === "file" || c.type === "directory");
|
|
826
|
+
const references = mentionChips.map(c => {
|
|
827
|
+
const ref = { type: c.type, name: c.name };
|
|
828
|
+
if (c.type === "session") ref.session_id = c.session_id;
|
|
829
|
+
else ref.path = c.path;
|
|
830
|
+
return ref;
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
const files = (hasFiles || fileDirChips.length > 0)
|
|
834
|
+
? _buildPendingFiles().concat(fileDirChips.map(c => ({ name: c.name, path: c.path, reference: true })))
|
|
835
|
+
: null;
|
|
836
|
+
const display = (hasFiles || mentionChips.length > 0)
|
|
837
|
+
? _buildBubbleHtml(finalContent, mentionChips)
|
|
838
|
+
: null;
|
|
808
839
|
|
|
809
840
|
Sessions.add(session);
|
|
810
841
|
if (session.project_id && typeof Projects !== "undefined" && Projects.expand) {
|
|
811
842
|
Projects.expand(session.project_id);
|
|
812
843
|
}
|
|
813
|
-
Sessions.setPendingMessage(session.id, finalContent, display, files);
|
|
814
|
-
input
|
|
844
|
+
Sessions.setPendingMessage(session.id, finalContent, display, files, references);
|
|
845
|
+
Composer.clear(input);
|
|
815
846
|
_pendingImages.length = 0;
|
|
816
847
|
_pendingFiles.length = 0;
|
|
817
848
|
_imageSeq = 0;
|
|
@@ -828,6 +859,8 @@ const NewSessionView = (() => {
|
|
|
828
859
|
if (_initialized) return;
|
|
829
860
|
_initialized = true;
|
|
830
861
|
|
|
862
|
+
document.addEventListener("langchange", _renderTitle);
|
|
863
|
+
|
|
831
864
|
NewSessionStore.on("newSession:agents-loaded", _renderAgents);
|
|
832
865
|
NewSessionStore.on("newSession:selection-changed", () => {
|
|
833
866
|
_renderAgents();
|
|
@@ -841,6 +874,18 @@ const NewSessionView = (() => {
|
|
|
841
874
|
// Enter-to-send + slash-command navigation are owned by SkillAC.attach().
|
|
842
875
|
}
|
|
843
876
|
|
|
877
|
+
// @ mention autocomplete — register before Composer/SkillAC so its keydown
|
|
878
|
+
// stopImmediatePropagation wins over chip deletion / Enter-send.
|
|
879
|
+
if (input && typeof MentionAC !== "undefined") {
|
|
880
|
+
MentionAC.attach({
|
|
881
|
+
input: "new-session-input",
|
|
882
|
+
menu: "ns-mention-autocomplete",
|
|
883
|
+
getWorkingDir: () => NewSessionStore.state.advanced.workingDir || "",
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
// contenteditable composer: chip deletion, plain-text paste, Shift+Enter.
|
|
887
|
+
if (input) Composer.init(input);
|
|
888
|
+
|
|
844
889
|
// Slash-command skill autocomplete for this composer (mirrors chat).
|
|
845
890
|
if (typeof SkillAC !== "undefined" && input) {
|
|
846
891
|
SkillAC.attach({
|
|
@@ -901,6 +946,7 @@ const NewSessionView = (() => {
|
|
|
901
946
|
|
|
902
947
|
async function onPanelShow() {
|
|
903
948
|
_bindOnce();
|
|
949
|
+
_loadAgentName().then(_renderTitle);
|
|
904
950
|
await NewSessionStore.loadAgents();
|
|
905
951
|
// Must run after loadAgents(): that call restores the remembered
|
|
906
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
|
|