clay-server 3.4.0-beta.11 → 3.4.0-beta.13
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/project-models.js +216 -0
- package/lib/project-session-handoff.js +162 -0
- package/lib/project-sessions.js +0 -8
- package/lib/project.js +30 -73
- package/lib/public/app.js +8 -0
- package/lib/public/css/menus.css +35 -56
- package/lib/public/css/pane.css +7 -1
- package/lib/public/css/session-actions.css +98 -0
- package/lib/public/index.html +4 -8
- package/lib/public/modules/agent-config-selects.js +69 -0
- package/lib/public/modules/app-header.js +0 -22
- package/lib/public/modules/app-messages.js +17 -5
- package/lib/public/modules/app-panels.js +13 -80
- package/lib/public/modules/app-projects.js +2 -0
- package/lib/public/modules/model-picker.js +262 -0
- package/lib/public/modules/session-actions.js +294 -0
- package/lib/public/modules/split-pair-ui.js +16 -78
- package/lib/public/modules/split-view.js +3 -0
- package/lib/public/style.css +1 -0
- package/lib/sdk-bridge.js +7 -3
- package/lib/session-handoff-context.js +110 -0
- package/lib/sessions.js +2 -0
- package/lib/ws-schema.js +8 -1
- package/lib/yoke/adapters/codex.js +3 -2
- package/lib/yoke/adapters/kiro.js +2 -2
- package/package.json +1 -1
package/lib/public/css/pane.css
CHANGED
|
@@ -100,7 +100,8 @@ body.pane-mode #input-area {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
.split-pane-title {
|
|
103
|
-
flex: 1;
|
|
103
|
+
flex: 0 1 auto;
|
|
104
|
+
max-width: 46%;
|
|
104
105
|
min-width: 0;
|
|
105
106
|
overflow: hidden;
|
|
106
107
|
text-overflow: ellipsis;
|
|
@@ -283,12 +284,17 @@ button.split-pair-role:hover { filter: brightness(1.25); }
|
|
|
283
284
|
color: var(--text-dimmer);
|
|
284
285
|
cursor: pointer;
|
|
285
286
|
flex-shrink: 0;
|
|
287
|
+
margin-left: auto;
|
|
286
288
|
}
|
|
287
289
|
|
|
288
290
|
.split-pane-context.has-data {
|
|
289
291
|
display: inline-flex;
|
|
290
292
|
}
|
|
291
293
|
|
|
294
|
+
.split-pane-context:not(.has-data) + .split-pane-close {
|
|
295
|
+
margin-left: auto;
|
|
296
|
+
}
|
|
297
|
+
|
|
292
298
|
.split-pane-context:hover {
|
|
293
299
|
background: var(--bg-alt);
|
|
294
300
|
color: var(--text);
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#header-session-actions-btn {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
justify-content: center;
|
|
5
|
+
width: 28px;
|
|
6
|
+
height: 28px;
|
|
7
|
+
padding: 0;
|
|
8
|
+
border: 1px solid transparent;
|
|
9
|
+
border-radius: 8px;
|
|
10
|
+
background: transparent;
|
|
11
|
+
color: var(--text-dimmer);
|
|
12
|
+
cursor: pointer;
|
|
13
|
+
flex-shrink: 0;
|
|
14
|
+
transition: color 0.15s, background 0.15s, border-color 0.15s;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#header-session-actions-btn:hover,
|
|
18
|
+
#header-session-actions-btn[aria-expanded="true"] {
|
|
19
|
+
color: var(--text);
|
|
20
|
+
background: rgba(var(--overlay-rgb), 0.05);
|
|
21
|
+
border-color: var(--border);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
#header-session-actions-btn.hidden { display: none; }
|
|
25
|
+
#header-session-actions-btn .lucide { width: 16px; height: 16px; }
|
|
26
|
+
|
|
27
|
+
.session-actions-menu {
|
|
28
|
+
position: fixed;
|
|
29
|
+
z-index: 10020;
|
|
30
|
+
width: 286px;
|
|
31
|
+
padding: 6px;
|
|
32
|
+
border: 1px solid var(--border);
|
|
33
|
+
border-radius: 12px;
|
|
34
|
+
background: color-mix(in srgb, var(--bg) 96%, transparent);
|
|
35
|
+
box-shadow: 0 14px 38px rgba(var(--shadow-rgb), 0.32), 0 2px 8px rgba(var(--shadow-rgb), 0.2);
|
|
36
|
+
backdrop-filter: blur(18px);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.session-actions-row {
|
|
40
|
+
width: 100%;
|
|
41
|
+
border: 0;
|
|
42
|
+
background: transparent;
|
|
43
|
+
color: var(--text);
|
|
44
|
+
font: inherit;
|
|
45
|
+
text-align: left;
|
|
46
|
+
cursor: pointer;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.session-actions-row {
|
|
50
|
+
display: grid;
|
|
51
|
+
grid-template-columns: 30px minmax(0, 1fr) 16px;
|
|
52
|
+
align-items: center;
|
|
53
|
+
gap: 8px;
|
|
54
|
+
min-height: 54px;
|
|
55
|
+
padding: 7px 8px;
|
|
56
|
+
border-radius: 8px;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.session-actions-row:hover { background: var(--bg-alt); }
|
|
60
|
+
.session-actions-row:disabled { cursor: default; opacity: 0.42; }
|
|
61
|
+
.session-actions-row:disabled:hover { background: transparent; }
|
|
62
|
+
|
|
63
|
+
.session-actions-row-icon {
|
|
64
|
+
display: grid;
|
|
65
|
+
place-items: center;
|
|
66
|
+
width: 28px;
|
|
67
|
+
height: 28px;
|
|
68
|
+
border: 1px solid var(--border-subtle);
|
|
69
|
+
border-radius: 8px;
|
|
70
|
+
background: rgba(var(--overlay-rgb), 0.035);
|
|
71
|
+
color: var(--accent2);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.session-actions-row-icon .lucide { width: 15px; height: 15px; }
|
|
75
|
+
.session-actions-row-copy { display: flex; min-width: 0; flex-direction: column; gap: 2px; }
|
|
76
|
+
.session-actions-row-copy strong { font-size: 12px; font-weight: 750; }
|
|
77
|
+
.session-actions-row-copy > span { color: var(--text-dimmer); font-size: 10px; line-height: 1.25; }
|
|
78
|
+
.session-actions-row-chevron { color: var(--text-dimmer); }
|
|
79
|
+
.session-actions-row-chevron .lucide { width: 13px; height: 13px; }
|
|
80
|
+
|
|
81
|
+
.handoff-modal { width: min(420px, calc(100vw - 32px)); }
|
|
82
|
+
.handoff-modal-desc { margin: -10px 0 14px; color: var(--text-dimmer); font-size: 12px; line-height: 1.45; }
|
|
83
|
+
.handoff-modal-source {
|
|
84
|
+
display: flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
gap: 10px;
|
|
87
|
+
margin-bottom: 14px;
|
|
88
|
+
padding: 10px;
|
|
89
|
+
border: 1px solid var(--border-subtle);
|
|
90
|
+
border-radius: 9px;
|
|
91
|
+
background: var(--bg-alt);
|
|
92
|
+
}
|
|
93
|
+
.handoff-modal-source > img { width: 26px; height: 26px; object-fit: contain; border-radius: 6px; }
|
|
94
|
+
.handoff-modal-source > span { display: grid; min-width: 0; gap: 1px; }
|
|
95
|
+
.handoff-modal-source strong { overflow: hidden; color: var(--text); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
|
|
96
|
+
.handoff-modal-source span span { color: var(--text-dimmer); font-size: 10px; }
|
|
97
|
+
.handoff-modal-field + .handoff-modal-field { margin-top: 10px; }
|
|
98
|
+
.handoff-modal-field .wt-modal-label { margin-top: 0; }
|
package/lib/public/index.html
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
(function(){try{var k="clay-theme-vars",v=localStorage.getItem(k),r=document.documentElement;if(v){var o=JSON.parse(v),p;for(p in o)r.style.setProperty(p,o[p]);var vt=localStorage.getItem(k.replace("-vars","-variant"));if(vt==="light"){r.classList.add("light-theme");r.classList.remove("dark-theme")}else{r.classList.add("dark-theme");r.classList.remove("light-theme")}var m=document.querySelector('meta[name="theme-color"]');if(m&&o["--bg"])m.setAttribute("content",o["--bg"])}else{var sl=window.matchMedia&&window.matchMedia("(prefers-color-scheme: light)").matches;if(sl){r.classList.add("light-theme");r.classList.remove("dark-theme")}}}catch(e){}})();
|
|
31
31
|
</script>
|
|
32
32
|
<script>if(window.navigator.standalone||window.matchMedia("(display-mode:standalone)").matches){document.documentElement.classList.add("pwa-standalone")}</script>
|
|
33
|
-
<link rel="stylesheet" href="style.css?v=
|
|
33
|
+
<link rel="stylesheet" href="style.css?v=20260826a">
|
|
34
34
|
<style>
|
|
35
35
|
@media(max-width:768px){
|
|
36
36
|
/* User messages: vertical stack, avatar on top, right-aligned */
|
|
@@ -388,12 +388,8 @@
|
|
|
388
388
|
<div id="ralph-sticky" class="hidden"></div>
|
|
389
389
|
<div id="debate-sticky" class="hidden"></div>
|
|
390
390
|
<div class="status">
|
|
391
|
-
<button id="header-
|
|
392
|
-
<
|
|
393
|
-
<i data-lucide="bot"></i>
|
|
394
|
-
<span class="header-worker-add-badge"><i data-lucide="plus"></i></span>
|
|
395
|
-
</span>
|
|
396
|
-
<span class="header-worker-label">Add Worker</span>
|
|
391
|
+
<button id="header-session-actions-btn" type="button" aria-label="Session actions" title="Session actions" aria-haspopup="menu" aria-expanded="false">
|
|
392
|
+
<i data-lucide="ellipsis"></i>
|
|
397
393
|
</button>
|
|
398
394
|
<button type="button" id="header-tui-font-btn" class="header-tui-font-btn hidden" title="Terminal font" aria-label="Terminal font">
|
|
399
395
|
<i data-lucide="type"></i>
|
|
@@ -2364,7 +2360,7 @@
|
|
|
2364
2360
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0/lib/addon-fit.min.js"></script>
|
|
2365
2361
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0/lib/addon-web-links.min.js"></script>
|
|
2366
2362
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-webgl@0/lib/addon-webgl.min.js"></script>
|
|
2367
|
-
<script type="module" src="app.js?v=
|
|
2363
|
+
<script type="module" src="app.js?v=20260826a"></script>
|
|
2368
2364
|
<div id="pwa-install-modal" class="pwa-modal hidden">
|
|
2369
2365
|
<div class="pwa-modal-backdrop"></div>
|
|
2370
2366
|
<div class="pwa-modal-card">
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { VENDOR_NAMES, VENDOR_ORDER, isExperimentalVendor } from './app-rendering.js';
|
|
2
|
+
import { effortLevelsFor, effortDisplayName } from './app-panels.js';
|
|
3
|
+
|
|
4
|
+
function optionValue(entry) {
|
|
5
|
+
if (typeof entry === "string") return entry;
|
|
6
|
+
return entry && (entry.value || entry.id) || "";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function optionLabel(entry) {
|
|
10
|
+
if (typeof entry === "string") return entry;
|
|
11
|
+
return entry && (entry.displayName || entry.name || entry.value || entry.id) || "";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function buildAgentVendorSelect(installed, preferred) {
|
|
15
|
+
var select = document.createElement("select");
|
|
16
|
+
select.className = "wt-modal-input";
|
|
17
|
+
for (var i = 0; i < VENDOR_ORDER.length; i++) {
|
|
18
|
+
var vendor = VENDOR_ORDER[i];
|
|
19
|
+
if (installed.indexOf(vendor) === -1) continue;
|
|
20
|
+
var option = document.createElement("option");
|
|
21
|
+
option.value = vendor;
|
|
22
|
+
option.textContent = (isExperimentalVendor(vendor) ? "🧪 " : "") + (VENDOR_NAMES[vendor] || vendor);
|
|
23
|
+
if (isExperimentalVendor(vendor)) option.title = "Experimental integration; not yet validated through direct use testing";
|
|
24
|
+
select.appendChild(option);
|
|
25
|
+
}
|
|
26
|
+
if (preferred && installed.indexOf(preferred) !== -1) select.value = preferred;
|
|
27
|
+
return select;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function fillAgentModels(select, vendor, options, preferred) {
|
|
31
|
+
var models = (options.modelsByVendor && options.modelsByVendor[vendor]) || [];
|
|
32
|
+
select.innerHTML = "";
|
|
33
|
+
var automatic = document.createElement("option");
|
|
34
|
+
automatic.value = "";
|
|
35
|
+
automatic.textContent = "Automatic";
|
|
36
|
+
select.appendChild(automatic);
|
|
37
|
+
for (var i = 0; i < models.length; i++) {
|
|
38
|
+
var option = document.createElement("option");
|
|
39
|
+
option.value = optionValue(models[i]);
|
|
40
|
+
option.textContent = optionLabel(models[i]);
|
|
41
|
+
select.appendChild(option);
|
|
42
|
+
}
|
|
43
|
+
select.value = preferred || "";
|
|
44
|
+
if (select.selectedIndex === -1) select.value = "";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function buildAgentEffortSelect() {
|
|
48
|
+
var select = document.createElement("select");
|
|
49
|
+
select.className = "wt-modal-input";
|
|
50
|
+
return select;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function fillAgentEffort(select, vendor, options, modelValue, preferred) {
|
|
54
|
+
var previous = preferred === undefined ? select.value : preferred;
|
|
55
|
+
var models = (options.modelsByVendor && options.modelsByVendor[vendor]) || [];
|
|
56
|
+
var levels = effortLevelsFor(vendor, models, modelValue);
|
|
57
|
+
select.innerHTML = "";
|
|
58
|
+
var automatic = document.createElement("option");
|
|
59
|
+
automatic.value = "";
|
|
60
|
+
automatic.textContent = "Default";
|
|
61
|
+
select.appendChild(automatic);
|
|
62
|
+
for (var i = 0; i < levels.length; i++) {
|
|
63
|
+
var option = document.createElement("option");
|
|
64
|
+
option.value = levels[i];
|
|
65
|
+
option.textContent = effortDisplayName(levels[i]);
|
|
66
|
+
select.appendChild(option);
|
|
67
|
+
}
|
|
68
|
+
select.value = levels.indexOf(previous) !== -1 ? previous : "";
|
|
69
|
+
}
|
|
@@ -12,7 +12,6 @@ import { saveToolState, resetToolState, restoreToolState } from './tools.js';
|
|
|
12
12
|
import { getSessionUsage, setSessionUsage, getContextData, setContextData, updateContextPanel, updateUsagePanel } from './app-panels.js';
|
|
13
13
|
import { onHistoryPrepended as onSessionSearchHistoryPrepended } from './session-search.js';
|
|
14
14
|
import { getCachedSessions } from './sidebar-sessions.js';
|
|
15
|
-
import { openPairDialog } from './split-pair-ui.js';
|
|
16
15
|
import { showConfirm } from './app-misc.js';
|
|
17
16
|
|
|
18
17
|
// --- Module-owned state ---
|
|
@@ -23,27 +22,6 @@ export function initHeader() {
|
|
|
23
22
|
var headerRenameBtn = document.getElementById("header-rename-btn");
|
|
24
23
|
var headerTitleEl = document.getElementById("header-title");
|
|
25
24
|
var headerInfoBtn = document.getElementById("header-info-btn");
|
|
26
|
-
var headerAddWorkerBtn = document.getElementById("header-add-worker-btn");
|
|
27
|
-
|
|
28
|
-
// --- Add Worker: the current session becomes the Driver of a new pair ---
|
|
29
|
-
if (headerAddWorkerBtn) {
|
|
30
|
-
headerAddWorkerBtn.addEventListener("click", function () {
|
|
31
|
-
var sessionId = store.get('activeSessionId');
|
|
32
|
-
if (!sessionId || store.get('splitPanes') || store.get('dmMode')) return;
|
|
33
|
-
openPairDialog({
|
|
34
|
-
sessionId: sessionId,
|
|
35
|
-
title: headerTitleEl ? headerTitleEl.textContent : "",
|
|
36
|
-
vendor: store.get('currentVendor') || "claude",
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
// A split shows the GROUP in the title bar; adding a worker there makes
|
|
40
|
-
// no sense (the group already has two members), so hide the button.
|
|
41
|
-
store.subscribe(function (state, prev) {
|
|
42
|
-
if (state.splitPanes !== prev.splitPanes) {
|
|
43
|
-
headerAddWorkerBtn.classList.toggle("hidden", !!state.splitPanes);
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
25
|
var headerFullAccessBtn = document.getElementById("header-full-access-btn");
|
|
48
26
|
|
|
49
27
|
function updateFullAccessButton() {
|
|
@@ -21,6 +21,7 @@ import { handlePaletteSessionSwitch, setPaletteVersion } from './command-palette
|
|
|
21
21
|
import { handleFindInSessionResults } from './session-search.js';
|
|
22
22
|
import { syncPaneTitles, maybeRestoreSplitGroup, openGroup } from './split-view.js';
|
|
23
23
|
import { showPairDialog, handlePairCreated, handleSplitDelegation, showWorkerDelegationNotice, hideWorkerDelegationNotice } from './split-pair-ui.js';
|
|
24
|
+
import { handleSessionActionMessage } from './session-actions.js';
|
|
24
25
|
import { renderWorkerProposal, updateWorkerProposal } from './worker-proposal.js';
|
|
25
26
|
import { handleInputSync, autoResize, builtinCommands, setScheduleBtnDisabled, sendShellResultToAgent } from './input.js';
|
|
26
27
|
import { startThinking, appendThinking, stopThinking, resetThinkingGroup, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, closeToolGroup, removeToolFromGroup, resetToolState, getTools, getPlanContent, setPlanContent, renderPlanBanner, renderPlanCard, getTodoTools, handleTodoWrite, handleTaskCreate, handleTaskUpdate, applyDeadSessionTodoCompaction, isPlanFilePath, enableMainInput, addTurnMeta, updateSubagentActivity, addSubagentToolEntry, markSubagentDone, initSubagentStop, updateSubagentProgress, updateSubagentTaskStatus, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionCancelled, markPermissionResolved, renderElicitationRequest, markElicitationResolved, renderUserDialogRequest, markUserDialogResolved, updateThinkingTokens } from './tools.js';
|
|
@@ -53,6 +54,7 @@ import { handleWhatsNewState, handleWhatsNewSeenResult, setKnownEntries as setWh
|
|
|
53
54
|
import { closeArticle as closeWhatsNewArticle } from './whats-new-article.js';
|
|
54
55
|
import { resolvePaneSession, resolveSwitchedVendor } from './pane-session.js';
|
|
55
56
|
import { selectDefaultVendorForBlankSession } from './vendor-selection.js';
|
|
57
|
+
import { getModelInfoUpdate, modelEntryMatches, handleModelSelectionResult } from './model-picker.js';
|
|
56
58
|
import { getModelEffortLevels, accumulateUsage, updateUsagePanel, accumulateContext, updateContextPanel, renderCtxPopover, updateStatusPanel } from './app-panels.js';
|
|
57
59
|
import { updateProjectList, resetClientState, showUpdateAvailable, handleRemoveProjectCheckResult, handleRemoveProjectResult, handleBrowseDirResult, handleAddProjectResult, handleCloneProgress } from './app-projects.js';
|
|
58
60
|
import { updateHistorySentinel, prependOlderHistory } from './app-header.js';
|
|
@@ -431,12 +433,12 @@ export function processMessage(msg) {
|
|
|
431
433
|
// currentModels with the wrong vendor's list and trigger app-panels
|
|
432
434
|
// to request models for the "wrong" vendor, which feeds back into a
|
|
433
435
|
// ping-pong loop of vendor flapping. See issue #336.
|
|
434
|
-
var
|
|
435
|
-
if (
|
|
436
|
+
var _modelLoadUpdate = getModelInfoUpdate(msg);
|
|
437
|
+
if (!_modelLoadUpdate) break;
|
|
436
438
|
|
|
437
439
|
var _modelVal = msg.model;
|
|
438
440
|
if (_modelVal && typeof _modelVal === "object") _modelVal = _modelVal.value || _modelVal.displayName || "";
|
|
439
|
-
var _miUpdate = { currentModels: msg.models || [] };
|
|
441
|
+
var _miUpdate = Object.assign({ currentModels: msg.models || [] }, _modelLoadUpdate);
|
|
440
442
|
if (Object.prototype.hasOwnProperty.call(msg, "model")) {
|
|
441
443
|
// Keep the user's existing selection only if it actually belongs to
|
|
442
444
|
// this vendor's model list. Otherwise (e.g. switching to a Codex
|
|
@@ -445,8 +447,7 @@ export function processMessage(msg) {
|
|
|
445
447
|
// model.
|
|
446
448
|
var _curModel = store.get('currentModel');
|
|
447
449
|
var _curInList = !!_curModel && (msg.models || []).some(function (m) {
|
|
448
|
-
|
|
449
|
-
return v === _curModel;
|
|
450
|
+
return modelEntryMatches(m, _curModel);
|
|
450
451
|
});
|
|
451
452
|
if (store.get('vendorSelectionLocked') && _curInList) {
|
|
452
453
|
// Keep the user's existing (valid) selection; only update models list
|
|
@@ -463,6 +464,10 @@ export function processMessage(msg) {
|
|
|
463
464
|
break;
|
|
464
465
|
}
|
|
465
466
|
|
|
467
|
+
case "model_selection_result":
|
|
468
|
+
handleModelSelectionResult(msg);
|
|
469
|
+
break;
|
|
470
|
+
|
|
466
471
|
case "config_state": {
|
|
467
472
|
var _cs = {};
|
|
468
473
|
if (msg.model) _cs.currentModel = msg.model;
|
|
@@ -597,6 +602,13 @@ export function processMessage(msg) {
|
|
|
597
602
|
if (createdPairGroup) openGroup(createdPairGroup);
|
|
598
603
|
break;
|
|
599
604
|
|
|
605
|
+
case "session_handoff_result":
|
|
606
|
+
case "handoff_session_options":
|
|
607
|
+
case "handoff_context":
|
|
608
|
+
case "handoff_created":
|
|
609
|
+
handleSessionActionMessage(msg);
|
|
610
|
+
break;
|
|
611
|
+
|
|
600
612
|
case "worker_proposal":
|
|
601
613
|
renderWorkerProposal(msg);
|
|
602
614
|
break;
|
|
@@ -7,6 +7,7 @@ import { store } from './store.js';
|
|
|
7
7
|
import { getWs } from './ws-ref.js';
|
|
8
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
|
-
|
|
354
|
+
setupModelPicker();
|
|
426
355
|
configModeList = $("config-mode-list");
|
|
427
356
|
configEffortSection = $("config-effort-section");
|
|
428
357
|
configEffortBar = $("config-effort-bar");
|
|
@@ -487,7 +416,7 @@ export function initPanels() {
|
|
|
487
416
|
if (vendor === (store.get('currentVendor') || "claude")) return;
|
|
488
417
|
var installed = store.get('installedVendors') || [];
|
|
489
418
|
if (installed.indexOf(vendor) === -1) return;
|
|
490
|
-
store.set({ currentVendor: vendor, currentModel: "", currentModels: [], vendorSelectionLocked: true, sessionVendorBound: true });
|
|
419
|
+
store.set({ currentVendor: vendor, currentModel: "", currentModels: [], vendorCapabilities: {}, vendorSelectionLocked: true, sessionVendorBound: true });
|
|
491
420
|
var ws = getWs();
|
|
492
421
|
if (ws) ws.send(JSON.stringify({ type: "set_vendor", vendor: vendor }));
|
|
493
422
|
}
|
|
@@ -528,8 +457,7 @@ export function initPanels() {
|
|
|
528
457
|
|
|
529
458
|
// Vendor changed -> switch model list and current model to match
|
|
530
459
|
if (state.currentVendor !== prev.currentVendor && state.currentVendor) {
|
|
531
|
-
|
|
532
|
-
if (ws) ws.send(JSON.stringify({ type: "get_vendor_models", vendor: state.currentVendor }));
|
|
460
|
+
requestVendorModels(state.currentVendor, true);
|
|
533
461
|
}
|
|
534
462
|
|
|
535
463
|
// config chip
|
|
@@ -539,6 +467,12 @@ export function initPanels() {
|
|
|
539
467
|
state.currentBetas !== prev.currentBetas ||
|
|
540
468
|
state.currentThinking !== prev.currentThinking ||
|
|
541
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 ||
|
|
542
476
|
state.codexApproval !== prev.codexApproval ||
|
|
543
477
|
state.codexSandbox !== prev.codexSandbox ||
|
|
544
478
|
state.codexWebSearch !== prev.codexWebSearch ||
|
|
@@ -639,6 +573,7 @@ export function initPanels() {
|
|
|
639
573
|
e.stopPropagation();
|
|
640
574
|
var wasHidden = configPopover.classList.toggle("hidden");
|
|
641
575
|
configChip.classList.toggle("active", !wasHidden);
|
|
576
|
+
if (!wasHidden) prepareModelPickerOpen();
|
|
642
577
|
});
|
|
643
578
|
}
|
|
644
579
|
|
|
@@ -692,7 +627,7 @@ export function updateConfigChip() {
|
|
|
692
627
|
var s = store.snap();
|
|
693
628
|
var vendor = s.currentVendor || "claude";
|
|
694
629
|
configChipLabel.textContent = modelDisplayName(s.currentModel, s.currentModels);
|
|
695
|
-
|
|
630
|
+
renderModelPicker();
|
|
696
631
|
rebuildModeList();
|
|
697
632
|
rebuildEffortBar();
|
|
698
633
|
|
|
@@ -879,9 +814,7 @@ export function updateContextPanel() {
|
|
|
879
814
|
hCtxEl = document.createElement("div");
|
|
880
815
|
hCtxEl.className = "header-context";
|
|
881
816
|
hCtxEl.innerHTML = '<div class="header-context-bar"><div class="header-context-fill"></div></div><span class="header-context-label"></span>';
|
|
882
|
-
|
|
883
|
-
var contextAnchor = workerBtn && workerBtn.parentNode === statusArea ? workerBtn.nextSibling : statusArea.firstChild;
|
|
884
|
-
statusArea.insertBefore(hCtxEl, contextAnchor);
|
|
817
|
+
statusArea.insertBefore(hCtxEl, statusArea.firstChild);
|
|
885
818
|
hCtxEl.addEventListener("mouseenter", function() {
|
|
886
819
|
if (store.get('richContextUsage')) {
|
|
887
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 });
|