clay-server 3.4.0-beta.2 → 3.4.0-beta.21

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 (82) hide show
  1. package/lib/daemon-projects.js +3 -3
  2. package/lib/daemon.js +59 -59
  3. package/lib/project-connection.js +35 -7
  4. package/lib/project-debate.js +4 -0
  5. package/lib/project-mate-interaction.js +21 -1
  6. package/lib/project-memory.js +3 -0
  7. package/lib/project-models.js +220 -0
  8. package/lib/project-session-handoff.js +162 -0
  9. package/lib/project-session-pair.js +14 -7
  10. package/lib/project-session-spawn.js +1 -1
  11. package/lib/project-sessions.js +13 -45
  12. package/lib/project-worker-proposal.js +51 -14
  13. package/lib/project.js +38 -80
  14. package/lib/public/antigravity-avatar.png +0 -0
  15. package/lib/public/app.js +29 -0
  16. package/lib/public/copilot-avatar.svg +5 -0
  17. package/lib/public/css/command-palette.css +22 -2
  18. package/lib/public/css/icon-strip.css +29 -13
  19. package/lib/public/css/input.css +56 -0
  20. package/lib/public/css/mates.css +6 -0
  21. package/lib/public/css/menus.css +38 -25
  22. package/lib/public/css/messages.css +9 -0
  23. package/lib/public/css/pane.css +16 -3
  24. package/lib/public/css/pwa-mobile.css +17 -0
  25. package/lib/public/css/session-actions.css +98 -0
  26. package/lib/public/css/title-bar.css +19 -0
  27. package/lib/public/grok-avatar.svg +4 -0
  28. package/lib/public/index.html +29 -7
  29. package/lib/public/junie-avatar.svg +11 -0
  30. package/lib/public/kimi-avatar.svg +4 -0
  31. package/lib/public/modules/agent-config-selects.js +69 -0
  32. package/lib/public/modules/app-header.js +4 -22
  33. package/lib/public/modules/app-messages.js +54 -11
  34. package/lib/public/modules/app-panels.js +36 -83
  35. package/lib/public/modules/app-projects.js +3 -1
  36. package/lib/public/modules/app-rendering.js +19 -4
  37. package/lib/public/modules/background-tasks-ui.js +55 -0
  38. package/lib/public/modules/mate-sidebar.js +11 -3
  39. package/lib/public/modules/model-picker.js +263 -0
  40. package/lib/public/modules/notifications.js +7 -1
  41. package/lib/public/modules/pane-bridge.js +11 -0
  42. package/lib/public/modules/pane-links.js +23 -0
  43. package/lib/public/modules/project-switcher.js +7 -6
  44. package/lib/public/modules/session-actions.js +294 -0
  45. package/lib/public/modules/sidebar-mates.js +1 -1
  46. package/lib/public/modules/sidebar-mobile.js +2 -1
  47. package/lib/public/modules/sidebar-projects.js +34 -43
  48. package/lib/public/modules/sidebar-sessions.js +20 -6
  49. package/lib/public/modules/split-pair-ui.js +16 -77
  50. package/lib/public/modules/split-view.js +3 -0
  51. package/lib/public/modules/tools.js +6 -1
  52. package/lib/public/modules/vendor-priority.js +14 -0
  53. package/lib/public/modules/vendor-selection.js +20 -0
  54. package/lib/public/modules/worker-proposal.js +6 -1
  55. package/lib/public/modules/worktree-location.js +17 -0
  56. package/lib/public/qwen-avatar.svg +4 -0
  57. package/lib/public/style.css +4 -2
  58. package/lib/sdk-bridge.js +103 -56
  59. package/lib/sdk-message-processor.js +26 -4
  60. package/lib/session-handoff-context.js +110 -0
  61. package/lib/session-notes-mcp-server.js +8 -1
  62. package/lib/sessions.js +4 -0
  63. package/lib/worktree.js +9 -4
  64. package/lib/ws-schema.js +9 -1
  65. package/lib/yoke/acp-agent-profiles.js +64 -14
  66. package/lib/yoke/adapters/antigravity.js +417 -0
  67. package/lib/yoke/adapters/claude.js +34 -0
  68. package/lib/yoke/adapters/codex.js +101 -5
  69. package/lib/yoke/adapters/copilot.js +7 -0
  70. package/lib/yoke/adapters/grok.js +7 -0
  71. package/lib/yoke/adapters/junie.js +7 -0
  72. package/lib/yoke/adapters/kimi.js +7 -0
  73. package/lib/yoke/adapters/kiro.js +2 -2
  74. package/lib/yoke/adapters/qwen.js +7 -0
  75. package/lib/yoke/codex-app-server.js +25 -8
  76. package/lib/yoke/index.js +67 -28
  77. package/lib/yoke/instructions.js +3 -0
  78. package/lib/yoke/interface.js +12 -0
  79. package/lib/yoke/vendor-registry.js +61 -6
  80. package/package.json +1 -1
  81. package/lib/public/gemini-avatar.svg +0 -11
  82. package/lib/yoke/adapters/gemini.js +0 -7
