clay-server 4.1.0-beta.14 → 4.1.0-beta.15
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.js +2 -2
- package/lib/public/app.js +3 -1
- package/lib/public/css/mobile-nav.css +7 -0
- package/lib/public/css/sidebar.css +7 -0
- package/lib/public/modules/app-connection.js +3 -0
- package/lib/public/modules/app-messages.js +7 -5
- package/lib/public/modules/app-panels.js +47 -12
- package/lib/public/modules/default-vendor.js +116 -0
- package/lib/public/modules/sidebar-mobile.js +49 -2
- package/lib/public/modules/sidebar-sessions.js +65 -5
- package/lib/sdk-message-processor.js +5 -0
- package/lib/server-default-vendor.js +60 -0
- package/lib/server.js +4 -0
- package/lib/users-default-vendor-preferences.js +80 -0
- package/lib/users.js +6 -0
- package/lib/ws-schema.js +3 -0
- package/lib/yoke/acp-agent-profiles.js +15 -2
- package/lib/yoke/adapters/codex.js +56 -13
- package/package.json +1 -1
package/lib/project.js
CHANGED
|
@@ -1598,8 +1598,8 @@ function createProjectContext(opts) {
|
|
|
1598
1598
|
}
|
|
1599
1599
|
|
|
1600
1600
|
// --- DM messages (delegated to server-level handler) ---
|
|
1601
|
-
if (msg.type === "home_debate_question_response" || msg.type === "home_debate_control" || msg.type === "home_mate_creation_question_response" || msg.type === "default_ai_get" || msg.type === "default_ai_catalog_get" || msg.type === "default_ai_set" || msg.type === "cursor_sharing_get" || msg.type === "cursor_sharing_set") {
|
|
1602
|
-
if (typeof opts.onDmMessage === "function") opts.onDmMessage(ws, msg);
|
|
1601
|
+
if (msg.type === "home_debate_question_response" || msg.type === "home_debate_control" || msg.type === "home_mate_creation_question_response" || msg.type === "default_ai_get" || msg.type === "default_ai_catalog_get" || msg.type === "default_ai_set" || msg.type === "default_vendor_get" || msg.type === "default_vendor_set" || msg.type === "cursor_sharing_get" || msg.type === "cursor_sharing_set") {
|
|
1602
|
+
if (typeof opts.onDmMessage === "function") opts.onDmMessage(ws, msg, slug);
|
|
1603
1603
|
return;
|
|
1604
1604
|
}
|
|
1605
1605
|
if (msg.type === "issue_reference_resolve" || msg.type === "home_clay_ask" || msg.type === "home_clay_session_resolve" || msg.type === "home_clay_log_resolve") {
|
package/lib/public/app.js
CHANGED
|
@@ -71,6 +71,7 @@ import { rememberHomePrimarySurface } from './modules/home-surface.js';
|
|
|
71
71
|
import { initRateLimit, handleRateLimitEvent as _rlHandleRateLimitEvent, updateRateLimitUsage as _rlUpdateRateLimitUsage, handleFastModeState as _rlHandleFastModeState, resetRateLimitState } from './modules/app-rate-limit.js';
|
|
72
72
|
import { initCursors, handleRemoteCursorMove as _curHandleRemoteCursorMove, handleRemoteCursorLeave as _curHandleRemoteCursorLeave, handleRemoteSelection as _curHandleRemoteSelection, clearRemoteCursors as _curClearRemoteCursors, initCursorToggle } from './modules/app-cursors.js';
|
|
73
73
|
import { initDefaultAi } from './modules/default-ai.js';
|
|
74
|
+
import { requestDefaultVendor } from './modules/default-vendor.js';
|
|
74
75
|
import { initFavicon, updateFavicon as _favUpdateFavicon, setSendBtnMode as _favSetSendBtnMode, blinkIO as _favBlinkIO, blinkSessionDot as _favBlinkSessionDot, updateCrossProjectBlink as _favUpdateCrossProjectBlink, startUrgentBlink as _favStartUrgentBlink, stopUrgentBlink as _favStopUrgentBlink, setActivity as _favSetActivity } from './modules/app-favicon.js';
|
|
75
76
|
import { initHeader, closeSessionInfoPopover as _hdrCloseSessionInfoPopover, updateHistorySentinel as _hdrUpdateHistorySentinel, requestMoreHistory as _hdrRequestMoreHistory, prependOlderHistory as _hdrPrependOlderHistory } from './modules/app-header.js';
|
|
76
77
|
import { initSessionActions } from './modules/session-actions.js';
|
|
@@ -427,6 +428,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
427
428
|
defaultAiState: { loading: false, saving: false, refreshRequestId: null, saveRequestId: null, catalogRequestIds: {}, catalogs: {}, preference: null, selection: null, installedVendors: [], accountAvailable: true, accountId: null, serverEpoch: null, canonicalRevision: 0, error: "" },
|
|
428
429
|
defaultAiDraft: { vendor: "", model: "", effort: "" },
|
|
429
430
|
defaultAiDraftDirty: false,
|
|
431
|
+
defaultVendorState: { loading: false, saving: false, getRequestId: null, saveRequestId: null, preference: null, preferencePresent: false, installedVendors: [], accountId: null, projectSlug: null, serverEpoch: null, canonicalRevision: 0, error: "" },
|
|
430
432
|
|
|
431
433
|
// dm
|
|
432
434
|
dmTargetUser: null,
|
|
@@ -1024,7 +1026,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
1024
1026
|
// the project Mate DM preference therefore hides only Mate shortcuts.
|
|
1025
1027
|
if (document.body) document.body.classList.add('is-multi-user');
|
|
1026
1028
|
}
|
|
1027
|
-
if (d.user && d.user.id) { store.set({ myUserId: d.user.id }); }
|
|
1029
|
+
if (d.user && d.user.id) { store.set({ myUserId: d.user.id }); requestDefaultVendor(); }
|
|
1028
1030
|
if (d.permissions) store.set({ permissions: d.permissions });
|
|
1029
1031
|
if (d.mustChangePin) showForceChangePinOverlay();
|
|
1030
1032
|
// Single-user mode: clear user strip skeletons immediately (no presence message will arrive)
|
|
@@ -423,6 +423,13 @@
|
|
|
423
423
|
.mobile-vendor-list .mobile-session-new-vendor:active {
|
|
424
424
|
background: rgba(var(--overlay-rgb), 0.1);
|
|
425
425
|
}
|
|
426
|
+
.mobile-vendor-row { display: flex; align-items: stretch; gap: 4px; }
|
|
427
|
+
.mobile-vendor-row .mobile-session-new-vendor { flex: 1 1 auto; min-width: 0; }
|
|
428
|
+
.mobile-vendor-set-default { border: 1px solid var(--border-subtle); border-radius: 8px; background: transparent; color: var(--text-dimmer); padding: 0 8px; font-size: 11px; white-space: nowrap; }
|
|
429
|
+
.mobile-vendor-set-default:active { background: rgba(var(--overlay-rgb), 0.1); }
|
|
430
|
+
.mobile-vendor-set-default:disabled { opacity: 0.55; }
|
|
431
|
+
.mobile-session-new-preference-status { padding: 4px 8px 8px; color: var(--error); font-size: 11px; }
|
|
432
|
+
.mobile-session-new-fallback-note { color: var(--text-muted); }
|
|
426
433
|
.mobile-vendor-note {
|
|
427
434
|
margin-left: auto;
|
|
428
435
|
font-size: 12px;
|
|
@@ -1464,6 +1464,13 @@
|
|
|
1464
1464
|
|
|
1465
1465
|
/* --- New-session vendor picker --- */
|
|
1466
1466
|
.session-new-menu { min-width: 200px; }
|
|
1467
|
+
.session-new-vendor-row { display: flex; align-items: stretch; }
|
|
1468
|
+
.session-new-vendor-row .session-new-vendor { flex: 1 1 auto; min-width: 0; }
|
|
1469
|
+
.session-new-set-default { border: 0; background: transparent; color: var(--text-dimmer); padding: 0 8px; font-size: 10px; cursor: pointer; }
|
|
1470
|
+
.session-new-set-default:hover { color: var(--text); background: rgba(var(--overlay-rgb), 0.05); }
|
|
1471
|
+
.session-new-set-default:disabled { opacity: 0.55; cursor: wait; }
|
|
1472
|
+
.session-new-preference-status { padding: 6px 10px; color: var(--error); font-size: 11px; }
|
|
1473
|
+
.session-new-fallback-note { color: var(--text-muted); border-bottom: 1px solid var(--border); }
|
|
1467
1474
|
|
|
1468
1475
|
.session-ctx-sep {
|
|
1469
1476
|
height: 1px;
|
|
@@ -17,6 +17,7 @@ import { resumeHomeChat } from './home-mate-chat.js';
|
|
|
17
17
|
import { requestHomeSurfacePreference } from './home-surface.js';
|
|
18
18
|
import { isHomeDebatesSurface } from './home-sub-surface.js';
|
|
19
19
|
import { beginDefaultAiConnection, requestDefaultAi } from './default-ai.js';
|
|
20
|
+
import { beginDefaultVendorConnection, requestDefaultVendor } from './default-vendor.js';
|
|
20
21
|
|
|
21
22
|
var reconnectTimer = null;
|
|
22
23
|
var reconnectDelay = 1000;
|
|
@@ -277,6 +278,8 @@ export function connect() {
|
|
|
277
278
|
onConnected();
|
|
278
279
|
beginDefaultAiConnection();
|
|
279
280
|
requestDefaultAi();
|
|
281
|
+
beginDefaultVendorConnection();
|
|
282
|
+
requestDefaultVendor();
|
|
280
283
|
};
|
|
281
284
|
|
|
282
285
|
newWs.onclose = function (e) {
|
|
@@ -19,6 +19,7 @@ import { refreshMobileChatSheet } from './sidebar-mobile.js';
|
|
|
19
19
|
import { renderMateSessionList, handleMateSearchResults, updateMateSidebarProfile } from './mate-sidebar.js';
|
|
20
20
|
import { openHomeChat, handleHomeMateHistory, handleHomeMateDelta, handleHomeMateSegment, handleHomeMateDone, handleHomeMateError, handleHomeMateSessionsState } from './home-mate-chat.js';
|
|
21
21
|
import { handleHomeSurfaceState } from './home-surface.js';
|
|
22
|
+
import { handleDefaultVendorMessage } from './default-vendor.js';
|
|
22
23
|
import { handleHomeMateMemoryState, handleHomeMateKnowledgeState } from './home-mate-settings.js';
|
|
23
24
|
import { renderKnowledgeList, handleKnowledgeContent } from './mate-knowledge.js';
|
|
24
25
|
import { renderMemoryList } from './mate-memory.js';
|
|
@@ -65,7 +66,7 @@ import { closeArticle as closeWhatsNewArticle } from './whats-new-article.js';
|
|
|
65
66
|
import { resolvePaneSession, resolveSwitchedVendor } from './pane-session.js';
|
|
66
67
|
import { selectDefaultVendorForBlankSession } from './vendor-selection.js';
|
|
67
68
|
import { getModelInfoUpdate, modelEntryValue, modelEntryMatches, handleModelSelectionResult, requestVendorModels } from './model-picker.js';
|
|
68
|
-
import { getModelEffortLevels, accumulateUsage, updateUsagePanel, accumulateContext, updateContextPanel, renderCtxPopover, updateStatusPanel } from './app-panels.js';
|
|
69
|
+
import { getModelEffortLevels, accumulateUsage, updateUsagePanel, accumulateContext, updateContextPanel, renderCtxPopover, updateStatusPanel, markContextUnavailable } from './app-panels.js';
|
|
69
70
|
import { updateProjectList, resetClientState, showUpdateAvailable, handleRemoveProjectCheckResult, handleRemoveProjectResult, handleBrowseDirResult, handleAddProjectResult, handleCloneProgress, finishProjectSessionActivation } from './app-projects.js';
|
|
70
71
|
import { updateHistorySentinel, prependOlderHistory } from './app-header.js';
|
|
71
72
|
import { hideHomeHub, showHomeHub } from './app-home-hub.js';
|
|
@@ -97,6 +98,7 @@ export function processMessage(msg) {
|
|
|
97
98
|
if (handleAutonomousRunMessage(msg)) return;
|
|
98
99
|
if (handleLoopInterviewMessage(msg)) return;
|
|
99
100
|
if (handleScheduledTaskMessage(msg)) return;
|
|
101
|
+
if (handleDefaultVendorMessage(msg)) return;
|
|
100
102
|
if (msg && msg.type === "schedule_message_result") {
|
|
101
103
|
handleScheduleMessageResult(msg);
|
|
102
104
|
return;
|
|
@@ -226,9 +228,8 @@ export function processMessage(msg) {
|
|
|
226
228
|
applyDeadSessionTodoCompaction();
|
|
227
229
|
}
|
|
228
230
|
// Restore cached rich context usage BEFORE updateContextPanel runs
|
|
229
|
-
if (msg.contextUsage) {
|
|
230
|
-
|
|
231
|
-
}
|
|
231
|
+
if (msg.contextUsage && !msg.contextUsage.unavailable) store.set({ richContextUsage: msg.contextUsage });
|
|
232
|
+
else markContextUnavailable();
|
|
232
233
|
// Restore accurate context data from the last result in full history
|
|
233
234
|
if (msg.lastUsage || msg.lastModelUsage) {
|
|
234
235
|
accumulateContext(msg.lastCost, msg.lastUsage, msg.lastModelUsage, msg.lastStreamInputTokens);
|
|
@@ -1273,7 +1274,8 @@ export function processMessage(msg) {
|
|
|
1273
1274
|
case "context_usage":
|
|
1274
1275
|
if (msg.sessionId != null && msg.sessionId !== store.get("activeSessionId")) break;
|
|
1275
1276
|
if (msg.data && !store.get('replayingHistory')) {
|
|
1276
|
-
|
|
1277
|
+
if (msg.data.unavailable) markContextUnavailable();
|
|
1278
|
+
else store.set({ richContextUsage: msg.data });
|
|
1277
1279
|
// UI sync handled by store subscriber in app-panels.js
|
|
1278
1280
|
}
|
|
1279
1281
|
break;
|
|
@@ -10,7 +10,7 @@ import { setupModelPicker, renderModelPicker, requestVendorModels, prepareModelP
|
|
|
10
10
|
|
|
11
11
|
// --- Module-owned state (not in store) ---
|
|
12
12
|
var sessionUsage = { cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
|
|
13
|
-
var contextData = { contextWindow: 0, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0, thinking: null, costBasis: null };
|
|
13
|
+
var contextData = { contextWindow: 0, currentInput: null, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0, thinking: null, costBasis: null };
|
|
14
14
|
var ctxPopoverEl = null;
|
|
15
15
|
var ctxHoverTimer = null;
|
|
16
16
|
var statusRefreshTimer = null;
|
|
@@ -718,9 +718,18 @@ export function contextPctClass(pct) {
|
|
|
718
718
|
|
|
719
719
|
export function updateContextPanel() {
|
|
720
720
|
if (!contextUsedEl) return;
|
|
721
|
-
// Context
|
|
722
|
-
|
|
723
|
-
|
|
721
|
+
// Context occupancy is separate from generic per-turn accounting. Codex
|
|
722
|
+
// only supplies currentInput from a verified last-turn snapshot; rich
|
|
723
|
+
// context usage is more authoritative when available.
|
|
724
|
+
var rich = store.get('richContextUsage');
|
|
725
|
+
var hasRich = rich && (
|
|
726
|
+
(typeof rich.totalTokens === "number" && typeof rich.maxTokens === "number" && rich.maxTokens > 0) ||
|
|
727
|
+
(typeof rich.input_tokens === "number" && typeof rich.contextWindow === "number" && rich.contextWindow > 0)
|
|
728
|
+
);
|
|
729
|
+
var hasCurrent = typeof contextData.currentInput === "number";
|
|
730
|
+
var hasContext = !!(hasRich || hasCurrent);
|
|
731
|
+
var used = hasRich ? (typeof rich.totalTokens === "number" ? rich.totalTokens : rich.input_tokens) : (hasCurrent ? contextData.currentInput : 0);
|
|
732
|
+
var win = hasRich ? (typeof rich.maxTokens === "number" ? rich.maxTokens : rich.contextWindow) : (hasCurrent ? contextData.contextWindow : 0);
|
|
724
733
|
var pct = win > 0 ? Math.min(100, (used / win) * 100) : 0;
|
|
725
734
|
var cls = contextPctClass(pct);
|
|
726
735
|
reportPaneContext({ pct: pct, used: used, win: win, cls: cls, model: contextData.model, cost: contextData.cost });
|
|
@@ -734,7 +743,7 @@ export function updateContextPanel() {
|
|
|
734
743
|
contextMiniFill.className = "context-mini-fill" + cls;
|
|
735
744
|
}
|
|
736
745
|
if (contextMiniLabel) {
|
|
737
|
-
contextMiniLabel.textContent = (win > 0 ? formatTokens(used) + "/" + formatTokens(win) : "
|
|
746
|
+
contextMiniLabel.textContent = (win > 0 ? formatTokens(used) + "/" + formatTokens(win) : "-");
|
|
738
747
|
}
|
|
739
748
|
// Header bar. A split group has no meaningful group-level context usage:
|
|
740
749
|
// the two members have separate windows, and the parent shell's own numbers
|
|
@@ -742,9 +751,9 @@ export function updateContextPanel() {
|
|
|
742
751
|
// chip instead, so hide the title-bar gauge while a split is open.
|
|
743
752
|
var splitOpen = !!store.get('splitPanes');
|
|
744
753
|
var headerCtxEl = store.get('headerContextEl');
|
|
745
|
-
if (headerCtxEl) headerCtxEl.classList.toggle("hidden", splitOpen);
|
|
746
|
-
if (contextMini) contextMini.classList.toggle("split-hidden", splitOpen);
|
|
747
|
-
if (
|
|
754
|
+
if (headerCtxEl) headerCtxEl.classList.toggle("hidden", splitOpen || !hasContext);
|
|
755
|
+
if (contextMini) contextMini.classList.toggle("split-hidden", splitOpen || !hasContext);
|
|
756
|
+
if (hasContext && !splitOpen) {
|
|
748
757
|
var statusArea = document.querySelector(".title-bar-content .status");
|
|
749
758
|
var hCtxEl = store.get('headerContextEl');
|
|
750
759
|
if (statusArea && !hCtxEl) {
|
|
@@ -808,7 +817,7 @@ export function accumulateContext(cost, usage, modelUsage, lastStreamInputTokens
|
|
|
808
817
|
// when available -- result.usage.input_tokens sums all API calls in a turn,
|
|
809
818
|
// inflating context usage when tools are involved.
|
|
810
819
|
// Falls back to the summed value for setups that don't emit message_start.
|
|
811
|
-
if (lastStreamInputTokens) {
|
|
820
|
+
if (typeof lastStreamInputTokens === "number") {
|
|
812
821
|
contextData.input = lastStreamInputTokens;
|
|
813
822
|
} else {
|
|
814
823
|
contextData.input = (usage.input_tokens || usage.inputTokens || 0)
|
|
@@ -817,6 +826,11 @@ export function accumulateContext(cost, usage, modelUsage, lastStreamInputTokens
|
|
|
817
826
|
contextData.output = usage.output_tokens || usage.outputTokens || 0;
|
|
818
827
|
contextData.cacheRead = usage.cache_read_input_tokens || usage.cacheReadInputTokens || 0;
|
|
819
828
|
contextData.cacheWrite = usage.cache_creation_input_tokens || usage.cacheCreationInputTokens || 0;
|
|
829
|
+
if (store.get('currentVendor') === "codex") {
|
|
830
|
+
contextData.currentInput = typeof lastStreamInputTokens === "number" ? lastStreamInputTokens : null;
|
|
831
|
+
} else {
|
|
832
|
+
contextData.currentInput = typeof lastStreamInputTokens === "number" ? lastStreamInputTokens : contextData.input;
|
|
833
|
+
}
|
|
820
834
|
}
|
|
821
835
|
contextData.turns++;
|
|
822
836
|
if (modelUsage) {
|
|
@@ -830,6 +844,21 @@ export function accumulateContext(cost, usage, modelUsage, lastStreamInputTokens
|
|
|
830
844
|
contextData.model = displayModel;
|
|
831
845
|
contextData.contextWindow = resolveContextWindow(displayModel, mu.contextWindow);
|
|
832
846
|
if (mu.maxOutputTokens) contextData.maxOutputTokens = mu.maxOutputTokens;
|
|
847
|
+
if (store.get('currentVendor') === "codex") {
|
|
848
|
+
var snapshot = mu.contextSnapshot;
|
|
849
|
+
if (snapshot) store.set({ richContextUsage: null });
|
|
850
|
+
if (snapshot && snapshot.valid === true
|
|
851
|
+
&& typeof snapshot.inputTokens === "number"
|
|
852
|
+
&& typeof snapshot.contextWindow === "number"
|
|
853
|
+
&& snapshot.contextWindow > 0) {
|
|
854
|
+
contextData.currentInput = snapshot.inputTokens;
|
|
855
|
+
contextData.contextWindow = snapshot.contextWindow;
|
|
856
|
+
} else {
|
|
857
|
+
// An explicit invalid marker is authoritative: do not retain an
|
|
858
|
+
// occupancy value from the previous live turn or history replay.
|
|
859
|
+
contextData.currentInput = null;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
833
862
|
|
|
834
863
|
// thinkingTokens and costBasis are per-model and optional: absent on
|
|
835
864
|
// older CLI builds and, for a resumed session, on the turns that ran
|
|
@@ -874,11 +903,17 @@ export function applyContextView(view) {
|
|
|
874
903
|
}
|
|
875
904
|
|
|
876
905
|
export function resetContextData() {
|
|
877
|
-
contextData = { contextWindow: 0, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0, thinking: null, costBasis: null };
|
|
906
|
+
contextData = { contextWindow: 0, currentInput: null, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0, thinking: null, costBasis: null };
|
|
878
907
|
store.set({ richContextUsage: null });
|
|
879
908
|
// hideCtxPopover + updateContextPanel handled by store subscriber
|
|
880
909
|
}
|
|
881
910
|
|
|
911
|
+
export function markContextUnavailable() {
|
|
912
|
+
contextData.currentInput = null;
|
|
913
|
+
store.set({ richContextUsage: null });
|
|
914
|
+
updateContextPanel();
|
|
915
|
+
}
|
|
916
|
+
|
|
882
917
|
export function resetContext() {
|
|
883
918
|
resetContextData();
|
|
884
919
|
// Keep view state, just reset data
|
|
@@ -952,8 +987,8 @@ export function renderCtxPopover() {
|
|
|
952
987
|
if (!ctxPopoverEl || !richContextUsage) return;
|
|
953
988
|
var d = richContextUsage;
|
|
954
989
|
var cats = d.categories || [];
|
|
955
|
-
var total = d.totalTokens || 0;
|
|
956
|
-
var max = d.maxTokens || 0;
|
|
990
|
+
var total = d.totalTokens != null ? d.totalTokens : (d.input_tokens || 0);
|
|
991
|
+
var max = d.maxTokens != null ? d.maxTokens : (d.contextWindow || 0);
|
|
957
992
|
var pct = d.percentage != null ? d.percentage : (max > 0 ? (total / max) * 100 : 0);
|
|
958
993
|
|
|
959
994
|
var html = "";
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { getWs } from './ws-ref.js';
|
|
3
|
+
import { firstInstalledVendor } from './vendor-priority.js';
|
|
4
|
+
|
|
5
|
+
function send(message) {
|
|
6
|
+
var ws = getWs();
|
|
7
|
+
if (!ws || ws.readyState !== 1) return false;
|
|
8
|
+
ws.send(JSON.stringify(message));
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
var sequence = 0;
|
|
13
|
+
|
|
14
|
+
function nextRequestId(prefix) {
|
|
15
|
+
sequence += 1;
|
|
16
|
+
return prefix + Date.now() + "-" + sequence;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function emptyState(accountId, projectSlug) {
|
|
20
|
+
return { loading: false, saving: false, getRequestId: null, saveRequestId: null, preference: null, preferencePresent: false, installedVendors: [], accountId: accountId || null, projectSlug: projectSlug || null, serverEpoch: null, canonicalRevision: 0, error: "" };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function requestDefaultVendor() {
|
|
24
|
+
var id = nextRequestId("default-vendor-");
|
|
25
|
+
var state = store.get("defaultVendorState") || {};
|
|
26
|
+
store.set({ defaultVendorState: Object.assign({}, state, { loading: true, getRequestId: id, error: "" }) });
|
|
27
|
+
if (!send({ type: "default_vendor_get", requestId: id })) {
|
|
28
|
+
store.set({ defaultVendorState: Object.assign({}, store.get("defaultVendorState") || {}, { loading: false, getRequestId: null, error: "Clay is offline. Reconnect and try again." }) });
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function beginDefaultVendorConnection() {
|
|
35
|
+
var state = store.get("defaultVendorState") || {};
|
|
36
|
+
store.set({ defaultVendorState: Object.assign({}, state, {
|
|
37
|
+
loading: false,
|
|
38
|
+
saving: false,
|
|
39
|
+
getRequestId: null,
|
|
40
|
+
saveRequestId: null,
|
|
41
|
+
preference: null,
|
|
42
|
+
preferencePresent: false,
|
|
43
|
+
installedVendors: [],
|
|
44
|
+
accountId: store.get("myUserId") || "default",
|
|
45
|
+
projectSlug: store.get("currentSlug") || null,
|
|
46
|
+
serverEpoch: null,
|
|
47
|
+
canonicalRevision: 0,
|
|
48
|
+
error: "",
|
|
49
|
+
}) });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function saveDefaultVendor(vendor) {
|
|
53
|
+
var state = store.get("defaultVendorState") || {};
|
|
54
|
+
if (vendor !== "" && (state.installedVendors || []).indexOf(vendor) === -1) return false;
|
|
55
|
+
var id = nextRequestId("default-vendor-save-");
|
|
56
|
+
store.set({ defaultVendorState: Object.assign({}, state, { saving: true, saveRequestId: id, error: "" }) });
|
|
57
|
+
if (!send({ type: "default_vendor_set", requestId: id, vendor: vendor })) {
|
|
58
|
+
store.set({ defaultVendorState: Object.assign({}, store.get("defaultVendorState") || {}, { saving: false, saveRequestId: null, error: "Clay is offline. Reconnect and try again." }) });
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function handleDefaultVendorMessage(msg) {
|
|
65
|
+
if (!msg || msg.type !== "default_vendor_state") return false;
|
|
66
|
+
var state = store.get("defaultVendorState") || {};
|
|
67
|
+
var accountId = store.get("myUserId") || "default";
|
|
68
|
+
var projectSlug = store.get("currentSlug") || null;
|
|
69
|
+
var isGetReply = !!msg.requestId && msg.requestId === state.getRequestId;
|
|
70
|
+
var isSaveReply = !!msg.requestId && msg.requestId === state.saveRequestId;
|
|
71
|
+
var isCorrelatedAccountFailure = (isGetReply || isSaveReply) && msg.accountId === null && msg.accountAvailable === false;
|
|
72
|
+
if ((!isCorrelatedAccountFailure && msg.accountId !== accountId) || (msg.accountId === null && !isCorrelatedAccountFailure)) return true;
|
|
73
|
+
if (Object.prototype.hasOwnProperty.call(msg, "projectSlug") && msg.projectSlug !== projectSlug) return true;
|
|
74
|
+
if (msg.serverEpoch && state.serverEpoch && msg.serverEpoch !== state.serverEpoch && (!msg.requestId || (msg.requestId !== state.getRequestId && msg.requestId !== state.saveRequestId))) return true;
|
|
75
|
+
if (msg.canonicalRevision !== undefined && msg.canonicalRevision < (state.canonicalRevision || 0) && msg.serverEpoch === state.serverEpoch) return true;
|
|
76
|
+
if (msg.requestId && !isGetReply && !isSaveReply) return true;
|
|
77
|
+
if (msg.requestId && !isSaveReply && msg.ready === false && state.saving) return true;
|
|
78
|
+
var next = Object.assign({}, state, {
|
|
79
|
+
accountId: msg.accountId || accountId,
|
|
80
|
+
projectSlug: projectSlug,
|
|
81
|
+
serverEpoch: msg.serverEpoch || state.serverEpoch || null,
|
|
82
|
+
canonicalRevision: typeof msg.canonicalRevision === "number" ? msg.canonicalRevision : state.canonicalRevision || 0,
|
|
83
|
+
installedVendors: Array.isArray(msg.installedVendors) ? msg.installedVendors : state.installedVendors || [],
|
|
84
|
+
loading: isGetReply ? false : state.loading,
|
|
85
|
+
saving: isSaveReply ? false : state.saving,
|
|
86
|
+
getRequestId: isGetReply ? null : state.getRequestId,
|
|
87
|
+
saveRequestId: isSaveReply ? null : state.saveRequestId,
|
|
88
|
+
error: msg.error || ""
|
|
89
|
+
});
|
|
90
|
+
if (Object.prototype.hasOwnProperty.call(msg, "preference")) {
|
|
91
|
+
next.preference = msg.preference || null;
|
|
92
|
+
next.preferencePresent = msg.preferencePresent === true;
|
|
93
|
+
}
|
|
94
|
+
store.set({ defaultVendorState: next });
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
store.subscribe(function (state, previous) {
|
|
99
|
+
if (state.myUserId !== previous.myUserId || state.currentSlug !== previous.currentSlug) {
|
|
100
|
+
var accountId = state.myUserId || "default";
|
|
101
|
+
store.set({ defaultVendorState: emptyState(accountId, state.currentSlug || null) });
|
|
102
|
+
if (state.connected) requestDefaultVendor();
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
export function resolvePreferredVendor(installedVendors) {
|
|
107
|
+
var installed = Array.isArray(installedVendors) ? installedVendors : [];
|
|
108
|
+
var state = store.get("defaultVendorState") || {};
|
|
109
|
+
if (state.preference && installed.indexOf(state.preference) !== -1) return state.preference;
|
|
110
|
+
return firstInstalledVendor(installed);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function defaultVendorUnavailable(installedVendors) {
|
|
114
|
+
var state = store.get("defaultVendorState") || {};
|
|
115
|
+
return !!state.preference && (installedVendors || []).indexOf(state.preference) === -1;
|
|
116
|
+
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
startNewSession
|
|
16
16
|
} from './sidebar-sessions.js';
|
|
17
17
|
import { VENDOR_AVATARS, VENDOR_NAMES, VENDOR_ORDER, VENDOR_HOMEPAGES, isExperimentalVendor } from './app-rendering.js';
|
|
18
|
+
import { defaultVendorUnavailable, resolvePreferredVendor, saveDefaultVendor } from './default-vendor.js';
|
|
18
19
|
import {
|
|
19
20
|
getCachedProjectList,
|
|
20
21
|
getCachedCurrentSlug,
|
|
@@ -675,14 +676,17 @@ function renderMobileSessionsInto(container) {
|
|
|
675
676
|
// the project's last-used vendor, plus a chevron that expands the full
|
|
676
677
|
// vendor list. The list expands inline rather than as a fixed-position
|
|
677
678
|
// popover because it lives inside a scrolling bottom sheet.
|
|
678
|
-
var mobileDefaultVendor = resolveDefaultVendor();
|
|
679
|
+
var mobileDefaultVendor = resolvePreferredVendor(store.get('installedVendors') || []) || resolveDefaultVendor();
|
|
679
680
|
var mobileInstalled = store.get('installedVendors') || [];
|
|
681
|
+
var mobileDefaultState = store.get('defaultVendorState') || {};
|
|
680
682
|
|
|
681
683
|
var newRow = document.createElement("div");
|
|
682
684
|
newRow.className = "mobile-session-new-row";
|
|
683
685
|
|
|
684
686
|
var mainBtn = document.createElement("button");
|
|
685
687
|
mainBtn.className = "mobile-session-new mobile-session-new-main";
|
|
688
|
+
mainBtn.title = "New " + (VENDOR_NAMES[mobileDefaultVendor] || mobileDefaultVendor) + " session";
|
|
689
|
+
if (defaultVendorUnavailable(mobileInstalled)) mainBtn.title += " (saved default is unavailable; using installed priority)";
|
|
686
690
|
mainBtn.innerHTML = '<img src="' + (VENDOR_AVATARS[mobileDefaultVendor] || VENDOR_AVATARS.claude) +
|
|
687
691
|
'" class="mobile-session-new-icon" alt=""><span>New ' + (VENDOR_NAMES[mobileDefaultVendor] || mobileDefaultVendor) + ' session</span>';
|
|
688
692
|
mainBtn.addEventListener("click", function () {
|
|
@@ -704,12 +708,20 @@ function renderMobileSessionsInto(container) {
|
|
|
704
708
|
});
|
|
705
709
|
newRow.appendChild(chevronBtn);
|
|
706
710
|
|
|
711
|
+
if (defaultVendorUnavailable(mobileInstalled) && mobileDefaultState.preference) {
|
|
712
|
+
var mobileFallbackNote = document.createElement("div");
|
|
713
|
+
mobileFallbackNote.className = "mobile-session-new-preference-status mobile-session-new-fallback-note";
|
|
714
|
+
mobileFallbackNote.textContent = "Saved default " + (VENDOR_NAMES[mobileDefaultState.preference] || mobileDefaultState.preference) + " is unavailable. Using " + (VENDOR_NAMES[mobileDefaultVendor] || mobileDefaultVendor) + ".";
|
|
715
|
+
}
|
|
716
|
+
|
|
707
717
|
// Every known vendor is listed. Uninstalled ones stay visible but disabled,
|
|
708
718
|
// and tapping one opens the vendor's homepage.
|
|
709
719
|
for (var vi = 0; vi < VENDOR_ORDER.length; vi++) {
|
|
710
720
|
(function (vendor) {
|
|
711
721
|
var isInstalled = mobileInstalled.indexOf(vendor) !== -1;
|
|
712
722
|
var name = VENDOR_NAMES[vendor] || vendor;
|
|
723
|
+
var vendorRow = document.createElement("div");
|
|
724
|
+
vendorRow.className = "mobile-vendor-row";
|
|
713
725
|
var vBtn = document.createElement("button");
|
|
714
726
|
vBtn.className = "mobile-session-new mobile-session-new-vendor";
|
|
715
727
|
if (!isInstalled) vBtn.classList.add("disabled");
|
|
@@ -726,11 +738,46 @@ function renderMobileSessionsInto(container) {
|
|
|
726
738
|
startNewSession(vendor);
|
|
727
739
|
closeMobileSheet();
|
|
728
740
|
});
|
|
729
|
-
|
|
741
|
+
vendorRow.appendChild(vBtn);
|
|
742
|
+
if (isInstalled) {
|
|
743
|
+
var defaultBtn = document.createElement("button");
|
|
744
|
+
defaultBtn.type = "button";
|
|
745
|
+
defaultBtn.className = "mobile-vendor-set-default";
|
|
746
|
+
defaultBtn.textContent = mobileDefaultState.saving ? "Saving…" : (vendor === mobileDefaultVendor && !defaultVendorUnavailable(mobileInstalled) ? "Default" : "Set default");
|
|
747
|
+
defaultBtn.disabled = mobileDefaultState.saving === true;
|
|
748
|
+
defaultBtn.setAttribute("aria-label", "Set " + name + " as default vendor");
|
|
749
|
+
defaultBtn.addEventListener("click", function (e) {
|
|
750
|
+
e.preventDefault();
|
|
751
|
+
e.stopPropagation();
|
|
752
|
+
saveDefaultVendor(vendor);
|
|
753
|
+
});
|
|
754
|
+
vendorRow.appendChild(defaultBtn);
|
|
755
|
+
}
|
|
756
|
+
vendorList.appendChild(vendorRow);
|
|
730
757
|
})(VENDOR_ORDER[vi]);
|
|
731
758
|
}
|
|
732
759
|
|
|
760
|
+
var automaticBtn = document.createElement("button");
|
|
761
|
+
automaticBtn.className = "mobile-session-new mobile-session-new-vendor mobile-session-new-automatic";
|
|
762
|
+
automaticBtn.type = "button";
|
|
763
|
+
automaticBtn.textContent = "Use automatic default";
|
|
764
|
+
automaticBtn.disabled = mobileDefaultState.saving === true;
|
|
765
|
+
automaticBtn.addEventListener("click", function (e) {
|
|
766
|
+
e.preventDefault();
|
|
767
|
+
e.stopPropagation();
|
|
768
|
+
saveDefaultVendor("");
|
|
769
|
+
});
|
|
770
|
+
vendorList.appendChild(automaticBtn);
|
|
771
|
+
if (mobileDefaultState.error) {
|
|
772
|
+
var mobileStatus = document.createElement("div");
|
|
773
|
+
mobileStatus.className = "mobile-session-new-preference-status";
|
|
774
|
+
mobileStatus.setAttribute("role", "alert");
|
|
775
|
+
mobileStatus.textContent = mobileDefaultState.error;
|
|
776
|
+
vendorList.appendChild(mobileStatus);
|
|
777
|
+
}
|
|
778
|
+
|
|
733
779
|
container.appendChild(newRow);
|
|
780
|
+
if (mobileFallbackNote) container.appendChild(mobileFallbackNote);
|
|
734
781
|
container.appendChild(vendorList);
|
|
735
782
|
|
|
736
783
|
// Partition: loop sessions vs normal sessions (same logic as desktop renderSessionList)
|
|
@@ -15,6 +15,7 @@ import { refreshMobileChatSheet } from './sidebar-mobile.js';
|
|
|
15
15
|
import { VENDOR_AVATARS, VENDOR_NAMES, VENDOR_ORDER, VENDOR_HOMEPAGES, isExperimentalVendor } from './app-rendering.js';
|
|
16
16
|
import { openGroup, separateGroup } from './split-view.js';
|
|
17
17
|
import { groupedSessionIds } from './split-group-helpers.js';
|
|
18
|
+
import { defaultVendorUnavailable, resolvePreferredVendor, saveDefaultVendor } from './default-vendor.js';
|
|
18
19
|
import { openPairDialog } from './split-pair-ui.js';
|
|
19
20
|
import {
|
|
20
21
|
hierarchyItemMatches,
|
|
@@ -41,6 +42,7 @@ var countdownContainer = null;
|
|
|
41
42
|
|
|
42
43
|
// --- Session context menu ---
|
|
43
44
|
var sessionCtxMenu = null;
|
|
45
|
+
var sessionCtxMenuAnchor = null;
|
|
44
46
|
var sessionCtxSessionId = null;
|
|
45
47
|
var draggedSessionId = null;
|
|
46
48
|
var draggedSessionBookmarked = false;
|
|
@@ -48,6 +50,18 @@ var headerSearchOpen = false;
|
|
|
48
50
|
var armedDeleteSessionId = null;
|
|
49
51
|
var armedDeleteTimer = null;
|
|
50
52
|
|
|
53
|
+
store.subscribe(function (state, previous) {
|
|
54
|
+
if (state.defaultVendorState !== previous.defaultVendorState || state.installedVendors !== previous.installedVendors) {
|
|
55
|
+
var reopenNewMenu = !!(sessionCtxMenu && sessionCtxMenuAnchor);
|
|
56
|
+
if (reopenNewMenu) closeSessionCtxMenu();
|
|
57
|
+
if (getSessionListEl()) renderSessionList();
|
|
58
|
+
if (reopenNewMenu) {
|
|
59
|
+
var freshAnchor = getSessionListEl().querySelector(".session-top-action.split-chevron");
|
|
60
|
+
if (freshAnchor) showNewSessionMenu(freshAnchor, resolveDefaultVendor());
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
51
65
|
function sendSessionBookmark(sessionId, bookmarked) {
|
|
52
66
|
if (getWs() && store.get('connected')) {
|
|
53
67
|
getWs().send(JSON.stringify({ type: "set_session_bookmark", sessionId: sessionId, bookmarked: !!bookmarked }));
|
|
@@ -317,10 +331,7 @@ function appendSessionCloseButton(el, session) {
|
|
|
317
331
|
// installed-vendor priority (Claude, then Codex, then the remaining vendors).
|
|
318
332
|
export function resolveDefaultVendor() {
|
|
319
333
|
var installed = store.get('installedVendors') || [];
|
|
320
|
-
|
|
321
|
-
if (installed.indexOf(VENDOR_ORDER[i]) !== -1) return VENDOR_ORDER[i];
|
|
322
|
-
}
|
|
323
|
-
return "claude";
|
|
334
|
+
return resolvePreferredVendor(installed) || "claude";
|
|
324
335
|
}
|
|
325
336
|
|
|
326
337
|
// Single entry point for session creation from the sidebar. Optimistically
|
|
@@ -361,6 +372,7 @@ function renderSessionTopActions() {
|
|
|
361
372
|
mainBtn.className = "session-top-action split-main";
|
|
362
373
|
mainBtn.type = "button";
|
|
363
374
|
mainBtn.title = "New " + (VENDOR_NAMES[defaultVendor] || defaultVendor) + " session";
|
|
375
|
+
if (defaultVendorUnavailable(store.get('installedVendors') || [])) mainBtn.title += " (saved default is unavailable; using installed priority)";
|
|
364
376
|
mainBtn.innerHTML = '<img src="' + (VENDOR_AVATARS[defaultVendor] || VENDOR_AVATARS.claude) +
|
|
365
377
|
'" class="session-top-action-icon" alt=""><span>New session</span>';
|
|
366
378
|
mainBtn.addEventListener("click", function () {
|
|
@@ -394,6 +406,15 @@ function showNewSessionMenu(anchorBtn, defaultVendor) {
|
|
|
394
406
|
var menu = document.createElement("div");
|
|
395
407
|
menu.className = "session-ctx-menu session-new-menu";
|
|
396
408
|
var installed = store.get('installedVendors') || [];
|
|
409
|
+
var defaultVendorState = store.get('defaultVendorState') || {};
|
|
410
|
+
sessionCtxMenuAnchor = anchorBtn;
|
|
411
|
+
|
|
412
|
+
if (defaultVendorUnavailable(installed) && defaultVendorState.preference) {
|
|
413
|
+
var fallbackNote = document.createElement("div");
|
|
414
|
+
fallbackNote.className = "session-new-preference-status session-new-fallback-note";
|
|
415
|
+
fallbackNote.textContent = "Saved default " + (VENDOR_NAMES[defaultVendorState.preference] || defaultVendorState.preference) + " is unavailable. Using " + (VENDOR_NAMES[defaultVendor] || defaultVendor) + ".";
|
|
416
|
+
menu.appendChild(fallbackNote);
|
|
417
|
+
}
|
|
397
418
|
|
|
398
419
|
// Every known vendor is listed. Uninstalled ones stay visible but disabled,
|
|
399
420
|
// and clicking one opens the vendor's homepage so the user can install it.
|
|
@@ -401,6 +422,8 @@ function showNewSessionMenu(anchorBtn, defaultVendor) {
|
|
|
401
422
|
(function (vendor) {
|
|
402
423
|
var isInstalled = installed.indexOf(vendor) !== -1;
|
|
403
424
|
var name = VENDOR_NAMES[vendor] || vendor;
|
|
425
|
+
var row = document.createElement("div");
|
|
426
|
+
row.className = "session-new-vendor-row";
|
|
404
427
|
var item = document.createElement("button");
|
|
405
428
|
item.className = "session-ctx-item session-new-vendor";
|
|
406
429
|
if (!isInstalled) item.classList.add("disabled");
|
|
@@ -423,10 +446,46 @@ function showNewSessionMenu(anchorBtn, defaultVendor) {
|
|
|
423
446
|
}
|
|
424
447
|
startNewSession(vendor);
|
|
425
448
|
});
|
|
426
|
-
|
|
449
|
+
row.appendChild(item);
|
|
450
|
+
if (isInstalled) {
|
|
451
|
+
var defaultBtn = document.createElement("button");
|
|
452
|
+
defaultBtn.type = "button";
|
|
453
|
+
defaultBtn.className = "session-new-set-default";
|
|
454
|
+
defaultBtn.textContent = defaultVendorState.saving ? "Saving…" : (vendor === defaultVendor && !defaultVendorUnavailable(installed) ? "Default" : "Set default");
|
|
455
|
+
defaultBtn.disabled = defaultVendorState.saving === true;
|
|
456
|
+
defaultBtn.setAttribute("aria-label", "Set " + name + " as default vendor");
|
|
457
|
+
defaultBtn.title = "Use " + name + " for New session by default";
|
|
458
|
+
defaultBtn.addEventListener("click", function (e) {
|
|
459
|
+
e.preventDefault();
|
|
460
|
+
e.stopPropagation();
|
|
461
|
+
saveDefaultVendor(vendor);
|
|
462
|
+
});
|
|
463
|
+
row.appendChild(defaultBtn);
|
|
464
|
+
}
|
|
465
|
+
menu.appendChild(row);
|
|
427
466
|
})(VENDOR_ORDER[i]);
|
|
428
467
|
}
|
|
429
468
|
|
|
469
|
+
var automatic = document.createElement("button");
|
|
470
|
+
automatic.className = "session-ctx-item session-new-automatic";
|
|
471
|
+
automatic.type = "button";
|
|
472
|
+
automatic.textContent = "Use automatic default";
|
|
473
|
+
automatic.disabled = defaultVendorState.saving === true;
|
|
474
|
+
automatic.title = "Use Clay's installed-vendor priority";
|
|
475
|
+
automatic.addEventListener("click", function (e) {
|
|
476
|
+
e.stopPropagation();
|
|
477
|
+
closeSessionCtxMenu();
|
|
478
|
+
saveDefaultVendor("");
|
|
479
|
+
});
|
|
480
|
+
menu.appendChild(automatic);
|
|
481
|
+
if (defaultVendorState.error) {
|
|
482
|
+
var status = document.createElement("div");
|
|
483
|
+
status.className = "session-new-preference-status";
|
|
484
|
+
status.setAttribute("role", "alert");
|
|
485
|
+
status.textContent = defaultVendorState.error;
|
|
486
|
+
menu.appendChild(status);
|
|
487
|
+
}
|
|
488
|
+
|
|
430
489
|
if (installed.length > 0) {
|
|
431
490
|
var pairSep = document.createElement("div");
|
|
432
491
|
pairSep.className = "session-ctx-sep";
|
|
@@ -660,6 +719,7 @@ export function getExpandedLoopRuns() {
|
|
|
660
719
|
// --- Context menu ---
|
|
661
720
|
|
|
662
721
|
function closeSessionCtxMenu() {
|
|
722
|
+
sessionCtxMenuAnchor = null;
|
|
663
723
|
if (sessionCtxMenu) {
|
|
664
724
|
sessionCtxMenu.remove();
|
|
665
725
|
sessionCtxMenu = null;
|
|
@@ -583,7 +583,12 @@ function attachMessageProcessor(ctx) {
|
|
|
583
583
|
&& /not logged in/i.test(previewTrimmed) && /\/login/i.test(previewTrimmed);
|
|
584
584
|
// Fetch rich context usage breakdown (fire-and-forget, non-blocking)
|
|
585
585
|
if (session.queryInstance && typeof session.queryInstance.getContextUsage === "function") {
|
|
586
|
+
var contextQuery = session.queryInstance;
|
|
587
|
+
var contextTurn = session.turnCount;
|
|
586
588
|
contextUsage.readUsage(session, "summary").then(function(ctxUsage) {
|
|
589
|
+
if (session.queryInstance !== contextQuery || session.turnCount !== contextTurn) return;
|
|
590
|
+
// null means the shared reader suppressed a superseded response;
|
|
591
|
+
// only an adapter's explicit unavailable object may clear the UI.
|
|
587
592
|
if (!ctxUsage) return;
|
|
588
593
|
session.lastContextUsage = ctxUsage;
|
|
589
594
|
sendToSession(session, { type: "context_usage", data: ctxUsage });
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Authenticated user-wide New session vendor preference service.
|
|
2
|
+
|
|
3
|
+
function attachDefaultVendorService(deps) {
|
|
4
|
+
var users = deps.users;
|
|
5
|
+
var homeChatHandler = deps.homeChatHandler;
|
|
6
|
+
var forEachAppClient = deps.forEachAppClient;
|
|
7
|
+
var revisions = {};
|
|
8
|
+
var epoch = deps.serverEpoch || require("crypto").randomBytes(12).toString("hex");
|
|
9
|
+
|
|
10
|
+
function multiUser() { return users.isMultiUser() === true; }
|
|
11
|
+
function actor(ws) {
|
|
12
|
+
if (!multiUser()) return { userId: "default", user: true };
|
|
13
|
+
var claimed = ws && ws._clayUser && ws._clayUser.id;
|
|
14
|
+
var user = claimed ? users.findUserById(claimed) : null;
|
|
15
|
+
return user ? { userId: user.id, user: user } : { userId: null, user: null };
|
|
16
|
+
}
|
|
17
|
+
function sameActor(ws, userId) { var current = actor(ws); return current.user && current.userId === userId; }
|
|
18
|
+
function send(ws, message) { if (ws && ws.readyState === 1) ws.send(JSON.stringify(Object.assign({ serverEpoch: epoch }, message))); }
|
|
19
|
+
function revision(userId) { return revisions[userId] || 0; }
|
|
20
|
+
function installedVendors(ws, who) {
|
|
21
|
+
var found;
|
|
22
|
+
try { found = homeChatHandler.findMateProject(multiUser() ? who.userId : null, null, true); } catch (e) { return []; }
|
|
23
|
+
if (!found || !found.ctx || typeof found.ctx.getVendorModelAvailability !== "function") return [];
|
|
24
|
+
var socket = Object.create(ws || null);
|
|
25
|
+
socket._clayUser = who.user === true ? null : who.user;
|
|
26
|
+
var choices = found.ctx.getVendorModelAvailability(socket) || [];
|
|
27
|
+
return choices.filter(function (choice) { return choice && choice.installed === true; }).map(function (choice) { return choice.id; });
|
|
28
|
+
}
|
|
29
|
+
function broadcast(userId, message, except) {
|
|
30
|
+
forEachAppClient(function (client) { if (client === except || !sameActor(client, userId)) return; send(client, message); });
|
|
31
|
+
}
|
|
32
|
+
function handle(ws, msg, projectSlug) {
|
|
33
|
+
if (!msg || ["default_vendor_get", "default_vendor_set"].indexOf(msg.type) === -1) return false;
|
|
34
|
+
var requestId = typeof msg.requestId === "string" ? msg.requestId.slice(0, 200) : null;
|
|
35
|
+
var who = actor(ws);
|
|
36
|
+
if (!who.user) { send(ws, { type: "default_vendor_state", requestId: requestId, accountAvailable: false, accountId: null, projectSlug: projectSlug || null, installedVendors: [], preference: null, error: "Your account is no longer available." }); return true; }
|
|
37
|
+
var installed = installedVendors(ws, who);
|
|
38
|
+
if (msg.type === "default_vendor_get") {
|
|
39
|
+
var saved = users.getDefaultVendorPreference(who.userId);
|
|
40
|
+
send(ws, { type: "default_vendor_state", requestId: requestId, projectSlug: projectSlug || null, accountAvailable: true, accountId: who.userId, installedVendors: installed, preference: saved.vendor, preferencePresent: saved.present, ready: !saved.error, canonicalRevision: revision(who.userId), error: saved.error || "" });
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
if (msg.vendor !== "" && (typeof msg.vendor !== "string" || installed.indexOf(msg.vendor) === -1)) {
|
|
44
|
+
send(ws, { type: "default_vendor_state", requestId: requestId, projectSlug: projectSlug || null, accountAvailable: true, accountId: who.userId, installedVendors: installed, preference: users.getDefaultVendorPreference(who.userId).vendor, ready: false, canonicalRevision: revision(who.userId), error: "That vendor is not installed or authorized." });
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
var savedResult = users.setDefaultVendorPreference(who.userId, msg.vendor);
|
|
48
|
+
if (!savedResult.ok) { send(ws, { type: "default_vendor_state", requestId: requestId, projectSlug: projectSlug || null, accountAvailable: true, accountId: who.userId, installedVendors: installed, ready: false, error: savedResult.error }); return true; }
|
|
49
|
+
revisions[who.userId] = revision(who.userId) + 1;
|
|
50
|
+
var state = { type: "default_vendor_state", requestId: null, projectSlug: projectSlug || null, accountAvailable: true, accountId: who.userId, installedVendors: installed, preference: msg.vendor || null, preferencePresent: !!msg.vendor, ready: true, canonicalRevision: revisions[who.userId], error: "" };
|
|
51
|
+
var broadcastState = Object.assign({}, state);
|
|
52
|
+
delete broadcastState.projectSlug;
|
|
53
|
+
broadcast(who.userId, broadcastState, ws);
|
|
54
|
+
send(ws, Object.assign({}, state, { requestId: requestId }));
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
return { handleMessage: handle };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { attachDefaultVendorService: attachDefaultVendorService };
|
package/lib/server.js
CHANGED
|
@@ -17,6 +17,7 @@ var serverHomeChat = require("./server-home-chat");
|
|
|
17
17
|
var shouldSuppressHomeResponse = require("./home-response-notifications").shouldSuppressHomeResponse;
|
|
18
18
|
var serverHomePreferences = require("./server-home-preferences");
|
|
19
19
|
var serverDefaultAi = require("./server-default-ai");
|
|
20
|
+
var serverDefaultVendor = require("./server-default-vendor");
|
|
20
21
|
var serverCursorSharing = require("./server-cursor-sharing");
|
|
21
22
|
var serverTools = require("./server-tools");
|
|
22
23
|
var serverAdmin = require("./server-admin");
|
|
@@ -1067,6 +1068,7 @@ function createServer(opts) {
|
|
|
1067
1068
|
var wss = new WebSocketServer({ noServer: true });
|
|
1068
1069
|
var homePreferencesHandler = null;
|
|
1069
1070
|
var defaultAiService = null;
|
|
1071
|
+
var defaultVendorService = null;
|
|
1070
1072
|
var cursorSharingService = null;
|
|
1071
1073
|
|
|
1072
1074
|
// Slug-less /ws handler: lets a user with no projects yet load the regular
|
|
@@ -1552,6 +1554,7 @@ function createServer(opts) {
|
|
|
1552
1554
|
});
|
|
1553
1555
|
|
|
1554
1556
|
defaultAiService = serverDefaultAi.attachDefaultAiService({ users: users, homeChatHandler: homeChatHandler, forEachAppClient: forEachAppClient });
|
|
1557
|
+
defaultVendorService = serverDefaultVendor.attachDefaultVendorService({ users: users, homeChatHandler: homeChatHandler, forEachAppClient: forEachAppClient });
|
|
1555
1558
|
cursorSharingService = serverCursorSharing.attachCursorSharingService({ users: users, forEachAppClient: forEachAppClient });
|
|
1556
1559
|
|
|
1557
1560
|
// --- User-installed home tools handler ---
|
|
@@ -1580,6 +1583,7 @@ function createServer(opts) {
|
|
|
1580
1583
|
|
|
1581
1584
|
function handleDmMessage(ws, msg, routedProjectSlug) {
|
|
1582
1585
|
if (cursorSharingService && cursorSharingService.handleMessage(ws, msg)) return true;
|
|
1586
|
+
if (defaultVendorService && defaultVendorService.handleMessage(ws, msg, routedProjectSlug)) return true;
|
|
1583
1587
|
if (defaultAiService && defaultAiService.handleMessage(ws, msg)) return true;
|
|
1584
1588
|
if (assignmentService.handleMessage(ws, msg, routedProjectSlug)) return true;
|
|
1585
1589
|
if (dmHandler.handleMessage(ws, msg)) return true;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Durable user-scoped New session vendor preference storage.
|
|
2
|
+
|
|
3
|
+
var PREFERENCE_KEY = "defaultVendorPreference";
|
|
4
|
+
|
|
5
|
+
function validVendor(value) {
|
|
6
|
+
return typeof value === "string" && /^[a-z0-9][a-z0-9_-]{0,39}$/.test(value);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function attachDefaultVendorPreferences(deps) {
|
|
10
|
+
var loadUsers = deps.loadUsers;
|
|
11
|
+
var saveUsers = deps.saveUsers;
|
|
12
|
+
var isMultiUser = deps.isMultiUser || function () { return false; };
|
|
13
|
+
var configModule = deps.configModule || require("./config");
|
|
14
|
+
|
|
15
|
+
function getRecord(data, userId) {
|
|
16
|
+
var records = Array.isArray(data.users) ? data.users : [];
|
|
17
|
+
for (var i = 0; i < records.length; i++) if (records[i] && records[i].id === userId) return records[i];
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readValue(value, source) {
|
|
22
|
+
if (value === undefined || value === null || value === "") return { present: false, vendor: null, source: null };
|
|
23
|
+
if (!validVendor(value)) return { present: true, vendor: null, source: source, error: "Default coding vendor is invalid." };
|
|
24
|
+
return { present: true, vendor: value, source: source };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getDefaultVendor(userId) {
|
|
28
|
+
var data;
|
|
29
|
+
try { data = loadUsers(); } catch (error) { return { present: false, vendor: null, source: null, error: "Unable to read default coding vendor: " + (error.message || error) }; }
|
|
30
|
+
var record = getRecord(data, userId);
|
|
31
|
+
if (isMultiUser() && !record) return { present: false, vendor: null, source: null, error: "User not found." };
|
|
32
|
+
if (record && Object.prototype.hasOwnProperty.call(record, PREFERENCE_KEY)) return readValue(record[PREFERENCE_KEY], "explicit");
|
|
33
|
+
if (record) return { present: false, vendor: null, source: null };
|
|
34
|
+
if (userId === "default") {
|
|
35
|
+
var config;
|
|
36
|
+
try { config = configModule.loadConfig() || {}; } catch (configError) { return { present: false, vendor: null, source: null, error: "Unable to read default coding vendor: " + (configError.message || configError) }; }
|
|
37
|
+
if (Object.prototype.hasOwnProperty.call(config, PREFERENCE_KEY)) return readValue(config[PREFERENCE_KEY], "explicit");
|
|
38
|
+
}
|
|
39
|
+
return { present: false, vendor: null, source: null };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function setDefaultVendor(userId, vendor) {
|
|
43
|
+
if (vendor !== "" && !validVendor(vendor)) return { ok: false, error: "Default coding vendor is invalid." };
|
|
44
|
+
var data;
|
|
45
|
+
try { data = loadUsers(); } catch (error) { return { ok: false, error: "Unable to read default coding vendor: " + (error.message || error) }; }
|
|
46
|
+
var record = getRecord(data, userId);
|
|
47
|
+
if (isMultiUser() && !record) return { ok: false, error: "User not found." };
|
|
48
|
+
if (record) {
|
|
49
|
+
var nextData = Object.assign({}, data, { users: (data.users || []).map(function (item) {
|
|
50
|
+
if (item !== record) return item;
|
|
51
|
+
var next = Object.assign({}, item);
|
|
52
|
+
if (vendor) next.defaultVendorPreference = vendor;
|
|
53
|
+
else delete next.defaultVendorPreference;
|
|
54
|
+
return next;
|
|
55
|
+
}) });
|
|
56
|
+
try { saveUsers(nextData); } catch (saveError) { return { ok: false, error: "Unable to save default coding vendor: " + (saveError.message || saveError) }; }
|
|
57
|
+
if (!vendor && userId === "default") {
|
|
58
|
+
try {
|
|
59
|
+
var recordConfig = configModule.loadConfig() || {};
|
|
60
|
+
delete recordConfig[PREFERENCE_KEY];
|
|
61
|
+
configModule.saveConfig(Object.assign({}, recordConfig));
|
|
62
|
+
} catch (recordConfigError) { return { ok: false, error: "Unable to clear default coding vendor: " + (recordConfigError.message || recordConfigError) }; }
|
|
63
|
+
}
|
|
64
|
+
return { ok: true, vendor: vendor };
|
|
65
|
+
}
|
|
66
|
+
if (userId !== "default") return { ok: false, error: "User not found." };
|
|
67
|
+
try {
|
|
68
|
+
var current = configModule.loadConfig() || {};
|
|
69
|
+
var nextConfig = Object.assign({}, current);
|
|
70
|
+
if (vendor) nextConfig.defaultVendorPreference = vendor;
|
|
71
|
+
else delete nextConfig.defaultVendorPreference;
|
|
72
|
+
configModule.saveConfig(nextConfig);
|
|
73
|
+
return { ok: true, vendor: vendor };
|
|
74
|
+
} catch (configSaveError) { return { ok: false, error: "Unable to save default coding vendor: " + (configSaveError.message || configSaveError) }; }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { getDefaultVendor: getDefaultVendor, setDefaultVendor: setDefaultVendor, validVendor: validVendor };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = { attachDefaultVendorPreferences: attachDefaultVendorPreferences, validVendor: validVendor, PREFERENCE_KEY: PREFERENCE_KEY };
|
package/lib/users.js
CHANGED
|
@@ -11,6 +11,7 @@ var { attachHomeSurfacePreferences } = require("./users-home-surface-preferences
|
|
|
11
11
|
var { attachExperimentalPreferences } = require("./users-experimental-preferences");
|
|
12
12
|
var { attachScheduledTaskPreferences } = require("./users-scheduled-task-preferences");
|
|
13
13
|
var { attachDefaultAiPreferences } = require("./users-default-ai-preferences");
|
|
14
|
+
var { attachDefaultVendorPreferences } = require("./users-default-vendor-preferences");
|
|
14
15
|
|
|
15
16
|
var USERS_FILE = path.join(CONFIG_DIR, "users.json");
|
|
16
17
|
|
|
@@ -404,6 +405,7 @@ var homeSurfacePreferences = attachHomeSurfacePreferences({ loadUsers: loadUsers
|
|
|
404
405
|
var experimentalPreferences = attachExperimentalPreferences({ loadUsers: loadUsers, saveUsers: saveUsers });
|
|
405
406
|
var scheduledTaskPreferences = attachScheduledTaskPreferences({ loadUsers: loadUsers, saveUsers: saveUsers });
|
|
406
407
|
var defaultAiPreferences = attachDefaultAiPreferences({ loadUsers: loadUsers, saveUsers: saveUsers, isMultiUser: auth.isMultiUser });
|
|
408
|
+
var defaultVendorPreferences = attachDefaultVendorPreferences({ loadUsers: loadUsers, saveUsers: saveUsers, isMultiUser: auth.isMultiUser });
|
|
407
409
|
|
|
408
410
|
// Alias auth functions
|
|
409
411
|
var isMultiUser = auth.isMultiUser;
|
|
@@ -464,6 +466,8 @@ var getScheduledTaskInterviewEngine = scheduledTaskPreferences.get;
|
|
|
464
466
|
var setScheduledTaskInterviewEngine = scheduledTaskPreferences.set;
|
|
465
467
|
var getDefaultAiPreference = defaultAiPreferences.getDefaultAiPreference;
|
|
466
468
|
var setDefaultAiPreference = defaultAiPreferences.setDefaultAiPreference;
|
|
469
|
+
var getDefaultVendorPreference = defaultVendorPreferences.getDefaultVendor;
|
|
470
|
+
var setDefaultVendorPreference = defaultVendorPreferences.setDefaultVendor;
|
|
467
471
|
|
|
468
472
|
module.exports = {
|
|
469
473
|
USERS_FILE: USERS_FILE,
|
|
@@ -545,6 +549,8 @@ module.exports = {
|
|
|
545
549
|
setScheduledTaskInterviewEngine: setScheduledTaskInterviewEngine,
|
|
546
550
|
getDefaultAiPreference: getDefaultAiPreference,
|
|
547
551
|
setDefaultAiPreference: setDefaultAiPreference,
|
|
552
|
+
getDefaultVendorPreference: getDefaultVendorPreference,
|
|
553
|
+
setDefaultVendorPreference: setDefaultVendorPreference,
|
|
548
554
|
getDeletedBuiltinKeys: getDeletedBuiltinKeys,
|
|
549
555
|
addDeletedBuiltinKey: addDeletedBuiltinKey,
|
|
550
556
|
removeDeletedBuiltinKey: removeDeletedBuiltinKey,
|
package/lib/ws-schema.js
CHANGED
|
@@ -723,6 +723,9 @@ var schema = {
|
|
|
723
723
|
"default_ai_get": { direction: "c2s", handler: "lib/server-default-ai.js", description: "Read the authenticated user's shared Default AI and available providers" },
|
|
724
724
|
"default_ai_catalog_get": { direction: "c2s", handler: "lib/server-default-ai.js", description: "Read one installed provider catalog for the Default AI picker" },
|
|
725
725
|
"default_ai_set": { direction: "c2s", handler: "lib/server-default-ai.js", description: "Validate and persist the authenticated user's shared Default AI" },
|
|
726
|
+
"default_vendor_get": { direction: "c2s", handler: "lib/server-default-vendor.js", description: "Read the authenticated user's New session vendor preference" },
|
|
727
|
+
"default_vendor_set": { direction: "c2s", handler: "lib/server-default-vendor.js", description: "Validate and persist the authenticated user's New session vendor preference" },
|
|
728
|
+
"default_vendor_state": { direction: "s2c", handler: "lib/public/modules/default-vendor.js", description: "Correlated or same-user-broadcast New session vendor preference" },
|
|
726
729
|
"default_ai_state": { direction: "s2c", handler: "lib/public/modules/default-ai.js", description: "Correlated or same-user-broadcast canonical Default AI state" },
|
|
727
730
|
"default_ai_catalog": { direction: "s2c", handler: "lib/public/modules/default-ai.js", description: "Correlated Default AI provider catalog" },
|
|
728
731
|
"home_mate_history": { direction: "s2c", handler: "lib/public/modules/home-mate-chat.js", description: "Selected mate's initial or refreshed chat history" },
|
|
@@ -82,13 +82,26 @@ function isSafeOpenCodePermission(value) {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
function validateOpenCodeConfig(config) {
|
|
85
|
-
if (!config || config
|
|
85
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
86
|
+
throw new Error("OpenCode resolved configuration is malformed");
|
|
87
|
+
}
|
|
88
|
+
var globalPermission = config.permission;
|
|
89
|
+
var globalAsk = globalPermission === "ask" || (
|
|
90
|
+
isSafeOpenCodePermission(globalPermission) && globalPermission["*"] === "ask"
|
|
91
|
+
);
|
|
92
|
+
if (!globalAsk) {
|
|
86
93
|
throw new Error("OpenCode resolved configuration does not preserve global ask permissions");
|
|
87
94
|
}
|
|
95
|
+
if (config.agent !== undefined && (!config.agent || typeof config.agent !== "object" || Array.isArray(config.agent))) {
|
|
96
|
+
throw new Error("OpenCode resolved configuration has malformed agents");
|
|
97
|
+
}
|
|
88
98
|
var agents = config.agent || {};
|
|
89
99
|
var names = Object.keys(agents);
|
|
90
100
|
for (var i = 0; i < names.length; i++) {
|
|
91
|
-
var agent = agents[names[i]]
|
|
101
|
+
var agent = agents[names[i]];
|
|
102
|
+
if (!agent || typeof agent !== "object" || Array.isArray(agent)) {
|
|
103
|
+
throw new Error("OpenCode agent has malformed resolved configuration: " + names[i]);
|
|
104
|
+
}
|
|
92
105
|
if (agent.permission === undefined) continue;
|
|
93
106
|
if (!isSafeOpenCodePermission(agent.permission)) {
|
|
94
107
|
throw new Error("OpenCode agent has unsafe resolved permissions: " + names[i]);
|
|
@@ -181,6 +181,11 @@ function flattenEvent(notification, state) {
|
|
|
181
181
|
|
|
182
182
|
if (method === "turn/started") {
|
|
183
183
|
state.turnStarted = true;
|
|
184
|
+
state.currentTurnId = params.turnId || params.turn_id || (params.turn && params.turn.id) || null;
|
|
185
|
+
state.lastInputTokens = null;
|
|
186
|
+
state.lastCachedTokens = null;
|
|
187
|
+
state.lastOutputTokens = null;
|
|
188
|
+
state.currentContextUsage = null;
|
|
184
189
|
var userUuid = generateUuid();
|
|
185
190
|
events.push({ yokeType: "turn_start", uuid: userUuid, messageType: "user" });
|
|
186
191
|
return events;
|
|
@@ -194,16 +199,38 @@ function flattenEvent(notification, state) {
|
|
|
194
199
|
if (turnStatus === "interrupted" || state.aborted) {
|
|
195
200
|
events.push({ yokeType: "interrupted" });
|
|
196
201
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
202
|
+
// Generic usage reporting is independent from current context occupancy.
|
|
203
|
+
// The latter is supplied only by a valid thread/tokenUsage snapshot.
|
|
204
|
+
// Some normal protocol completions omit usage even though the preceding
|
|
205
|
+
// token snapshot carried useful per-turn accounting. Keep that fallback
|
|
206
|
+
// for the generic usage panel, but never use it as context occupancy.
|
|
207
|
+
var inputTokens = usage && typeof usage.input_tokens === "number"
|
|
208
|
+
? usage.input_tokens : (typeof state.lastInputTokens === "number" ? state.lastInputTokens : 0);
|
|
209
|
+
var outputTokens = usage && typeof usage.output_tokens === "number"
|
|
210
|
+
? usage.output_tokens : (typeof state.lastOutputTokens === "number" ? state.lastOutputTokens : 0);
|
|
211
|
+
var cachedTokens = usage && typeof usage.cached_input_tokens === "number"
|
|
212
|
+
? usage.cached_input_tokens : (typeof state.lastCachedTokens === "number" ? state.lastCachedTokens : 0);
|
|
201
213
|
var hasTokenData = inputTokens > 0 || outputTokens > 0;
|
|
202
214
|
var resultModelUsage = {};
|
|
203
215
|
// Codex reports the real window in thread/tokenUsage/updated. Dropping it
|
|
204
216
|
// made the client fall back to its model-name table, which has no entry
|
|
205
217
|
// for codex model ids and defaulted to Claude's 200k denominator.
|
|
206
|
-
|
|
218
|
+
var contextSnapshot = { valid: false };
|
|
219
|
+
if (state.currentContextUsage
|
|
220
|
+
&& typeof state.currentContextUsage.input_tokens === "number"
|
|
221
|
+
&& typeof state.currentContextUsage.contextWindow === "number"
|
|
222
|
+
&& state.currentContextUsage.contextWindow > 0) {
|
|
223
|
+
contextSnapshot = {
|
|
224
|
+
valid: true,
|
|
225
|
+
inputTokens: state.currentContextUsage.input_tokens,
|
|
226
|
+
contextWindow: state.currentContextUsage.contextWindow,
|
|
227
|
+
};
|
|
228
|
+
if (state.currentContextUsage.turnId) contextSnapshot.turnId = state.currentContextUsage.turnId;
|
|
229
|
+
}
|
|
230
|
+
resultModelUsage[state.model] = {
|
|
231
|
+
contextWindow: state.modelContextWindow || null,
|
|
232
|
+
contextSnapshot: contextSnapshot,
|
|
233
|
+
};
|
|
207
234
|
var assistantUuid = generateUuid();
|
|
208
235
|
events.push({
|
|
209
236
|
yokeType: "result",
|
|
@@ -683,24 +710,36 @@ function flattenEvent(notification, state) {
|
|
|
683
710
|
if (method === "thread/tokenUsage/updated") {
|
|
684
711
|
var tu = params.tokenUsage;
|
|
685
712
|
if (tu) {
|
|
713
|
+
var tokenTurnId = params.turnId || params.turn_id || tu.turnId || null;
|
|
714
|
+
if (tokenTurnId && state.currentTurnId && tokenTurnId !== state.currentTurnId) {
|
|
715
|
+
state.lastInputTokens = null;
|
|
716
|
+
state.lastCachedTokens = null;
|
|
717
|
+
state.lastOutputTokens = null;
|
|
718
|
+
state.currentContextUsage = null;
|
|
719
|
+
return events;
|
|
720
|
+
}
|
|
721
|
+
var currentWindow = null;
|
|
686
722
|
var turnUsage = tu.lastTurn;
|
|
687
723
|
if (turnUsage) {
|
|
688
724
|
var candidateInput = typeof turnUsage.inputTokens === "number" ? turnUsage.inputTokens : null;
|
|
689
|
-
|
|
690
|
-
|
|
725
|
+
currentWindow = typeof tu.modelContextWindow === "number" && isFinite(tu.modelContextWindow) && tu.modelContextWindow > 0
|
|
726
|
+
? tu.modelContextWindow : null;
|
|
727
|
+
state.lastInputTokens = candidateInput !== null && isFinite(candidateInput) && candidateInput >= 0 && currentWindow !== null && candidateInput <= currentWindow ? candidateInput : null;
|
|
691
728
|
state.lastCachedTokens = turnUsage.cachedInputTokens || 0;
|
|
692
729
|
state.lastOutputTokens = turnUsage.outputTokens || 0;
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
730
|
+
if (state.lastInputTokens !== null && currentWindow !== null) {
|
|
731
|
+
state.currentContextUsage = { input_tokens: state.lastInputTokens, contextWindow: currentWindow };
|
|
732
|
+
if (tokenTurnId || state.currentTurnId) state.currentContextUsage.turnId = tokenTurnId || state.currentTurnId;
|
|
733
|
+
} else {
|
|
734
|
+
state.currentContextUsage = null;
|
|
735
|
+
}
|
|
697
736
|
} else {
|
|
698
737
|
state.lastInputTokens = null;
|
|
699
738
|
state.lastCachedTokens = null;
|
|
700
739
|
state.lastOutputTokens = null;
|
|
701
740
|
state.currentContextUsage = null;
|
|
702
741
|
}
|
|
703
|
-
if (
|
|
742
|
+
if (currentWindow !== null) state.modelContextWindow = currentWindow;
|
|
704
743
|
}
|
|
705
744
|
return events;
|
|
706
745
|
}
|
|
@@ -750,6 +789,7 @@ function createEventState(model) {
|
|
|
750
789
|
lastCachedTokens: null,
|
|
751
790
|
lastOutputTokens: null,
|
|
752
791
|
currentContextUsage: null,
|
|
792
|
+
currentTurnId: null,
|
|
753
793
|
modelContextWindow: null,
|
|
754
794
|
done: false,
|
|
755
795
|
aborted: false,
|
|
@@ -1288,7 +1328,10 @@ function createCodexQueryHandle(appServer, queryOpts) {
|
|
|
1288
1328
|
},
|
|
1289
1329
|
|
|
1290
1330
|
getContextUsage: function() {
|
|
1291
|
-
return Promise.resolve(state.currentContextUsage
|
|
1331
|
+
return Promise.resolve(state.currentContextUsage || {
|
|
1332
|
+
unavailable: true,
|
|
1333
|
+
reason: "current_context_unavailable",
|
|
1334
|
+
});
|
|
1292
1335
|
},
|
|
1293
1336
|
|
|
1294
1337
|
abort: function() {
|
package/package.json
CHANGED