clay-server 3.4.0-beta.9 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/lib/daemon.js +58 -58
  2. package/lib/project-connection.js +19 -6
  3. package/lib/project-models.js +220 -0
  4. package/lib/project-session-handoff.js +162 -0
  5. package/lib/project-session-pair.js +14 -7
  6. package/lib/project-session-spawn.js +1 -1
  7. package/lib/project-sessions.js +13 -45
  8. package/lib/project-worker-proposal.js +51 -14
  9. package/lib/project.js +33 -77
  10. package/lib/public/app.js +11 -0
  11. package/lib/public/copilot-avatar.svg +5 -0
  12. package/lib/public/css/filebrowser.css +6 -1
  13. package/lib/public/css/input.css +56 -0
  14. package/lib/public/css/mates.css +6 -0
  15. package/lib/public/css/menus.css +35 -56
  16. package/lib/public/css/pane.css +18 -3
  17. package/lib/public/css/session-actions.css +98 -0
  18. package/lib/public/grok-avatar.svg +4 -0
  19. package/lib/public/index.html +26 -9
  20. package/lib/public/junie-avatar.svg +11 -0
  21. package/lib/public/kimi-avatar.svg +4 -0
  22. package/lib/public/modules/agent-config-selects.js +69 -0
  23. package/lib/public/modules/app-connection.js +6 -0
  24. package/lib/public/modules/app-header.js +0 -22
  25. package/lib/public/modules/app-messages.js +48 -9
  26. package/lib/public/modules/app-panels.js +33 -82
  27. package/lib/public/modules/app-projects.js +2 -0
  28. package/lib/public/modules/app-rendering.js +16 -1
  29. package/lib/public/modules/background-tasks-ui.js +55 -0
  30. package/lib/public/modules/filebrowser-tabs.js +65 -0
  31. package/lib/public/modules/filebrowser.js +23 -3
  32. package/lib/public/modules/mate-sidebar.js +10 -2
  33. package/lib/public/modules/model-picker.js +263 -0
  34. package/lib/public/modules/pane-bridge.js +6 -0
  35. package/lib/public/modules/session-actions.js +294 -0
  36. package/lib/public/modules/sidebar-mobile.js +2 -1
  37. package/lib/public/modules/sidebar-sessions.js +17 -1
  38. package/lib/public/modules/split-pair-ui.js +16 -77
  39. package/lib/public/modules/split-view.js +11 -1
  40. package/lib/public/modules/tools.js +5 -0
  41. package/lib/public/modules/vendor-priority.js +6 -1
  42. package/lib/public/modules/worker-proposal.js +6 -1
  43. package/lib/public/qwen-avatar.svg +4 -0
  44. package/lib/public/style.css +1 -0
  45. package/lib/sdk-bridge.js +57 -34
  46. package/lib/sdk-message-processor.js +26 -4
  47. package/lib/session-handoff-context.js +110 -0
  48. package/lib/session-notes-mcp-server.js +1 -0
  49. package/lib/sessions.js +4 -0
  50. package/lib/ws-schema.js +9 -1
  51. package/lib/yoke/acp-agent-profiles.js +64 -0
  52. package/lib/yoke/adapters/acp.js +2 -1
  53. package/lib/yoke/adapters/claude.js +34 -0
  54. package/lib/yoke/adapters/codex.js +38 -3
  55. package/lib/yoke/adapters/copilot.js +7 -0
  56. package/lib/yoke/adapters/grok.js +7 -0
  57. package/lib/yoke/adapters/junie.js +7 -0
  58. package/lib/yoke/adapters/kimi.js +7 -0
  59. package/lib/yoke/adapters/kiro.js +4 -3
  60. package/lib/yoke/adapters/qwen.js +7 -0
  61. package/lib/yoke/codex-background-tasks.js +84 -0
  62. package/lib/yoke/index.js +43 -14
  63. package/lib/yoke/instructions.js +3 -0
  64. package/lib/yoke/interface.js +21 -0
  65. package/lib/yoke/vendor-registry.js +55 -0
  66. package/package.json +1 -1