@@ -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;
@@ -89,7 +89,7 @@ var EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
89
89
  var EFFORT_LEVELS_BY_VENDOR = {
90
90
  claude: ["low", "medium", "high", "xhigh", "max"],
91
91
  codex: ["minimal", "low", "medium", "high", "xhigh"],
92
- gemini: [],
92
+ antigravity: [],
93
93
  opencode: [],
94
94
  kiro: ["low", "medium", "high", "xhigh", "max"],
95
95
  };
@@ -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");
@@ -443,14 +372,24 @@ export function initPanels() {
443
372
  var vendorToggleWrap = $("vendor-toggle-wrap");
444
373
  var vendorBtnClaude = $("vendor-btn-claude");
445
374
  var vendorBtnCodex = $("vendor-btn-codex");
446
- var vendorBtnGemini = $("vendor-btn-gemini");
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
- gemini: vendorBtnGemini,
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,15 +416,20 @@ 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 });
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
  }
481
423
 
482
424
  if (vendorBtnClaude) vendorBtnClaude.addEventListener("click", function() { onVendorClick("claude"); });
483
425
  if (vendorBtnCodex) vendorBtnCodex.addEventListener("click", function() { onVendorClick("codex"); });
484
- if (vendorBtnGemini) vendorBtnGemini.addEventListener("click", function() { onVendorClick("gemini"); });
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
 
@@ -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';
@@ -322,7 +323,7 @@ export function renderProjectList() {
322
323
  isWorktree: p.isWorktree || false,
323
324
  parentSlug: p.parentSlug || null,
324
325
  branch: p.branch || null,
325
- worktreeAccessible: p.worktreeAccessible !== undefined ? p.worktreeAccessible : true,
326
+ worktreeExternal: p.worktreeExternal === true,
326
327
  };
327
328
  });
328
329
  var st = store.snap();
@@ -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,33 +13,48 @@ 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, EXPERIMENTAL_VENDORS, isExperimentalVendor } from './vendor-priority.js';
16
17
 
17
18
  // Keep these client constants aligned with lib/yoke/vendor-registry.js. The
18
19
  // browser cannot import the server's CommonJS registry directly.
19
20
  export var VENDOR_AVATARS = {
20
21
  claude: "/claude-code-avatar.png",
21
22
  codex: "/codex-avatar.png",
22
- gemini: "/gemini-avatar.svg",
23
+ antigravity: "/antigravity-avatar.png",
23
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",
24
30
  kiro: "/kiro-avatar.svg",
25
31
  };
26
32
  export var VENDOR_NAMES = {
27
33
  claude: "Claude Code",
28
34
  codex: "Codex",
29
- gemini: "Gemini CLI",
35
+ antigravity: "Antigravity CLI",
30
36
  opencode: "OpenCode",
37
+ kimi: "Kimi Code",
38
+ grok: "Grok Build",
39
+ copilot: "GitHub Copilot CLI",
40
+ qwen: "Qwen Code",
41
+ junie: "Junie CLI",
31
42
  kiro: "Kiro CLI",
32
43
  };
33
44
  // Display order for every vendor Clay knows about, installed or not. Pickers
34
45
  // render the full list so a missing CLI reads as "not installed yet" rather
35
46
  // than "Clay doesn't support it".
36
- export var VENDOR_ORDER = ["claude", "codex", "gemini", "opencode", "kiro"];
37
47
  // Where to send the user when they pick a vendor whose CLI isn't installed.
