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.
- package/lib/daemon-projects.js +3 -3
- package/lib/daemon.js +59 -59
- package/lib/project-connection.js +35 -7
- package/lib/project-debate.js +4 -0
- package/lib/project-mate-interaction.js +21 -1
- package/lib/project-memory.js +3 -0
- package/lib/project-models.js +220 -0
- package/lib/project-session-handoff.js +162 -0
- package/lib/project-session-pair.js +14 -7
- package/lib/project-session-spawn.js +1 -1
- package/lib/project-sessions.js +13 -45
- package/lib/project-worker-proposal.js +51 -14
- package/lib/project.js +38 -80
- package/lib/public/antigravity-avatar.png +0 -0
- package/lib/public/app.js +29 -0
- package/lib/public/copilot-avatar.svg +5 -0
- package/lib/public/css/command-palette.css +22 -2
- package/lib/public/css/icon-strip.css +29 -13
- package/lib/public/css/input.css +56 -0
- package/lib/public/css/mates.css +6 -0
- package/lib/public/css/menus.css +38 -25
- package/lib/public/css/messages.css +9 -0
- package/lib/public/css/pane.css +16 -3
- package/lib/public/css/pwa-mobile.css +17 -0
- package/lib/public/css/session-actions.css +98 -0
- package/lib/public/css/title-bar.css +19 -0
- package/lib/public/grok-avatar.svg +4 -0
- package/lib/public/index.html +29 -7
- package/lib/public/junie-avatar.svg +11 -0
- package/lib/public/kimi-avatar.svg +4 -0
- package/lib/public/modules/agent-config-selects.js +69 -0
- package/lib/public/modules/app-header.js +4 -22
- package/lib/public/modules/app-messages.js +54 -11
- package/lib/public/modules/app-panels.js +36 -83
- package/lib/public/modules/app-projects.js +3 -1
- package/lib/public/modules/app-rendering.js +19 -4
- package/lib/public/modules/background-tasks-ui.js +55 -0
- package/lib/public/modules/mate-sidebar.js +11 -3
- package/lib/public/modules/model-picker.js +263 -0
- package/lib/public/modules/notifications.js +7 -1
- package/lib/public/modules/pane-bridge.js +11 -0
- package/lib/public/modules/pane-links.js +23 -0
- package/lib/public/modules/project-switcher.js +7 -6
- package/lib/public/modules/session-actions.js +294 -0
- package/lib/public/modules/sidebar-mates.js +1 -1
- package/lib/public/modules/sidebar-mobile.js +2 -1
- package/lib/public/modules/sidebar-projects.js +34 -43
- package/lib/public/modules/sidebar-sessions.js +20 -6
- package/lib/public/modules/split-pair-ui.js +16 -77
- package/lib/public/modules/split-view.js +3 -0
- package/lib/public/modules/tools.js +6 -1
- package/lib/public/modules/vendor-priority.js +14 -0
- package/lib/public/modules/vendor-selection.js +20 -0
- package/lib/public/modules/worker-proposal.js +6 -1
- package/lib/public/modules/worktree-location.js +17 -0
- package/lib/public/qwen-avatar.svg +4 -0
- package/lib/public/style.css +4 -2
- package/lib/sdk-bridge.js +103 -56
- package/lib/sdk-message-processor.js +26 -4
- package/lib/session-handoff-context.js +110 -0
- package/lib/session-notes-mcp-server.js +8 -1
- package/lib/sessions.js +4 -0
- package/lib/worktree.js +9 -4
- package/lib/ws-schema.js +9 -1
- package/lib/yoke/acp-agent-profiles.js +64 -14
- package/lib/yoke/adapters/antigravity.js +417 -0
- package/lib/yoke/adapters/claude.js +34 -0
- package/lib/yoke/adapters/codex.js +101 -5
- package/lib/yoke/adapters/copilot.js +7 -0
- package/lib/yoke/adapters/grok.js +7 -0
- package/lib/yoke/adapters/junie.js +7 -0
- package/lib/yoke/adapters/kimi.js +7 -0
- package/lib/yoke/adapters/kiro.js +2 -2
- package/lib/yoke/adapters/qwen.js +7 -0
- package/lib/yoke/codex-app-server.js +25 -8
- package/lib/yoke/index.js +67 -28
- package/lib/yoke/instructions.js +3 -0
- package/lib/yoke/interface.js +12 -0
- package/lib/yoke/vendor-registry.js +61 -6
- package/package.json +1 -1
- package/lib/public/gemini-avatar.svg +0 -11
- 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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
160
|
-
var vendorKeys = ["claude", "codex", "
|
|
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 (
|
|
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) {
|