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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/lib/clacky/agent/message_compressor_helper.rb +4 -1
  4. data/lib/clacky/agent/session_serializer.rb +1 -0
  5. data/lib/clacky/agent.rb +1 -1
  6. data/lib/clacky/agent_config.rb +4 -2
  7. data/lib/clacky/agent_profile.rb +25 -3
  8. data/lib/clacky/default_extensions/ext-studio/ext.yml +2 -2
  9. data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +42 -33
  10. data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +4 -2
  11. data/lib/clacky/default_extensions/git/ext.yml +3 -0
  12. data/lib/clacky/default_extensions/meeting/ext.yml +3 -0
  13. data/lib/clacky/default_extensions/time_machine/ext.yml +4 -0
  14. data/lib/clacky/default_skills/new/SKILL.md +17 -12
  15. data/lib/clacky/extension/api_extension.rb +2 -0
  16. data/lib/clacky/extension/loader.rb +1 -0
  17. data/lib/clacky/extension/packager.rb +60 -15
  18. data/lib/clacky/extension/verifier.rb +1 -1
  19. data/lib/clacky/message_format/anthropic.rb +1 -1
  20. data/lib/clacky/message_format/bedrock.rb +1 -1
  21. data/lib/clacky/message_format/open_ai.rb +8 -0
  22. data/lib/clacky/providers.rb +61 -0
  23. data/lib/clacky/server/http_server.rb +154 -15
  24. data/lib/clacky/server/web_ui_controller.rb +12 -2
  25. data/lib/clacky/session_manager.rb +1 -0
  26. data/lib/clacky/tools/terminal.rb +30 -3
  27. data/lib/clacky/version.rb +1 -1
  28. data/lib/clacky/web/app.css +301 -171
  29. data/lib/clacky/web/app.js +16 -2
  30. data/lib/clacky/web/features/extensions/store.js +214 -9
  31. data/lib/clacky/web/features/extensions/view.js +156 -11
  32. data/lib/clacky/web/features/new-session/store.js +25 -3
  33. data/lib/clacky/web/features/new-session/view.js +337 -140
  34. data/lib/clacky/web/features/skills/view.js +1 -1
  35. data/lib/clacky/web/i18n.js +29 -11
  36. data/lib/clacky/web/index.html +36 -64
  37. data/lib/clacky/web/sessions.js +2 -1
  38. data/lib/clacky/web/utils.js +1 -0
  39. data/lib/clacky/web/ws-dispatcher.js +39 -3
  40. metadata +1 -1
@@ -13,16 +13,20 @@
13
13
  // ───────────────────────────────────────────────────────────────────────────
14
14
 
15
15
  const NewSessionStore = (() => {
16
- const LS_AGENT_KEY = "openclacky.newSession.lastAgent";
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
- return (data && data.models) || [];
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
- return home.replace(/\/+$/, "") + "/clacky_workspace";
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 = "custom";
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
- select.innerHTML = "";
250
- if (models.length === 0) {
251
- const opt = document.createElement("option");
252
- opt.value = "";
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
- const dirInput = $("new-session-directory");
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 && !dirInput.value) {
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
- btn.disabled = (!hasText && !hasFiles) || NewSessionStore.state.creating;
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, "&amp;").replace(/</g, "&lt;")
617
+ .replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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
- if (!content && !hasFiles) return;
503
-
504
- const initCheckbox = $("new-session-init-project");
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(content) : null;
774
+ const display = hasFiles ? _buildBubbleHtml(finalContent) : null;
520
775
 
521
776
  Sessions.add(session);
522
- Sessions.setPendingMessage(session.id, content, display, files);
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
- _updateInitProjectVisibility();
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
- const toggle = $("new-session-toggle-advanced");
606
- const advanced = $("new-session-advanced");
607
- if (toggle && advanced) {
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
- if (NewSessionStore.state.agents.length === 0) {
667
- await NewSessionStore.loadAgents();
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)}">