38
48
  export var VENDOR_HOMEPAGES = {
39
49
  claude: "https://claude.com/product/claude-code",
40
50
  codex: "https://openai.com/codex/",
41
- gemini: "https://github.com/google-gemini/gemini-cli",
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
+ }
@@ -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;
@@ -152,19 +153,26 @@ export function showMateSidebar(mateId, mateData) {
152
153
  var vendorIcons = {
153
154
  claude: "/claude-code-avatar.png",
154
155
  codex: "/codex-avatar.png",
155
- gemini: "/gemini-avatar.svg",
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", gemini: "Gemini CLI", opencode: "OpenCode", kiro: "Kiro CLI" };
160
- var vendorKeys = ["claude", "codex", "gemini", "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;
@@ -0,0 +1,263 @@
1
+ import { store } from './store.js';
2
+ import { getWs } from './ws-ref.js';
3
+
4
+ var configModelList = null;
5
+ var configPopover = null;
6
+ var configChip = null;
7
+ var requestCounter = 0;
8
+ var selectionCounter = 0;
9
+ var requestTimer = null;
10
+
11
+ export function modelEntryValue(entry) {
12
+ if (!entry) return "";
13
+ if (typeof entry === "string") return entry;
14
+ return entry.value || entry.id || "";
15
+ }
16
+
17
+ export function modelEntryMatches(entry, value) {
18
+ if (!entry || !value) return false;
19
+ if (typeof entry === "string") return entry === value;
20
+ return entry.value === value || entry.id === value || entry.resolvedModel === value;
21
+ }
22
+
23
+ export function modelDisplayName(value, models) {
24
+ if (!value) return "";
25
+ var list = models || [];
26
+ for (var i = 0; i < list.length; i++) {
27
+ if (modelEntryMatches(list[i], value)) {
28
+ return typeof list[i] === "string" ? list[i] : (list[i].displayName || modelEntryValue(list[i]));
29
+ }
30
+ }
31
+ return value;
32
+ }
33
+
34
+ function vendorDisplayName(vendor) {
35
+ var info = store.get('vendorInfo') || {};
36
+ return info[vendor] && info[vendor].displayName ? info[vendor].displayName : vendor;
37
+ }
38
+
39
+ function clearRequestTimer() {
40
+ if (!requestTimer) return;
41
+ clearTimeout(requestTimer);
42
+ requestTimer = null;
43
+ }
44
+
45
+ export function setupModelPicker() {
46
+ configModelList = document.getElementById("config-model-list");
47
+ configPopover = document.getElementById("config-popover");
48
+ configChip = document.getElementById("config-chip");
49
+ }
50
+
51
+ export function resetModelPickerState() {
52
+ clearRequestTimer();
53
+ store.set({
54
+ modelListVendor: "",
55
+ modelListStatus: "idle",
56
+ modelListError: "",
57
+ modelRequestId: "",
58
+ modelSelectionPending: null,
59
+ modelSelectionError: "",
60
+ });
61
+ }
62
+
63
+ export function requestVendorModels(vendor, force) {
64
+ if (!vendor) return null;
65
+ var s = store.snap();
66
+ if (!force && s.modelListVendor === vendor) {
67
+ if (s.modelListStatus === "loading") return s.modelRequestId || null;
68
+ if (s.modelListStatus === "ready" && s.currentModels && s.currentModels.length > 0) return null;
69
+ }
70
+
71
+ var ws = getWs();
72
+ if (!ws || ws.readyState !== 1) {
73
+ store.set({
74
+ modelListVendor: vendor,
75
+ modelListStatus: "error",
76
+ modelListError: "The connection is not ready. Reconnect and retry.",
77
+ });
78
+ return null;
79
+ }
80
+
81
+ requestCounter++;
82
+ var requestId = "models-" + Date.now().toString(36) + "-" + requestCounter;
83
+ clearRequestTimer();
84
+ store.set({
85
+ modelListVendor: vendor,
86
+ modelListStatus: "loading",
87
+ modelListError: "",
88
+ modelRequestId: requestId,
89
+ });
90
+ ws.send(JSON.stringify({ type: "get_vendor_models", vendor: vendor, requestId: requestId }));
91
+ requestTimer = setTimeout(function() {
92
+ var current = store.snap();
93
+ if (current.modelRequestId !== requestId || current.modelListStatus !== "loading") return;
94
+ store.set({
95
+ modelListStatus: "error",
96
+ modelListError: vendorDisplayName(vendor) + " model loading timed out. Retry to try again.",
97
+ });
98
+ }, 35000);
99
+ return requestId;
100
+ }
101
+
102
+ export function prepareModelPickerOpen() {
103
+ var s = store.snap();
104
+ var vendor = s.currentVendor || "claude";
105
+ var needsModels = !s.currentModels || s.currentModels.length === 0;
106
+ var wrongVendor = s.modelListVendor !== vendor;
107
+ var retryable = s.modelListStatus === "error" || s.modelListStatus === "empty" || s.modelListStatus === "idle";
108
+ requestVendorModels(vendor, needsModels || wrongVendor || retryable);
109
+ }
110
+
111
+ export function getModelInfoUpdate(msg) {
112
+ var vendor = msg.vendor || store.get('currentVendor') || "claude";
113
+ var currentVendor = store.get('currentVendor');
114
+ if (currentVendor && vendor !== currentVendor) return null;
115
+ if (msg.sessionId != null && msg.sessionId !== store.get('activeSessionId')) return null;
116
+
117
+ var s = store.snap();
118
+ if (msg.requestId && s.modelRequestId && msg.requestId !== s.modelRequestId) return null;
119
+
120
+ var models = Array.isArray(msg.models) ? msg.models : [];
121
+ var unsolicitedEmptyWhileLoading = s.modelListStatus === "loading"
122
+ && s.modelListVendor === vendor
123
+ && !msg.requestId
124
+ && !msg.modelStatus
125
+ && !msg.error
126
+ && models.length === 0;
127
+ if (unsolicitedEmptyWhileLoading) {
128
+ return {
129
+ modelListVendor: vendor,
130
+ modelListStatus: "loading",
131
+ modelListError: "",
132
+ };
133
+ }
134
+
135
+ clearRequestTimer();
136
+ var status = msg.modelStatus || (msg.error ? "error" : (models.length > 0 ? "ready" : "empty"));
137
+ return {
138
+ modelListVendor: vendor,
139
+ modelListStatus: status,
140
+ modelListError: msg.error || "",
141
+ modelRequestId: "",
142
+ };
143
+ }
144
+
145
+ export function requestModelSelection(model) {
146
+ var ws = getWs();
147
+ if (!ws || ws.readyState !== 1) {
148
+ store.set({ modelSelectionError: "The connection is not ready. Reconnect before selecting a model." });
149
+ return;
150
+ }
151
+ var s = store.snap();
152
+ selectionCounter++;
153
+ var requestId = "model-select-" + Date.now().toString(36) + "-" + selectionCounter;
154
+ store.set({
155
+ currentModel: model,
156
+ modelSelectionPending: { requestId: requestId, model: model, previousModel: s.currentModel || "" },
157
+ modelSelectionError: "",
158
+ });
159
+ ws.send(JSON.stringify({
160
+ type: "set_model",
161
+ model: model,
162
+ vendor: s.currentVendor || "claude",
163
+ requestId: requestId,
164
+ }));
165
+ }
166
+
167
+ export function handleModelSelectionResult(msg) {
168
+ var pending = store.get('modelSelectionPending');
169
+ if (!pending || (msg.requestId && msg.requestId !== pending.requestId)) return false;
170
+ if (msg.ok) {
171
+ store.set({
172
+ currentModel: msg.model || pending.model,
173
+ modelSelectionPending: null,
174
+ modelSelectionError: "",
175
+ });
176
+ if (configPopover) configPopover.classList.add("hidden");
177
+ if (configChip) configChip.classList.remove("active");
178
+ } else {
179
+ store.set({
180
+ currentModel: pending.previousModel,
181
+ modelSelectionPending: null,
182
+ modelSelectionError: msg.error || "The model could not be selected.",
183
+ });
184
+ }
185
+ return true;
186
+ }
187
+
188
+ function appendState(title, detail, retry) {
189
+ var stateEl = document.createElement("div");
190
+ stateEl.className = "config-model-state";
191
+ if (title === "Loading models…") {
192
+ var spinner = document.createElement("span");
193
+ spinner.className = "config-model-spinner";
194
+ spinner.setAttribute("aria-hidden", "true");
195
+ stateEl.appendChild(spinner);
196
+ }
197
+ var textWrap = document.createElement("div");
198
+ var titleEl = document.createElement("div");
199
+ titleEl.className = "config-model-state-title";
200
+ titleEl.textContent = title;
201
+ textWrap.appendChild(titleEl);
202
+ if (detail) {
203
+ var detailEl = document.createElement("div");
204
+ detailEl.className = "config-model-state-detail";
205
+ detailEl.textContent = detail;
206
+ textWrap.appendChild(detailEl);
207
+ }
208
+ stateEl.appendChild(textWrap);
209
+ if (retry) {
210
+ var retryBtn = document.createElement("button");
211
+ retryBtn.type = "button";
212
+ retryBtn.className = "config-model-retry";
213
+ retryBtn.textContent = "Retry";
214
+ retryBtn.addEventListener("click", function() {
215
+ requestVendorModels(store.get('currentVendor') || "claude", true);
216
+ });
217
+ stateEl.appendChild(retryBtn);
218
+ }
219
+ configModelList.appendChild(stateEl);
220
+ }
221
+
222
+ export function renderModelPicker() {
223
+ if (!configModelList) return;
224
+ var s = store.snap();
225
+ var vendor = s.currentVendor || "claude";
226
+ var status = s.modelListVendor === vendor ? s.modelListStatus : "idle";
227
+ configModelList.innerHTML = "";
228
+
229
+ if (status === "loading") {
230
+ appendState("Loading models…", "Starting " + vendorDisplayName(vendor) + " and requesting its model catalog.", false);
231
+ return;
232
+ }
233
+ if (status === "error") {
234
+ appendState("Couldn’t load models", s.modelListError || "An unexpected error occurred.", true);
235
+ return;
236
+ }
237
+ if (status === "empty" || !s.currentModels || s.currentModels.length === 0) {
238
+ appendState("No models available", s.modelListError || "The vendor returned an empty model catalog.", true);
239
+ return;
240
+ }
241
+
242
+ for (var i = 0; i < s.currentModels.length; i++) {
243
+ var item = s.currentModels[i];
244
+ var value = modelEntryValue(item);
245
+ if (!value) continue;
246
+ var button = document.createElement("button");
247
+ button.className = "config-radio-item";
248
+ if (modelEntryMatches(item, s.currentModel)) button.classList.add("active");
249
+ button.dataset.model = value;
250
+ button.textContent = typeof item === "string" ? item : (item.displayName || value);
251
+ if (s.modelSelectionPending && s.modelSelectionPending.model === value) {
252
+ button.classList.add("pending");
253
+ button.textContent += " · Selecting…";
254
+ }
255
+ button.disabled = !!s.modelSelectionPending;
256
+ button.addEventListener("click", function() { requestModelSelection(this.dataset.model); });
257
+ configModelList.appendChild(button);
258
+ }
259
+
260
+ if (s.modelSelectionError) {
261
+ appendState("Model selection failed", s.modelSelectionError, false);
262
+ }
263
+ }
@@ -72,6 +72,7 @@ export function initNotifications(_ctx) {
72
72
  if (window.visualViewport) {
73
73
  var layout = $("layout");
74
74
  var mobileTabBar = document.getElementById("mobile-tab-bar");
75
+ var keyboardWasOpen = false;
75
76
  // Capture initial viewport height before any keyboard interaction.
76
77
  // In iOS PWA standalone mode, window.innerHeight shrinks when the
77
78
  // keyboard opens, so we need a stable baseline for comparison.
@@ -86,7 +87,12 @@ export function initNotifications(_ctx) {
86
87
  // Toggle class so CSS can remove the tab-bar bottom padding while keyboard is up
87
88
  var keyboardOpen = vv.height < stableViewportHeight - 100;
88
89
  document.body.classList.toggle("keyboard-open", keyboardOpen);
89
- if (!keyboardOpen) ctx.scrollToBottom();
90
+ if (keyboardOpen && !keyboardWasOpen && ctx.forceScrollToBottom) {
91
+ requestAnimationFrame(function() { ctx.forceScrollToBottom(); });
92
+ } else if (!keyboardOpen) {
93
+ ctx.scrollToBottom();
94
+ }
95
+ keyboardWasOpen = keyboardOpen;
90
96
  // Hide tab bar when software keyboard is open
91
97
  if (mobileTabBar) {
92
98
  if (keyboardOpen) {