openclacky 1.5.4 → 1.5.5
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 +31 -0
- data/lib/clacky/agent/message_compressor_helper.rb +4 -1
- data/lib/clacky/agent/session_serializer.rb +1 -0
- data/lib/clacky/agent.rb +1 -1
- data/lib/clacky/agent_config.rb +4 -2
- data/lib/clacky/agent_profile.rb +25 -3
- data/lib/clacky/default_extensions/ext-studio/ext.yml +2 -2
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +42 -33
- data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +4 -2
- data/lib/clacky/default_extensions/git/ext.yml +3 -0
- data/lib/clacky/default_extensions/meeting/ext.yml +3 -0
- data/lib/clacky/default_extensions/time_machine/ext.yml +4 -0
- data/lib/clacky/default_skills/new/SKILL.md +17 -12
- data/lib/clacky/extension/api_extension.rb +2 -0
- data/lib/clacky/extension/loader.rb +1 -0
- data/lib/clacky/extension/packager.rb +60 -15
- data/lib/clacky/extension/verifier.rb +1 -1
- data/lib/clacky/message_format/anthropic.rb +1 -1
- data/lib/clacky/message_format/bedrock.rb +1 -1
- data/lib/clacky/message_format/open_ai.rb +8 -0
- data/lib/clacky/providers.rb +61 -0
- data/lib/clacky/server/http_server.rb +154 -15
- data/lib/clacky/server/web_ui_controller.rb +12 -2
- data/lib/clacky/session_manager.rb +1 -0
- data/lib/clacky/tools/terminal.rb +30 -3
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +301 -171
- data/lib/clacky/web/app.js +16 -2
- data/lib/clacky/web/features/extensions/store.js +214 -9
- data/lib/clacky/web/features/extensions/view.js +156 -11
- data/lib/clacky/web/features/new-session/store.js +25 -3
- data/lib/clacky/web/features/new-session/view.js +337 -140
- data/lib/clacky/web/features/skills/view.js +1 -1
- data/lib/clacky/web/i18n.js +29 -11
- data/lib/clacky/web/index.html +36 -64
- data/lib/clacky/web/sessions.js +2 -1
- data/lib/clacky/web/utils.js +1 -0
- data/lib/clacky/web/ws-dispatcher.js +39 -3
- metadata +1 -1
|
@@ -13,16 +13,20 @@
|
|
|
13
13
|
// ───────────────────────────────────────────────────────────────────────────
|
|
14
14
|
|
|
15
15
|
const NewSessionStore = (() => {
|
|
16
|
-
const LS_AGENT_KEY
|
|
16
|
+
const LS_AGENT_KEY = "openclacky.newSession.lastAgent";
|
|
17
|
+
const LS_PROJECT_KEY = "openclacky.newSession.lastProject";
|
|
17
18
|
|
|
18
19
|
const _state = {
|
|
19
20
|
agents: [],
|
|
20
21
|
selectedAgentId: null,
|
|
22
|
+
defaultDir: "",
|
|
23
|
+
models: [],
|
|
21
24
|
advanced: {
|
|
22
25
|
name: "",
|
|
23
26
|
modelId: "",
|
|
24
27
|
workingDir: "",
|
|
25
28
|
initProject: false,
|
|
29
|
+
projectId: null,
|
|
26
30
|
},
|
|
27
31
|
advancedOpen: false,
|
|
28
32
|
creating: false,
|
|
@@ -61,6 +65,10 @@ const NewSessionStore = (() => {
|
|
|
61
65
|
|
|
62
66
|
_emit("newSession:agents-loaded", { agents: _state.agents });
|
|
63
67
|
_emit("newSession:selection-changed", { agentId: _state.selectedAgentId });
|
|
68
|
+
|
|
69
|
+
// Restore last-used project from localStorage.
|
|
70
|
+
const lastProject = localStorage.getItem(LS_PROJECT_KEY);
|
|
71
|
+
if (lastProject) _state.advanced.projectId = lastProject;
|
|
64
72
|
} catch (e) {
|
|
65
73
|
console.error("Failed to load agents:", e);
|
|
66
74
|
}
|
|
@@ -86,6 +94,14 @@ const NewSessionStore = (() => {
|
|
|
86
94
|
|
|
87
95
|
function updateAdvanced(patch) {
|
|
88
96
|
Object.assign(_state.advanced, patch);
|
|
97
|
+
// Persist last-selected project so it's restored on next visit.
|
|
98
|
+
if ("projectId" in patch) {
|
|
99
|
+
if (patch.projectId) {
|
|
100
|
+
localStorage.setItem(LS_PROJECT_KEY, patch.projectId);
|
|
101
|
+
} else {
|
|
102
|
+
localStorage.removeItem(LS_PROJECT_KEY);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
89
105
|
_emit("newSession:advanced-changed", { advanced: _state.advanced });
|
|
90
106
|
}
|
|
91
107
|
|
|
@@ -94,7 +110,9 @@ const NewSessionStore = (() => {
|
|
|
94
110
|
const res = await fetch("/api/config");
|
|
95
111
|
if (!res.ok) return [];
|
|
96
112
|
const data = await res.json();
|
|
97
|
-
|
|
113
|
+
const models = (data && data.models) || [];
|
|
114
|
+
_state.models = models;
|
|
115
|
+
return models;
|
|
98
116
|
} catch (e) {
|
|
99
117
|
console.error("Failed to load models:", e);
|
|
100
118
|
return [];
|
|
@@ -108,7 +126,9 @@ const NewSessionStore = (() => {
|
|
|
108
126
|
const data = await res.json();
|
|
109
127
|
const home = data && data.home;
|
|
110
128
|
if (!home) return "";
|
|
111
|
-
|
|
129
|
+
const dir = home.replace(/\/+$/, "") + "/clacky_workspace";
|
|
130
|
+
_state.defaultDir = dir;
|
|
131
|
+
return dir;
|
|
112
132
|
} catch (e) {
|
|
113
133
|
return "";
|
|
114
134
|
}
|
|
@@ -138,6 +158,7 @@ const NewSessionStore = (() => {
|
|
|
138
158
|
if (!useDefaults) {
|
|
139
159
|
if (adv.workingDir.trim()) payload.working_dir = adv.workingDir.trim();
|
|
140
160
|
if (adv.modelId) payload.model_id = adv.modelId;
|
|
161
|
+
if (adv.projectId) payload.project_id = adv.projectId;
|
|
141
162
|
}
|
|
142
163
|
|
|
143
164
|
const res = await fetch("/api/sessions", {
|
|
@@ -169,6 +190,7 @@ const NewSessionStore = (() => {
|
|
|
169
190
|
function reset() {
|
|
170
191
|
_state.advanced.name = "";
|
|
171
192
|
_state.advanced.initProject = false;
|
|
193
|
+
// projectId is intentionally kept — restored on next visit from localStorage.
|
|
172
194
|
_emit("newSession:reset", {});
|
|
173
195
|
}
|
|
174
196
|
|
|
@@ -120,12 +120,12 @@ const NewSessionView = (() => {
|
|
|
120
120
|
if (!_isBuiltin(a) && a.source === "extension") {
|
|
121
121
|
const badge = document.createElement("span");
|
|
122
122
|
badge.className = "agent-card-badge";
|
|
123
|
-
badge.textContent = "EXT";
|
|
123
|
+
badge.textContent = _isZh() ? "扩展" : "EXT";
|
|
124
124
|
card.appendChild(badge);
|
|
125
125
|
} else if (a.source === "user") {
|
|
126
126
|
const badge = document.createElement("span");
|
|
127
127
|
badge.className = "agent-card-badge agent-card-badge--custom";
|
|
128
|
-
badge.textContent = "
|
|
128
|
+
badge.textContent = _isZh() ? "自定义" : "Custom";
|
|
129
129
|
card.appendChild(badge);
|
|
130
130
|
}
|
|
131
131
|
|
|
@@ -138,7 +138,6 @@ const NewSessionView = (() => {
|
|
|
138
138
|
});
|
|
139
139
|
|
|
140
140
|
_updatePlaceholder();
|
|
141
|
-
_renderAgentPanels();
|
|
142
141
|
_renderStarterPrompts();
|
|
143
142
|
}
|
|
144
143
|
|
|
@@ -202,38 +201,6 @@ const NewSessionView = (() => {
|
|
|
202
201
|
container.style.display = "block";
|
|
203
202
|
}
|
|
204
203
|
|
|
205
|
-
function _renderAgentPanels() {
|
|
206
|
-
const box = $("new-session-agent-panels");
|
|
207
|
-
if (!box) return;
|
|
208
|
-
const agent = NewSessionStore.currentAgent();
|
|
209
|
-
const ext = window.Clacky && Clacky.ext;
|
|
210
|
-
const contrib = (agent && ext && typeof ext.contributionsForAgent === "function")
|
|
211
|
-
? ext.contributionsForAgent(agent.id)
|
|
212
|
-
: null;
|
|
213
|
-
const panels = (contrib && contrib.panels) || [];
|
|
214
|
-
const skills = (contrib && contrib.skills) || [];
|
|
215
|
-
if (!panels.length && !skills.length) {
|
|
216
|
-
box.style.display = "none";
|
|
217
|
-
box.replaceChildren();
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
const zh = _isZh();
|
|
221
|
-
const label = document.createElement("span");
|
|
222
|
-
label.className = "ns-panels-label";
|
|
223
|
-
label.textContent = zh ? "此助手额外提供" : "This agent adds";
|
|
224
|
-
box.replaceChildren(label);
|
|
225
|
-
panels.forEach((p) => box.appendChild(_contribChip((zh && p.title_zh) ? p.title_zh : p.title, "panel")));
|
|
226
|
-
skills.forEach((s) => box.appendChild(_contribChip((zh && s.title_zh) ? s.title_zh : s.title, "skill")));
|
|
227
|
-
box.style.display = "flex";
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function _contribChip(text, kind) {
|
|
231
|
-
const chip = document.createElement("span");
|
|
232
|
-
chip.className = `ns-panel-chip ns-panel-chip--${kind}`;
|
|
233
|
-
chip.textContent = text;
|
|
234
|
-
return chip;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
204
|
function _updatePlaceholder() {
|
|
238
205
|
const input = $("new-session-input");
|
|
239
206
|
if (!input) return;
|
|
@@ -242,58 +209,34 @@ const NewSessionView = (() => {
|
|
|
242
209
|
|
|
243
210
|
async function _populateModels() {
|
|
244
211
|
if (_modelsLoaded) return;
|
|
245
|
-
const select = $("new-session-model");
|
|
246
|
-
if (!select) return;
|
|
247
212
|
const models = await NewSessionStore.loadModels();
|
|
213
|
+
_modelsLoaded = true;
|
|
248
214
|
|
|
249
|
-
|
|
250
|
-
if (
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
opt.textContent = "No models configured";
|
|
254
|
-
select.appendChild(opt);
|
|
255
|
-
_modelsLoaded = true;
|
|
256
|
-
return;
|
|
215
|
+
// Set default modelId in store if not already set
|
|
216
|
+
if (!NewSessionStore.state.advanced.modelId) {
|
|
217
|
+
const def = models.find((m) => m.type === "default") || models[0];
|
|
218
|
+
if (def) NewSessionStore.updateAdvanced({ modelId: def.id || "" });
|
|
257
219
|
}
|
|
258
|
-
|
|
259
|
-
models.forEach((m) => {
|
|
260
|
-
const opt = document.createElement("option");
|
|
261
|
-
opt.value = m.id || "";
|
|
262
|
-
const typeBadge = m.type === "default" ? "[default] " : "";
|
|
263
|
-
opt.textContent = `${typeBadge}${m.model} (${m.api_key_masked})`;
|
|
264
|
-
if (m.type === "default") {
|
|
265
|
-
opt.selected = true;
|
|
266
|
-
NewSessionStore.updateAdvanced({ modelId: opt.value });
|
|
267
|
-
}
|
|
268
|
-
select.appendChild(opt);
|
|
269
|
-
});
|
|
270
|
-
_modelsLoaded = true;
|
|
220
|
+
_renderContextBar();
|
|
271
221
|
}
|
|
272
222
|
|
|
273
223
|
async function _prefillDefaultDir() {
|
|
274
|
-
|
|
275
|
-
if (!dirInput || dirInput.value) return;
|
|
224
|
+
if (NewSessionStore.state.advanced.workingDir) return; // already set
|
|
276
225
|
const home = await NewSessionStore.loadDefaultDirectory();
|
|
277
|
-
if (home
|
|
278
|
-
dirInput.value = home;
|
|
226
|
+
if (home) {
|
|
279
227
|
NewSessionStore.updateAdvanced({ workingDir: home });
|
|
228
|
+
_renderContextBar();
|
|
280
229
|
}
|
|
281
230
|
}
|
|
282
231
|
|
|
283
|
-
function _updateInitProjectVisibility() {
|
|
284
|
-
const field = $("new-session-init-project-field");
|
|
285
|
-
if (!field) return;
|
|
286
|
-
const agent = NewSessionStore.currentAgent();
|
|
287
|
-
field.style.display = agent && agent.id === "coding" ? "block" : "none";
|
|
288
|
-
}
|
|
289
|
-
|
|
290
232
|
function _updateSendButton() {
|
|
291
233
|
const btn = $("new-session-send");
|
|
292
234
|
const input = $("new-session-input");
|
|
293
235
|
if (!btn || !input) return;
|
|
294
236
|
const hasText = input.value.trim().length > 0;
|
|
295
237
|
const hasFiles = _pendingImages.length > 0 || _pendingFiles.length > 0;
|
|
296
|
-
|
|
238
|
+
const initProject = NewSessionStore.state.advanced.initProject === true;
|
|
239
|
+
btn.disabled = (!hasText && !hasFiles && !initProject) || NewSessionStore.state.creating;
|
|
297
240
|
}
|
|
298
241
|
|
|
299
242
|
// ── Attachments (image compression + generic file upload) ───────────────
|
|
@@ -494,15 +437,321 @@ const NewSessionView = (() => {
|
|
|
494
437
|
return html;
|
|
495
438
|
}
|
|
496
439
|
|
|
440
|
+
|
|
441
|
+
// ── Context bar (chips above the input) ──────────────────────────────────
|
|
442
|
+
// Shows: Agent · Model · Working Dir · Session Name (optional)
|
|
443
|
+
// Each chip opens a lightweight popover when clicked.
|
|
444
|
+
|
|
445
|
+
let _ctxPopoverDismiss = null;
|
|
446
|
+
|
|
447
|
+
function _dismissCtxPopover() {
|
|
448
|
+
if (_ctxPopoverDismiss) { _ctxPopoverDismiss(); _ctxPopoverDismiss = null; }
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function _makeChip(iconSvg, label, onClick) {
|
|
452
|
+
const chip = document.createElement("button");
|
|
453
|
+
chip.type = "button";
|
|
454
|
+
chip.className = "ns-ctx-chip";
|
|
455
|
+
|
|
456
|
+
if (iconSvg) {
|
|
457
|
+
const icon = document.createElement("span");
|
|
458
|
+
icon.className = "ns-ctx-chip-icon";
|
|
459
|
+
icon.innerHTML = iconSvg;
|
|
460
|
+
chip.appendChild(icon);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const text = document.createElement("span");
|
|
464
|
+
text.className = "ns-ctx-chip-label";
|
|
465
|
+
text.textContent = label;
|
|
466
|
+
chip.appendChild(text);
|
|
467
|
+
|
|
468
|
+
chip.addEventListener("click", (e) => { e.stopPropagation(); onClick(chip); });
|
|
469
|
+
return chip;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function _showCtxPopover(anchor, buildFn) {
|
|
473
|
+
_dismissCtxPopover();
|
|
474
|
+
|
|
475
|
+
const pop = document.createElement("div");
|
|
476
|
+
pop.className = "ns-ctx-popover";
|
|
477
|
+
buildFn(pop);
|
|
478
|
+
|
|
479
|
+
const rect = anchor.getBoundingClientRect();
|
|
480
|
+
pop.style.position = "fixed";
|
|
481
|
+
pop.style.left = `${rect.left}px`;
|
|
482
|
+
pop.style.top = `${rect.bottom + 6}px`;
|
|
483
|
+
document.body.appendChild(pop);
|
|
484
|
+
|
|
485
|
+
// Flip up if overflows viewport bottom
|
|
486
|
+
const popRect = pop.getBoundingClientRect();
|
|
487
|
+
if (popRect.bottom > window.innerHeight - 8) {
|
|
488
|
+
pop.style.top = `${rect.top - popRect.height - 6}px`;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const dismiss = (e) => {
|
|
492
|
+
if (!pop.contains(e.target) && e.target !== anchor) {
|
|
493
|
+
pop.remove();
|
|
494
|
+
document.removeEventListener("mousedown", dismiss, true);
|
|
495
|
+
_ctxPopoverDismiss = null;
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
document.addEventListener("mousedown", dismiss, true);
|
|
499
|
+
_ctxPopoverDismiss = () => {
|
|
500
|
+
pop.remove();
|
|
501
|
+
document.removeEventListener("mousedown", dismiss, true);
|
|
502
|
+
};
|
|
503
|
+
return pop;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// ── Session name chip — custom modal dialog ──
|
|
507
|
+
// ── Session name chip — modal dialog (reuses standard modal classes) ──
|
|
508
|
+
function _openNamePopover(_anchor) {
|
|
509
|
+
const zh = _isZh();
|
|
510
|
+
const current = NewSessionStore.state.advanced.name || "";
|
|
511
|
+
|
|
512
|
+
return new Promise((resolve) => {
|
|
513
|
+
const overlay = document.createElement("div");
|
|
514
|
+
overlay.className = "modal-overlay";
|
|
515
|
+
overlay.innerHTML = `
|
|
516
|
+
<div class="modal-box sm">
|
|
517
|
+
<div class="modal-header">
|
|
518
|
+
<h3 class="modal-title">${zh ? "会话名称" : "Session Name"}</h3>
|
|
519
|
+
</div>
|
|
520
|
+
<div class="modal-body">
|
|
521
|
+
<div class="modal-field">
|
|
522
|
+
<label class="modal-label">
|
|
523
|
+
<span>${zh ? "名称" : "Name"}</span>
|
|
524
|
+
</label>
|
|
525
|
+
<input type="text" class="modal-input ns-name-modal-input"
|
|
526
|
+
placeholder="${zh ? "留空自动生成" : "Leave empty to auto-generate"}"
|
|
527
|
+
autocomplete="off" spellcheck="false"
|
|
528
|
+
value="${_escHtml(current)}">
|
|
529
|
+
</div>
|
|
530
|
+
</div>
|
|
531
|
+
<div class="modal-footer">
|
|
532
|
+
<button type="button" class="btn-secondary ns-name-modal-cancel">${zh ? "取消" : "Cancel"}</button>
|
|
533
|
+
<button type="button" class="btn-primary ns-name-modal-confirm">${zh ? "确定" : "OK"}</button>
|
|
534
|
+
</div>
|
|
535
|
+
</div>`;
|
|
536
|
+
document.body.appendChild(overlay);
|
|
537
|
+
|
|
538
|
+
const input = overlay.querySelector(".ns-name-modal-input");
|
|
539
|
+
const cancel = overlay.querySelector(".ns-name-modal-cancel");
|
|
540
|
+
const confirm = overlay.querySelector(".ns-name-modal-confirm");
|
|
541
|
+
|
|
542
|
+
setTimeout(() => { input.focus(); input.select(); }, 50);
|
|
543
|
+
|
|
544
|
+
const cleanup = (save) => {
|
|
545
|
+
overlay.remove();
|
|
546
|
+
if (save) {
|
|
547
|
+
NewSessionStore.updateAdvanced({ name: input.value.trim() });
|
|
548
|
+
_renderContextBar();
|
|
549
|
+
}
|
|
550
|
+
resolve(save ? input.value.trim() : null);
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
confirm.addEventListener("click", () => cleanup(true));
|
|
554
|
+
cancel.addEventListener("click", () => cleanup(false));
|
|
555
|
+
overlay.addEventListener("click", (e) => { if (e.target === overlay) cleanup(false); });
|
|
556
|
+
input.addEventListener("keydown", (e) => {
|
|
557
|
+
if (e.key === "Enter") cleanup(true);
|
|
558
|
+
if (e.key === "Escape") cleanup(false);
|
|
559
|
+
});
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// ── Project chip popover — list projects for selection ──
|
|
564
|
+
function _openProjectPopover(anchor) {
|
|
565
|
+
_showCtxPopover(anchor, (pop) => {
|
|
566
|
+
const zh = _isZh();
|
|
567
|
+
const projects = (typeof Projects !== "undefined" && Projects.all) ? Projects.all() : [];
|
|
568
|
+
const currentId = NewSessionStore.state.advanced.projectId;
|
|
569
|
+
|
|
570
|
+
if (!projects.length) {
|
|
571
|
+
const empty = document.createElement("div");
|
|
572
|
+
empty.className = "ns-ctx-popover-empty";
|
|
573
|
+
empty.textContent = zh ? "暂无项目" : "No projects";
|
|
574
|
+
pop.appendChild(empty);
|
|
575
|
+
} else {
|
|
576
|
+
projects.forEach((proj) => {
|
|
577
|
+
const item = document.createElement("button");
|
|
578
|
+
item.type = "button";
|
|
579
|
+
item.className = "ns-ctx-popover-item" + (proj.id === currentId ? " active" : "");
|
|
580
|
+
const folderIcon = `<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="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>`;
|
|
581
|
+
item.innerHTML = folderIcon + `<span style="margin-left:6px">${_escHtml(proj.name)}</span>`;
|
|
582
|
+
item.style.display = "flex";
|
|
583
|
+
item.style.alignItems = "center";
|
|
584
|
+
item.addEventListener("click", () => {
|
|
585
|
+
const dir = proj.working_dir || NewSessionStore.state.defaultDir || "";
|
|
586
|
+
NewSessionStore.updateAdvanced({ projectId: proj.id, workingDir: dir });
|
|
587
|
+
_dismissCtxPopover();
|
|
588
|
+
_renderContextBar();
|
|
589
|
+
});
|
|
590
|
+
pop.appendChild(item);
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Separator + "No project" clear option
|
|
595
|
+
if (projects.length) {
|
|
596
|
+
const sep = document.createElement("div");
|
|
597
|
+
sep.style.cssText = "height:1px;background:var(--color-border-primary);margin:4px 8px";
|
|
598
|
+
pop.appendChild(sep);
|
|
599
|
+
}
|
|
600
|
+
const clearItem = document.createElement("button");
|
|
601
|
+
clearItem.type = "button";
|
|
602
|
+
clearItem.className = "ns-ctx-popover-item" + (!currentId ? " active" : "");
|
|
603
|
+
clearItem.textContent = zh ? "不选项目" : "No project";
|
|
604
|
+
clearItem.addEventListener("click", () => {
|
|
605
|
+
const defaultDir = NewSessionStore.state.defaultDir || "";
|
|
606
|
+
NewSessionStore.updateAdvanced({ projectId: null, workingDir: defaultDir });
|
|
607
|
+
_dismissCtxPopover();
|
|
608
|
+
_renderContextBar();
|
|
609
|
+
});
|
|
610
|
+
pop.appendChild(clearItem);
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function _escHtml(str) {
|
|
615
|
+
return String(str || "")
|
|
616
|
+
.replace(/&/g, "&").replace(/</g, "<")
|
|
617
|
+
.replace(/>/g, ">").replace(/"/g, """);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// ── Model chip popover ──
|
|
621
|
+
function _openModelPopover(anchor) {
|
|
622
|
+
_showCtxPopover(anchor, (pop) => {
|
|
623
|
+
const models = NewSessionStore.state.models || [];
|
|
624
|
+
const currentId = NewSessionStore.state.advanced.modelId;
|
|
625
|
+
|
|
626
|
+
if (!models.length) {
|
|
627
|
+
const empty = document.createElement("div");
|
|
628
|
+
empty.className = "ns-ctx-popover-empty";
|
|
629
|
+
empty.textContent = I18n.t("sessions.modal.model");
|
|
630
|
+
pop.appendChild(empty);
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
models.forEach((m) => {
|
|
634
|
+
const isDefault = m.type === "default";
|
|
635
|
+
const modelName = m.model || m.id || "";
|
|
636
|
+
|
|
637
|
+
const item = document.createElement("button");
|
|
638
|
+
item.type = "button";
|
|
639
|
+
item.className = "ns-ctx-popover-item sib-model-option" + ((m.id || "") === currentId ? " active" : "");
|
|
640
|
+
item.style.cssText = "display:flex;align-items:center;justify-content:space-between;gap:0.5rem;";
|
|
641
|
+
|
|
642
|
+
const nameSpan = document.createElement("span");
|
|
643
|
+
nameSpan.textContent = modelName;
|
|
644
|
+
item.appendChild(nameSpan);
|
|
645
|
+
|
|
646
|
+
if (isDefault) {
|
|
647
|
+
const badge = document.createElement("span");
|
|
648
|
+
badge.className = "model-badge default";
|
|
649
|
+
badge.textContent = "default";
|
|
650
|
+
item.appendChild(badge);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
item.addEventListener("click", () => {
|
|
654
|
+
NewSessionStore.updateAdvanced({ modelId: m.id || "" });
|
|
655
|
+
_dismissCtxPopover();
|
|
656
|
+
_renderContextBar();
|
|
657
|
+
});
|
|
658
|
+
pop.appendChild(item);
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// ── Working dir chip popover ──
|
|
664
|
+
async function _openDirPopover(_anchor) {
|
|
665
|
+
const start = NewSessionStore.state.advanced.workingDir || "";
|
|
666
|
+
const picked = await window.openDirectoryPicker(start, null);
|
|
667
|
+
if (picked) {
|
|
668
|
+
NewSessionStore.updateAdvanced({ workingDir: picked });
|
|
669
|
+
_renderContextBar();
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// ── Init project chip (coding agent only) — toggle directly ──
|
|
674
|
+
function _toggleInitProject(chip) {
|
|
675
|
+
const newVal = !NewSessionStore.state.advanced.initProject;
|
|
676
|
+
NewSessionStore.updateAdvanced({ initProject: newVal });
|
|
677
|
+
chip.classList.toggle("ns-ctx-chip--active", newVal);
|
|
678
|
+
_updateSendButton();
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// ── Render context bar ──
|
|
682
|
+
function _renderContextBar() {
|
|
683
|
+
const bar = $("ns-context-bar");
|
|
684
|
+
if (!bar) return;
|
|
685
|
+
bar.innerHTML = "";
|
|
686
|
+
|
|
687
|
+
const zh = _isZh();
|
|
688
|
+
const agent = NewSessionStore.currentAgent();
|
|
689
|
+
const adv = NewSessionStore.state.advanced;
|
|
690
|
+
|
|
691
|
+
// Project chip
|
|
692
|
+
const projectId = adv.projectId;
|
|
693
|
+
const project = projectId && typeof Projects !== "undefined" && Projects.find
|
|
694
|
+
? Projects.find(projectId)
|
|
695
|
+
: null;
|
|
696
|
+
const projectLabel = project
|
|
697
|
+
? project.name
|
|
698
|
+
: (zh ? "选择项目" : "Project");
|
|
699
|
+
const projectIcon = `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>`;
|
|
700
|
+
const projectChip = _makeChip(projectIcon, projectLabel, _openProjectPopover);
|
|
701
|
+
if (!projectId) projectChip.classList.add("ns-ctx-chip--muted");
|
|
702
|
+
bar.appendChild(projectChip);
|
|
703
|
+
|
|
704
|
+
// Session name chip (2nd position)
|
|
705
|
+
const nameVal = adv.name || "";
|
|
706
|
+
const nameLabel = nameVal || (zh ? "会话名称" : "Name");
|
|
707
|
+
const nameIcon = `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>`;
|
|
708
|
+
const nameChip = _makeChip(nameIcon, nameLabel, _openNamePopover);
|
|
709
|
+
if (!nameVal) nameChip.classList.add("ns-ctx-chip--muted");
|
|
710
|
+
bar.appendChild(nameChip);
|
|
711
|
+
|
|
712
|
+
// Model chip — show selected model name from store (strip prefix/suffix for brevity)
|
|
713
|
+
let modelLabel = zh ? "模型" : "Model";
|
|
714
|
+
const modelId = adv.modelId;
|
|
715
|
+
if (modelId) {
|
|
716
|
+
const models = NewSessionStore.state.models || [];
|
|
717
|
+
const found = models.find((m) => (m.id || "") === modelId);
|
|
718
|
+
if (found) {
|
|
719
|
+
const raw = found.model || found.id || modelId;
|
|
720
|
+
modelLabel = raw.length > 28 ? raw.slice(0, 26) + "…" : raw;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
const modelIcon = `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/><path d="M20 3v4"/><path d="M22 5h-4"/><path d="M4 17v2"/><path d="M5 18H3"/></svg>`;
|
|
724
|
+
bar.appendChild(_makeChip(modelIcon, modelLabel, _openModelPopover));
|
|
725
|
+
|
|
726
|
+
// Working dir chip
|
|
727
|
+
const dirVal = adv.workingDir || "";
|
|
728
|
+
const dirShort = dirVal
|
|
729
|
+
? dirVal.replace(/^.*[\/]([^\/]+[\/][^\/]*)$/, "$1").replace(/^.*[\/]/, "").slice(-32)
|
|
730
|
+
: (zh ? "工作目录" : "Directory");
|
|
731
|
+
const dirIcon = `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>`;
|
|
732
|
+
const dirChip = _makeChip(dirIcon, dirShort || (zh ? "工作目录" : "Directory"), _openDirPopover);
|
|
733
|
+
if (!dirVal) dirChip.classList.add("ns-ctx-chip--muted");
|
|
734
|
+
bar.appendChild(dirChip);
|
|
735
|
+
|
|
736
|
+
// Init project chip — only for coding agent
|
|
737
|
+
if (agent && agent.id === "coding") {
|
|
738
|
+
const active = NewSessionStore.state.advanced.initProject === true;
|
|
739
|
+
const initLabel = zh ? "初始化项目" : "Init project";
|
|
740
|
+
const initIcon = `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>`;
|
|
741
|
+
const initChip = _makeChip(initIcon, initLabel, _toggleInitProject);
|
|
742
|
+
if (active) initChip.classList.add("ns-ctx-chip--active");
|
|
743
|
+
bar.appendChild(initChip);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
497
747
|
async function _submit() {
|
|
498
748
|
const input = $("new-session-input");
|
|
499
749
|
if (!input) return;
|
|
500
750
|
const content = input.value.trim();
|
|
501
751
|
const hasFiles = _pendingImages.length > 0 || _pendingFiles.length > 0;
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
const initProject = initCheckbox && initCheckbox.checked;
|
|
752
|
+
const initProject = NewSessionStore.state.advanced.initProject === true;
|
|
753
|
+
// Allow empty input when initProject is checked — /new will be auto-sent.
|
|
754
|
+
if (!content && !hasFiles && !initProject) return;
|
|
506
755
|
|
|
507
756
|
const btn = $("new-session-send");
|
|
508
757
|
if (btn) btn.disabled = true;
|
|
@@ -515,11 +764,17 @@ const NewSessionView = (() => {
|
|
|
515
764
|
return;
|
|
516
765
|
}
|
|
517
766
|
|
|
767
|
+
// When initProject is selected, send /new as the first message so the
|
|
768
|
+
// coding agent triggers the new-project skill. If the user also typed
|
|
769
|
+
// something, append it as the project description.
|
|
770
|
+
const finalContent = initProject
|
|
771
|
+
? (content ? `/new\n${content}` : "/new")
|
|
772
|
+
: content;
|
|
518
773
|
const files = hasFiles ? _buildPendingFiles() : null;
|
|
519
|
-
const display = hasFiles ? _buildBubbleHtml(
|
|
774
|
+
const display = hasFiles ? _buildBubbleHtml(finalContent) : null;
|
|
520
775
|
|
|
521
776
|
Sessions.add(session);
|
|
522
|
-
Sessions.setPendingMessage(session.id,
|
|
777
|
+
Sessions.setPendingMessage(session.id, finalContent, display, files);
|
|
523
778
|
input.value = "";
|
|
524
779
|
_pendingImages.length = 0;
|
|
525
780
|
_pendingFiles.length = 0;
|
|
@@ -540,7 +795,7 @@ const NewSessionView = (() => {
|
|
|
540
795
|
NewSessionStore.on("newSession:agents-loaded", _renderAgents);
|
|
541
796
|
NewSessionStore.on("newSession:selection-changed", () => {
|
|
542
797
|
_renderAgents();
|
|
543
|
-
|
|
798
|
+
_renderContextBar();
|
|
544
799
|
});
|
|
545
800
|
NewSessionStore.on("newSession:creating", _updateSendButton);
|
|
546
801
|
|
|
@@ -602,73 +857,15 @@ const NewSessionView = (() => {
|
|
|
602
857
|
const sendBtn = $("new-session-send");
|
|
603
858
|
if (sendBtn) sendBtn.addEventListener("click", _submit);
|
|
604
859
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
toggle.addEventListener("click", async () => {
|
|
609
|
-
const open = advanced.hidden;
|
|
610
|
-
advanced.hidden = !open;
|
|
611
|
-
toggle.classList.toggle("is-open", open);
|
|
612
|
-
if (open) {
|
|
613
|
-
await _populateModels();
|
|
614
|
-
await _prefillDefaultDir();
|
|
615
|
-
requestAnimationFrame(() =>
|
|
616
|
-
advanced.scrollIntoView({ behavior: "smooth", block: "end" })
|
|
617
|
-
);
|
|
618
|
-
}
|
|
619
|
-
NewSessionStore.toggleAdvanced(open);
|
|
620
|
-
});
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
const nameInput = $("new-session-name");
|
|
624
|
-
if (nameInput) {
|
|
625
|
-
nameInput.addEventListener("input", (e) =>
|
|
626
|
-
NewSessionStore.updateAdvanced({ name: e.target.value })
|
|
627
|
-
);
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
const modelSelect = $("new-session-model");
|
|
631
|
-
if (modelSelect) {
|
|
632
|
-
modelSelect.addEventListener("change", (e) =>
|
|
633
|
-
NewSessionStore.updateAdvanced({ modelId: e.target.value })
|
|
634
|
-
);
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
const dirInput = $("new-session-directory");
|
|
638
|
-
if (dirInput) {
|
|
639
|
-
dirInput.addEventListener("input", (e) =>
|
|
640
|
-
NewSessionStore.updateAdvanced({ workingDir: e.target.value })
|
|
641
|
-
);
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
const browseBtn = $("new-session-browse-btn");
|
|
645
|
-
if (browseBtn && dirInput) {
|
|
646
|
-
browseBtn.addEventListener("click", async () => {
|
|
647
|
-
const start = dirInput.value.trim();
|
|
648
|
-
const picked = await window.openDirectoryPicker(start, null);
|
|
649
|
-
if (picked) {
|
|
650
|
-
dirInput.value = picked;
|
|
651
|
-
NewSessionStore.updateAdvanced({ workingDir: picked });
|
|
652
|
-
}
|
|
653
|
-
});
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
const initCheckbox = $("new-session-init-project");
|
|
657
|
-
if (initCheckbox) {
|
|
658
|
-
initCheckbox.addEventListener("change", (e) =>
|
|
659
|
-
NewSessionStore.updateAdvanced({ initProject: e.target.checked })
|
|
660
|
-
);
|
|
661
|
-
}
|
|
860
|
+
// Context bar: load models + default dir eagerly, then render chips
|
|
861
|
+
_populateModels().then(() => _renderContextBar());
|
|
862
|
+
_prefillDefaultDir().then(() => _renderContextBar());
|
|
662
863
|
}
|
|
663
864
|
|
|
664
865
|
async function onPanelShow() {
|
|
665
866
|
_bindOnce();
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
} else {
|
|
669
|
-
_renderAgents();
|
|
670
|
-
}
|
|
671
|
-
_updateInitProjectVisibility();
|
|
867
|
+
await NewSessionStore.loadAgents();
|
|
868
|
+
_renderContextBar();
|
|
672
869
|
_updateSendButton();
|
|
673
870
|
const input = $("new-session-input");
|
|
674
871
|
if (input) input.focus();
|
|
@@ -189,7 +189,7 @@ const SkillsView = (() => {
|
|
|
189
189
|
<span class="${badgeClass}">${badgeLabel}</span>
|
|
190
190
|
${skill.invalid ? `<span class="skill-badge skill-badge-invalid">${I18n.t("skills.badge.invalid")}</span>` : ""}
|
|
191
191
|
</div>
|
|
192
|
-
<div class="skill-card-desc">${escapeHtml(description)}</div>
|
|
192
|
+
<div class="skill-card-desc" data-tooltip="${escapeHtml(description)}" data-tooltip-pos="top" data-tooltip-overflow>${escapeHtml(description)}</div>
|
|
193
193
|
</div>
|
|
194
194
|
<div class="skill-card-actions">
|
|
195
195
|
<label class="skill-toggle ${toggleDisabled ? "skill-toggle-disabled" : ""}" data-tooltip="${escapeHtml(toggleTitle)}">
|