@@ -5,8 +5,9 @@ import { refreshIcons } from "./icons.js";
5
5
  import { escapeHtml, showToast } from "./utils.js";
6
6
  import { store } from './store.js';
7
7
  import { getWs } from './ws-ref.js';
8
- import { VENDOR_NAMES } from './app-rendering.js';
8
+ import { VENDOR_NAMES, isExperimentalVendor } from './app-rendering.js';
9
9
  import { reportPaneContext } from './pane-bridge.js';
10
+ import { setupModelPicker, renderModelPicker, requestVendorModels, prepareModelPickerOpen, modelDisplayName } from './model-picker.js';
10
11
 
11
12
  // --- Module-owned state (not in store) ---
12
13
  var sessionUsage = { cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
@@ -20,7 +21,6 @@ var configChipWrap = null;
20
21
  var configChip = null;
21
22
  var configChipLabel = null;
22
23
  var configPopover = null;
23
- var configModelList = null;
24
24
  var configModeList = null;
25
25
  var configEffortSection = null;
26
26
  var configEffortBar = null;
@@ -132,18 +132,6 @@ export function setContextData(v) { contextData = v; }
132
132
 
133
133
  // --- Internal helpers ---
134
134
 
135
- function modelDisplayName(value, models) {
136
- if (!value) return "";
137
- if (models) {
138
- for (var i = 0; i < models.length; i++) {
139
- var m = models[i];
140
- if (typeof m === "string") { if (m === value) return m; }
141
- else if (m.value === value && m.displayName) return m.displayName;
142
- }
143
- }
144
- return value;
145
- }
146
-
147
135
  function modeDisplayName(value) {
148
136
  for (var i = 0; i < MODE_OPTIONS.length; i++) {
149
137
  if (MODE_OPTIONS[i].value === value) return MODE_OPTIONS[i].label;
@@ -198,65 +186,6 @@ function hasBeta(name) {
198
186
  return false;
199
187
  }
200
188
 
201
- function rebuildModelList() {
202
- if (!configModelList) return;
203
- // Picker lock policy is capability-based (yoke midSessionModelSwitch):
204
- // Claude -> free. The SDK supports mid-thread swaps on a live query
205
- // (query.setModel: "Change the model used for subsequent
206
- // responses"), and sdk-bridge already routes set_model to
207
- // queryInstance.setModel. Cache re-priming on switch is a
208
- // cost, not a breakage.
209
- // Codex / Kiro -> locked once the session has history: their threads
210
- // bind the model at creation and a mid-thread change would be
211
- // silently ignored. Picks made before the first message still
212
- // work everywhere (sdk-bridge stores into sm.currentModel when
213
- // there's no active queryInstance).
214
- var modelSection = configModelList.parentElement;
215
- var s = store.snap();
216
- var vendor = s.currentVendor || "claude";
217
- if (modelSection) modelSection.style.display = "";
218
- configModelList.innerHTML = "";
219
-
220
- var caps = s.vendorCapabilities || {};
221
- var lockedForGui = s.activeSessionMode === "gui" && !!s.sessionHasHistory && !caps.midSessionModelSwitch;
222
-
223
- var list = s.currentModels.length > 0 ? s.currentModels : (s.currentModel ? [{ value: s.currentModel, displayName: s.currentModel }] : []);
224
- for (var i = 0; i < list.length; i++) {
225
- var item = list[i];
226
- // Support both object { value, displayName } and plain string formats
227
- var value = typeof item === "string" ? item : (item.value || "");
228
- var label = typeof item === "string" ? item : (item.displayName || value);
229
- var btn = document.createElement("button");
230
- btn.className = "config-radio-item";
231
- if (value === s.currentModel) btn.classList.add("active");
232
- if (lockedForGui) btn.classList.add("locked");
233
- btn.dataset.model = value;
234
- btn.textContent = label;
235
- if (lockedForGui) {
236
- btn.disabled = true;
237
- btn.title = "This vendor binds the model when the session starts. Start a new session to change it.";
238
- } else {
239
- btn.addEventListener("click", function () {
240
- var model = this.dataset.model;
241
- var ws = getWs();
242
- if (ws && ws.readyState === 1) {
243
- ws.send(JSON.stringify({ type: "set_model", model: model }));
244
- }
245
- configPopover.classList.add("hidden");
246
- configChip.classList.remove("active");
247
- });
248
- }
249
- configModelList.appendChild(btn);
250
- }
251
-
252
- if (lockedForGui) {
253
- var hint = document.createElement("div");
254
- hint.className = "config-model-hint";
255
- hint.textContent = "Locked after first message. Start a new session to change models.";
256
- configModelList.appendChild(hint);
257
- }
258
- }
259
-
260
189
  function rebuildModeList() {
261
190
  if (!configModeList) return;
262
191
  configModeList.innerHTML = "";
@@ -422,7 +351,7 @@ export function initPanels() {
422
351
  configChip = $("config-chip");
423
352
  configChipLabel = $("config-chip-label");
424
353
  configPopover = $("config-popover");
425
- configModelList = $("config-model-list");
354
+ setupModelPicker();
426
355
  configModeList = $("config-mode-list");
427
356
  configEffortSection = $("config-effort-section");
428
357
  configEffortBar = $("config-effort-bar");
@@ -445,12 +374,22 @@ export function initPanels() {
445
374
  var vendorBtnCodex = $("vendor-btn-codex");
446
375
  var vendorBtnAntigravity = $("vendor-btn-antigravity");
447
376
  var vendorBtnOpenCode = $("vendor-btn-opencode");
377
+ var vendorBtnKimi = $("vendor-btn-kimi");
378
+ var vendorBtnGrok = $("vendor-btn-grok");
379
+ var vendorBtnCopilot = $("vendor-btn-copilot");
380
+ var vendorBtnQwen = $("vendor-btn-qwen");
381
+ var vendorBtnJunie = $("vendor-btn-junie");
448
382
  var vendorBtnKiro = $("vendor-btn-kiro");
449
383
  var vendorBtns = {
450
384
  claude: vendorBtnClaude,
451
385
  codex: vendorBtnCodex,
452
386
  antigravity: vendorBtnAntigravity,
453
387
  opencode: vendorBtnOpenCode,
388
+ kimi: vendorBtnKimi,
389
+ grok: vendorBtnGrok,
390
+ copilot: vendorBtnCopilot,
391
+ qwen: vendorBtnQwen,
392
+ junie: vendorBtnJunie,
454
393
  kiro: vendorBtnKiro,
455
394
  };
456
395
 
@@ -466,7 +405,10 @@ export function initPanels() {
466
405
  var isInstalled = installed.indexOf(v) !== -1;
467
406
  btn.classList.toggle("active", v === current);
468
407
  btn.classList.toggle("disabled", !isInstalled);
469
- btn.title = isInstalled ? (VENDOR_NAMES[v] || v) : (VENDOR_NAMES[v] || v) + " is not installed";
408
+ btn.classList.toggle("experimental", isExperimentalVendor(v));
409
+ var title = isInstalled ? (VENDOR_NAMES[v] || v) : (VENDOR_NAMES[v] || v) + " is not installed";
410
+ if (isExperimentalVendor(v)) title += " · Experimental integration; not yet validated through direct use testing";
411
+ btn.title = title;
470
412
  }
471
413
  }
472
414
 
@@ -474,7 +416,7 @@ export function initPanels() {
474
416
  if (vendor === (store.get('currentVendor') || "claude")) return;
475
417
  var installed = store.get('installedVendors') || [];
476
418
  if (installed.indexOf(vendor) === -1) return;
477
- store.set({ currentVendor: vendor, currentModel: "", currentModels: [], vendorSelectionLocked: true, sessionVendorBound: true });
419
+ store.set({ currentVendor: vendor, currentModel: "", currentModels: [], vendorCapabilities: {}, vendorSelectionLocked: true, sessionVendorBound: true });
478
420
  var ws = getWs();
479
421
  if (ws) ws.send(JSON.stringify({ type: "set_vendor", vendor: vendor }));
480
422
  }
@@ -483,6 +425,11 @@ export function initPanels() {
483
425
  if (vendorBtnCodex) vendorBtnCodex.addEventListener("click", function() { onVendorClick("codex"); });
484
426
  if (vendorBtnAntigravity) vendorBtnAntigravity.addEventListener("click", function() { onVendorClick("antigravity"); });
485
427
  if (vendorBtnOpenCode) vendorBtnOpenCode.addEventListener("click", function() { onVendorClick("opencode"); });
428
+ if (vendorBtnKimi) vendorBtnKimi.addEventListener("click", function() { onVendorClick("kimi"); });
429
+ if (vendorBtnGrok) vendorBtnGrok.addEventListener("click", function() { onVendorClick("grok"); });
430
+ if (vendorBtnCopilot) vendorBtnCopilot.addEventListener("click", function() { onVendorClick("copilot"); });
431
+ if (vendorBtnQwen) vendorBtnQwen.addEventListener("click", function() { onVendorClick("qwen"); });
432
+ if (vendorBtnJunie) vendorBtnJunie.addEventListener("click", function() { onVendorClick("junie"); });
486
433
  if (vendorBtnKiro) vendorBtnKiro.addEventListener("click", function() { onVendorClick("kiro"); });
487
434
 
488
435
  // --- Reactive UI sync ---
@@ -510,8 +457,7 @@ export function initPanels() {
510
457
 
511
458
  // Vendor changed -> switch model list and current model to match
512
459
  if (state.currentVendor !== prev.currentVendor && state.currentVendor) {
513
- var ws = getWs();
514
- if (ws) ws.send(JSON.stringify({ type: "get_vendor_models", vendor: state.currentVendor }));
460
+ requestVendorModels(state.currentVendor, true);
515
461
  }
516
462
 
517
463
  // config chip
@@ -521,6 +467,12 @@ export function initPanels() {
521
467
  state.currentBetas !== prev.currentBetas ||
522
468
  state.currentThinking !== prev.currentThinking ||
523
469
  state.currentVendor !== prev.currentVendor ||
470
+ state.currentModels !== prev.currentModels ||
471
+ state.vendorCapabilities !== prev.vendorCapabilities ||
472
+ state.modelListStatus !== prev.modelListStatus ||
473
+ state.modelListError !== prev.modelListError ||
474
+ state.modelSelectionPending !== prev.modelSelectionPending ||
475
+ state.modelSelectionError !== prev.modelSelectionError ||
524
476
  state.codexApproval !== prev.codexApproval ||
525
477
  state.codexSandbox !== prev.codexSandbox ||
526
478
  state.codexWebSearch !== prev.codexWebSearch ||
@@ -621,6 +573,7 @@ export function initPanels() {
621
573
  e.stopPropagation();
622
574
  var wasHidden = configPopover.classList.toggle("hidden");
623
575
  configChip.classList.toggle("active", !wasHidden);
576
+ if (!wasHidden) prepareModelPickerOpen();
624
577
  });
625
578
  }
626
579
 
@@ -674,7 +627,7 @@ export function updateConfigChip() {
674
627
  var s = store.snap();
675
628
  var vendor = s.currentVendor || "claude";
676
629
  configChipLabel.textContent = modelDisplayName(s.currentModel, s.currentModels);
677
- rebuildModelList();
630
+ renderModelPicker();
678
631
  rebuildModeList();
679
632
  rebuildEffortBar();
680
633
 
@@ -861,9 +814,7 @@ export function updateContextPanel() {
861
814
  hCtxEl = document.createElement("div");
862
815
  hCtxEl.className = "header-context";
863
816
  hCtxEl.innerHTML = '<div class="header-context-bar"><div class="header-context-fill"></div></div><span class="header-context-label"></span>';
864
- var workerBtn = document.getElementById("header-add-worker-btn");
865
- var contextAnchor = workerBtn && workerBtn.parentNode === statusArea ? workerBtn.nextSibling : statusArea.firstChild;
866
- statusArea.insertBefore(hCtxEl, contextAnchor);
817
+ statusArea.insertBefore(hCtxEl, statusArea.firstChild);
867
818
  hCtxEl.addEventListener("mouseenter", function() {
868
819
  if (store.get('richContextUsage')) {
869
820
  showCtxPopover();
@@ -5,6 +5,7 @@ import { escapeHtml, showToast } from './utils.js';
5
5
  import { refreshIcons } from './icons.js';
6
6
  import { parseEmojis } from './markdown.js';
7
7
  import { store } from './store.js';
8
+ import { resetModelPickerState } from './model-picker.js';
8
9
  import { getWs, setWs } from './ws-ref.js';
9
10
  import { getMessagesEl, getStatusDot } from './dom-refs.js';
10
11
  import { userAvatarUrl } from './avatar.js';
@@ -363,6 +364,7 @@ export function renderProjectList() {
363
364
  }
364
365
 
365
366
  export function resetClientState() {
367
+ resetModelPickerState();
366
368
  if (isSearchOpen()) closeSearch();
367
369
  getMessagesEl().innerHTML = "";
368
370
  store.set({ currentMsgEl: null });
@@ -13,7 +13,7 @@ import { showImageModal, showPasteModal } from './app-misc.js';
13
13
  import { sendMessage, hasSendableContent } from './input.js';
14
14
  import { getChatLayout } from './theme.js';
15
15
  import { getScheduledMsgEl } from './app-rate-limit.js';
16
- export { VENDOR_ORDER } from './vendor-priority.js';
16
+ export { VENDOR_ORDER, EXPERIMENTAL_VENDORS, isExperimentalVendor } from './vendor-priority.js';
17
17
 
18
18
  // Keep these client constants aligned with lib/yoke/vendor-registry.js. The
19
19
  // browser cannot import the server's CommonJS registry directly.
@@ -22,6 +22,11 @@ export var VENDOR_AVATARS = {
22
22
  codex: "/codex-avatar.png",
23
23
  antigravity: "/antigravity-avatar.png",
24
24
  opencode: "/opencode-avatar.svg",
25
+ kimi: "/kimi-avatar.svg",
26
+ grok: "/grok-avatar.svg",
27
+ copilot: "/copilot-avatar.svg",
28
+ qwen: "/qwen-avatar.svg",
29
+ junie: "/junie-avatar.svg",
25
30
  kiro: "/kiro-avatar.svg",
26
31
  };
27
32
  export var VENDOR_NAMES = {
@@ -29,6 +34,11 @@ export var VENDOR_NAMES = {
29
34
  codex: "Codex",
30
35
  antigravity: "Antigravity CLI",
31
36
  opencode: "OpenCode",
37
+ kimi: "Kimi Code",
38
+ grok: "Grok Build",
39
+ copilot: "GitHub Copilot CLI",
40
+ qwen: "Qwen Code",
41
+ junie: "Junie CLI",
32
42
  kiro: "Kiro CLI",
33
43
  };
34
44
  // Display order for every vendor Clay knows about, installed or not. Pickers
@@ -40,6 +50,11 @@ export var VENDOR_HOMEPAGES = {
40
50
  codex: "https://openai.com/codex/",
41
51
  antigravity: "https://antigravity.google/product/antigravity-cli",
42
52
  opencode: "https://opencode.ai/",
53
+ kimi: "https://www.kimi.com/code/",
54
+ grok: "https://docs.x.ai/build/overview",
55
+ copilot: "https://github.com/features/copilot/cli",
56
+ qwen: "https://qwenlm.github.io/qwen-code-docs/",
57
+ junie: "https://junie.jetbrains.com/",
43
58
  kiro: "https://kiro.dev/",
44
59
  };
45
60
  var NEW_MSG_BTN_DEFAULT = "\u2193 Latest";
@@ -0,0 +1,55 @@
1
+ import { store } from './store.js';
2
+ import { getWs } from './ws-ref.js';
3
+ import { escapeHtml } from './utils.js';
4
+
5
+ var expanded = false;
6
+
7
+ export function initBackgroundTasksUi() {
8
+ store.subscribe(function(state, prev) {
9
+ if (state.activeBackgroundTasks !== prev.activeBackgroundTasks) renderBackgroundTasks();
10
+ });
11
+ renderBackgroundTasks();
12
+ }
13
+
14
+ function renderBackgroundTasks() {
15
+ var tasks = store.get('activeBackgroundTasks') || [];
16
+ var existing = document.getElementById('background-tasks-bar');
17
+ if (tasks.length === 0) {
18
+ if (existing) existing.remove();
19
+ expanded = false;
20
+ return;
21
+ }
22
+ var inputArea = document.getElementById('input-area');
23
+ if (!inputArea || !inputArea.parentNode) return;
24
+ var bar = existing || document.createElement('div');
25
+ bar.id = 'background-tasks-bar';
26
+ bar.className = 'background-tasks-bar';
27
+ var taskLabel = tasks.length === 1 ? 'background task' : 'background tasks';
28
+ var html = '<button type="button" class="background-tasks-toggle" aria-expanded="' + expanded + '">' +
29
+ '<span aria-hidden="true">⏳</span><span class="background-tasks-chip">' + tasks.length + '</span>' +
30
+ '<span>' + tasks.length + ' ' + taskLabel + '</span></button>';
31
+ if (expanded) {
32
+ html += '<div class="background-tasks-list">';
33
+ for (var i = 0; i < tasks.length; i++) {
34
+ var task = tasks[i];
35
+ html += '<div class="background-task-row"><span class="background-task-description" title="' + escapeHtml(task.description || '') + '">' +
36
+ escapeHtml(task.description || 'Background task') + '</span><span class="background-task-type">' +
37
+ escapeHtml(task.task_type || 'other') + '</span><button type="button" class="background-task-stop" data-task-id="' +
38
+ escapeHtml(task.task_id || '') + '">Stop</button></div>';
39
+ }
40
+ html += '</div>';
41
+ }
42
+ bar.innerHTML = html;
43
+ if (!existing) inputArea.parentNode.insertBefore(bar, inputArea);
44
+ bar.querySelector('.background-tasks-toggle').addEventListener('click', function() {
45
+ expanded = !expanded;
46
+ renderBackgroundTasks();
47
+ });
48
+ var stopButtons = bar.querySelectorAll('.background-task-stop');
49
+ for (var j = 0; j < stopButtons.length; j++) {
50
+ stopButtons[j].addEventListener('click', function() {
51
+ var ws = getWs();
52
+ if (ws && ws.readyState === 1) ws.send(JSON.stringify({ type: 'stop_task', taskId: this.dataset.taskId }));
53
+ });
54
+ }
55
+ }
@@ -0,0 +1,65 @@
1
+ var tabs = new Map();
2
+ var focusedPath = null;
3
+ var onFocus = null;
4
+ var onEmpty = null;
5
+
6
+ function label(path) {
7
+ var parts = String(path || "").split("/");
8
+ return parts[parts.length - 1] || path;
9
+ }
10
+
11
+ function render() {
12
+ var root = document.getElementById("file-viewer-tabs");
13
+ if (!root) return;
14
+ root.innerHTML = "";
15
+ tabs.forEach(function(value, path) {
16
+ var tab = document.createElement("button");
17
+ tab.type = "button";
18
+ tab.className = "file-viewer-tab" + (path === focusedPath ? " active" : "");
19
+ tab.textContent = label(path);
20
+ tab.title = path;
21
+ tab.addEventListener("click", function() { focusFileViewerTab(path); });
22
+ var close = document.createElement("span");
23
+ close.className = "file-viewer-tab-close";
24
+ close.textContent = "×";
25
+ close.addEventListener("click", function(event) { event.stopPropagation(); closeFileViewerTab(path); });
26
+ tab.appendChild(close);
27
+ root.appendChild(tab);
28
+ });
29
+ }
30
+
31
+ export function initFileViewerTabs(options) {
32
+ onFocus = options && options.onFocus;
33
+ onEmpty = options && options.onEmpty;
34
+ }
35
+
36
+ export function openFileViewerTab(path, data) {
37
+ if (!path) return false;
38
+ var exists = tabs.has(path);
39
+ tabs.set(path, Object.assign(tabs.get(path) || {}, data || {}));
40
+ focusedPath = path;
41
+ render();
42
+ return !exists;
43
+ }
44
+
45
+ export function focusFileViewerTab(path) {
46
+ if (!tabs.has(path)) return false;
47
+ focusedPath = path;
48
+ render();
49
+ if (onFocus) onFocus(path, tabs.get(path));
50
+ return true;
51
+ }
52
+
53
+ export function closeFileViewerTab(path) {
54
+ if (!tabs.has(path)) return;
55
+ var paths = Array.from(tabs.keys());
56
+ var index = paths.indexOf(path);
57
+ tabs.delete(path);
58
+ if (focusedPath === path) focusedPath = paths[index + 1] || paths[index - 1] || null;
59
+ render();
60
+ if (focusedPath && onFocus) onFocus(focusedPath, tabs.get(focusedPath));
61
+ if (!focusedPath && onEmpty) onEmpty();
62
+ }
63
+
64
+ export function focusedFileViewerTab() { return focusedPath; }
65
+ export function clearFileViewerTabs() { tabs.clear(); focusedPath = null; render(); }
@@ -9,6 +9,7 @@ import { copyMarkdownFormatting } from './rich-clipboard.js';
9
9
  import { animateMarkdownChange, beginMarkdownPresentation, cancelMarkdownFollow, isFollowingMarkdown } from './markdown-live-edit.js';
10
10
  import { store } from './store.js';
11
11
  import { enterMarkdownSlides, exitMarkdownSlides, handleMarkdownSlideKey, syncMarkdownSlidesButton, toggleMarkdownSlideLevelMenu } from './markdown-slides.js';
12
+ import { initFileViewerTabs, openFileViewerTab, closeFileViewerTab, focusedFileViewerTab, clearFileViewerTabs } from './filebrowser-tabs.js';
12
13
 
13
14
  var ctx;
14
15
  var showDropHint = function () {};
@@ -30,6 +31,14 @@ var pendingFileAt = null; // callback for pending file-at
30
31
 
31
32
  export function initFileBrowser(_ctx) {
32
33
  ctx = _ctx;
34
+ var mainPanels = document.getElementById("main-panels");
35
+ if (mainPanels && ctx.fileViewerEl) mainPanels.insertBefore(ctx.fileViewerEl, mainPanels.firstChild);
36
+ initFileViewerTabs({
37
+ onFocus: function(path) {
38
+ if (path !== currentFilePath) requestFileContent(path);
39
+ },
40
+ onEmpty: function() { teardownFileViewer(); },
41
+ });
33
42
 
34
43
  // Load material file icons in background
35
44
  initFileIcons().then(function () {
@@ -133,7 +142,7 @@ export function initFileBrowser(_ctx) {
133
142
 
134
143
  // Close button
135
144
  document.getElementById("file-viewer-close").addEventListener("click", function () {
136
- closeFileViewer();
145
+ closeFileViewerTab(focusedFileViewerTab());
137
146
  });
138
147
 
139
148
  // Full-viewport presentation toggle
@@ -448,6 +457,14 @@ function ensureRenderedMarkdown() {
448
457
  }
449
458
 
450
459
  export function closeFileViewer() {
460
+ if (focusedFileViewerTab()) {
461
+ closeFileViewerTab(focusedFileViewerTab());
462
+ return;
463
+ }
464
+ teardownFileViewer();
465
+ }
466
+
467
+ function teardownFileViewer() {
451
468
  if (currentFilePath && isFollowingMarkdown(currentFilePath)) cancelMarkdownFollow();
452
469
  sendUnwatch();
453
470
  inlineDiffActive = false;
@@ -475,8 +492,8 @@ export function setFileViewerFullscreen(enabled) {
475
492
  }
476
493
 
477
494
  export function resetFileBrowser() {
478
- // Close viewer
479
- closeFileViewer();
495
+ clearFileViewerTabs();
496
+ teardownFileViewer();
480
497
  // Clear all cached state
481
498
  treeData = {};
482
499
  currentContent = null;
@@ -513,6 +530,7 @@ export function openFile(filePath, opts) {
513
530
  cancelMarkdownFollow();
514
531
  }
515
532
  pendingRenderedOpen = !!(opts && opts.rendered);
533
+ openFileViewerTab(filePath);
516
534
  if (opts && opts.diff) {
517
535
  pendingOpenMode = { type: "diff", oldStr: opts.diff.oldStr, newStr: opts.diff.newStr };
518
536
  } else {
@@ -540,6 +558,7 @@ export function openWorkingTreeDiff(diff) {
540
558
 
541
559
  export function presentMarkdownEdit(msg) {
542
560
  if (!msg || !beginMarkdownPresentation(msg.path)) return;
561
+ openFileViewerTab(msg.path, { content: msg.content });
543
562
  pendingOpenMode = null;
544
563
  pendingRenderedOpen = true;
545
564
  pendingRefresh = false;
@@ -1031,6 +1050,7 @@ function renderEntries(container, entries, depth) {
1031
1050
  // --- File viewer ---
1032
1051
 
1033
1052
  function showFileContent(msg) {
1053
+ openFileViewerTab(msg.path, { content: msg.content });
1034
1054
  var pathEl = document.getElementById("file-viewer-path");
1035
1055
  var bodyEl = document.getElementById("file-viewer-body");
1036
1056
  var renderBtn = document.getElementById("file-viewer-render");
@@ -6,6 +6,7 @@ import { hideKnowledge } from './mate-knowledge.js';
6
6
  import { isSchedulerOpen, closeScheduler } from './scheduler.js';
7
7
  import { hideNotes } from './sticky-notes.js';
8
8
  import { openSearch as openSessionSearch } from './session-search.js';
9
+ import { isExperimentalVendor } from './vendor-priority.js';
9
10
 
10
11
  var getWs = null;
11
12
  var currentMateId = null;
@@ -154,17 +155,24 @@ export function showMateSidebar(mateId, mateData) {
154
155
  codex: "/codex-avatar.png",
155
156
  antigravity: "/antigravity-avatar.png",
156
157
  opencode: "/opencode-avatar.svg",
158
+ kimi: "/kimi-avatar.svg",
159
+ grok: "/grok-avatar.svg",
160
+ copilot: "/copilot-avatar.svg",
161
+ qwen: "/qwen-avatar.svg",
162
+ junie: "/junie-avatar.svg",
157
163
  kiro: "/kiro-avatar.svg",
158
164
  };
159
- var vendorNames = { claude: "Claude Code", codex: "Codex", antigravity: "Antigravity CLI", opencode: "OpenCode", kiro: "Kiro CLI" };
160
- var vendorKeys = ["claude", "codex", "antigravity", "opencode", "kiro"];
165
+ var vendorNames = { claude: "Claude Code", codex: "Codex", grok: "Grok Build", kimi: "Kimi Code", copilot: "GitHub Copilot CLI", qwen: "Qwen Code", junie: "Junie CLI", antigravity: "Antigravity CLI", opencode: "OpenCode", kiro: "Kiro CLI" };
166
+ var vendorKeys = ["claude", "codex", "grok", "kimi", "copilot", "qwen", "junie", "antigravity", "opencode", "kiro"];
161
167
  mateVendorWrap.innerHTML = "";
162
168
  for (var vi = 0; vi < vendorKeys.length; vi++) {
163
169
  var vk = vendorKeys[vi];
164
170
  var vBtn = document.createElement("button");
165
171
  vBtn.className = "mate-vendor-btn" + (vk === mateVendor ? " active" : "");
172
+ if (isExperimentalVendor(vk)) vBtn.classList.add("experimental");
166
173
  if (available.indexOf(vk) === -1) vBtn.classList.add("disabled");
167
174
  vBtn.dataset.vendor = vk;
175
+ vBtn.title = vendorNames[vk] + (isExperimentalVendor(vk) ? " · Experimental integration; not yet validated through direct use testing" : "");
168
176
  vBtn.innerHTML = '<img src="' + vendorIcons[vk] + '" class="mate-vendor-icon" alt="' + vendorNames[vk] + '"><span class="mate-vendor-label">' + vendorNames[vk] + '</span>';
169
177
  vBtn.addEventListener("click", function() {
170
178
  var v = this.dataset.vendor;