clay-server 4.1.0-beta.13 → 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/issues-mcp-server.js +1 -1
- package/lib/project-file-path.js +13 -0
- package/lib/project-file-watch.js +19 -8
- package/lib/project-filesystem.js +10 -16
- package/lib/project-http.js +19 -11
- package/lib/project-logs-mcp-server.js +8 -8
- package/lib/project.js +3 -2
- package/lib/public/app.js +15 -9
- package/lib/public/css/git-panel.css +13 -12
- package/lib/public/css/git-placard.css +4 -4
- 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/app-rate-limit.js +148 -61
- package/lib/public/modules/default-vendor.js +116 -0
- package/lib/public/modules/filebrowser-tabs.js +1 -0
- package/lib/public/modules/filebrowser.js +45 -18
- package/lib/public/modules/issues.js +4 -6
- package/lib/public/modules/project-logs.js +4 -9
- package/lib/public/modules/right-workbench.js +44 -0
- package/lib/public/modules/scheduled-tasks.js +4 -5
- package/lib/public/modules/sidebar-mobile.js +51 -3
- package/lib/public/modules/sidebar-sessions.js +65 -5
- package/lib/public/modules/sticky-notes-browser.js +4 -13
- package/lib/public/modules/terminal.js +4 -4
- package/lib/sdk-message-processor.js +11 -2
- package/lib/server-default-vendor.js +60 -0
- package/lib/server.js +4 -0
- package/lib/session-notes-mcp-server.js +2 -4
- 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/lib/yoke/vendor-registry.js +1 -1
- package/package.json +1 -1
|
@@ -14,6 +14,7 @@ import { renderMiniMarkdown, getTitle } from './sticky-note-markdown.js';
|
|
|
14
14
|
import { enhanceClayLogLinks } from './clay-log-links.js';
|
|
15
15
|
import { send } from './sticky-notes-shared.js';
|
|
16
16
|
import { listNoteData, focusNoteOnCanvas, hideNotes, showNotes } from './sticky-notes.js';
|
|
17
|
+
import { claimRightWorkbench, registerRightWorkbench, releaseRightWorkbench } from './right-workbench.js';
|
|
17
18
|
|
|
18
19
|
var panel = null;
|
|
19
20
|
var gridEl = null;
|
|
@@ -21,18 +22,6 @@ var tabsEl = null;
|
|
|
21
22
|
|
|
22
23
|
// Registered by app.js so this module never imports the other right-pane tools
|
|
23
24
|
// and cannot form an import cycle with them.
|
|
24
|
-
var exclusiveClosers = [];
|
|
25
|
-
|
|
26
|
-
export function registerExclusiveClosers(fns) {
|
|
27
|
-
exclusiveClosers = (fns || []).filter(function (fn) { return typeof fn === "function"; });
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function closeOtherRightTools() {
|
|
31
|
-
for (var i = 0; i < exclusiveClosers.length; i++) {
|
|
32
|
-
try { exclusiveClosers[i](); } catch (e) { /* a missing tool must not block opening */ }
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
25
|
function isClosed(data) {
|
|
37
26
|
if (!data) return false;
|
|
38
27
|
if (data.state === "closed") return true;
|
|
@@ -283,7 +272,7 @@ export function renderNotesBrowser() {
|
|
|
283
272
|
export function openNotesBrowser() {
|
|
284
273
|
ensurePanel();
|
|
285
274
|
if (!panel) return;
|
|
286
|
-
|
|
275
|
+
claimRightWorkbench("notes-browser");
|
|
287
276
|
// The floating canvas would otherwise sit on top of the pane.
|
|
288
277
|
hideNotes();
|
|
289
278
|
panel.classList.remove("hidden");
|
|
@@ -295,6 +284,7 @@ export function openNotesBrowser() {
|
|
|
295
284
|
}
|
|
296
285
|
|
|
297
286
|
export function closeNotesBrowser() {
|
|
287
|
+
releaseRightWorkbench("notes-browser");
|
|
298
288
|
if (!store.get('notesBrowserOpen')) return;
|
|
299
289
|
if (panel) panel.classList.add("hidden");
|
|
300
290
|
// Fullscreen is always dropped on close so the next open is a bounded pane.
|
|
@@ -311,6 +301,7 @@ export function isNotesBrowserOpen() {
|
|
|
311
301
|
}
|
|
312
302
|
|
|
313
303
|
export function initNotesBrowser() {
|
|
304
|
+
registerRightWorkbench("notes-browser", closeNotesBrowser);
|
|
314
305
|
document.addEventListener("keydown", function (e) {
|
|
315
306
|
if (e.key !== "Escape") return;
|
|
316
307
|
if (!store.get('notesBrowserOpen')) return;
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { iconHtml, refreshIcons } from './icons.js';
|
|
2
2
|
import { closeSidebar } from './sidebar.js';
|
|
3
|
-
import {
|
|
3
|
+
import { claimRightWorkbench, registerRightWorkbench, releaseRightWorkbench } from './right-workbench.js';
|
|
4
4
|
import { copyToClipboard } from './utils.js';
|
|
5
5
|
import { getTerminalTheme } from './theme.js';
|
|
6
6
|
import { getTerminalFontFamily, getTerminalFontSize, onTerminalFontChange } from './terminal-prefs.js';
|
|
7
7
|
import { createKeyToolbar } from './terminal-toolbar.js';
|
|
8
|
-
import { closeIssues } from './issues.js';
|
|
9
8
|
|
|
10
9
|
var ctx;
|
|
11
10
|
var tabs = new Map(); // termId -> { id, title, exited, xterm, fitAddon, bodyEl }
|
|
@@ -136,6 +135,7 @@ function createMultiLineLinkProvider(xterm) {
|
|
|
136
135
|
// --- Init ---
|
|
137
136
|
export function initTerminal(_ctx) {
|
|
138
137
|
ctx = _ctx;
|
|
138
|
+
registerRightWorkbench("terminal", closeTerminal);
|
|
139
139
|
|
|
140
140
|
// Close panel button
|
|
141
141
|
document.getElementById("terminal-close").addEventListener("click", function () {
|
|
@@ -193,8 +193,7 @@ export function openTerminal() {
|
|
|
193
193
|
var container = ctx.terminalContainerEl;
|
|
194
194
|
|
|
195
195
|
// Hide file viewer if open (also unwatches)
|
|
196
|
-
|
|
197
|
-
closeIssues();
|
|
196
|
+
claimRightWorkbench("terminal");
|
|
198
197
|
|
|
199
198
|
container.classList.remove("hidden");
|
|
200
199
|
isOpen = true;
|
|
@@ -224,6 +223,7 @@ export function openTerminal() {
|
|
|
224
223
|
|
|
225
224
|
// --- Close terminal panel (hide, detach, but keep PTYs alive) ---
|
|
226
225
|
export function closeTerminal() {
|
|
226
|
+
releaseRightWorkbench("terminal");
|
|
227
227
|
var container = ctx.terminalContainerEl;
|
|
228
228
|
container.classList.remove("panel-fullscreen");
|
|
229
229
|
container.classList.add("hidden");
|
|
@@ -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 });
|
|
@@ -778,10 +783,12 @@ function attachMessageProcessor(ctx) {
|
|
|
778
783
|
var info = parsed.rateLimitInfo;
|
|
779
784
|
console.log("[sdk-bridge] rate_limit_event for session " + session.localId + ": status=" + info.status + " resetsAt=" + info.resetsAt + " isUsingOverage=" + info.isUsingOverage + " isProcessing=" + session.isProcessing);
|
|
780
785
|
|
|
781
|
-
//
|
|
786
|
+
// Persist the scoped reset snapshot so reconnect/history restore can reproject it.
|
|
782
787
|
if (info.rateLimitType && info.resetsAt) {
|
|
783
|
-
|
|
788
|
+
sendAndRecord(session, {
|
|
784
789
|
type: "rate_limit_usage",
|
|
790
|
+
sessionId: session.localId,
|
|
791
|
+
vendor: session.vendor || "claude",
|
|
785
792
|
rateLimitType: info.rateLimitType,
|
|
786
793
|
resetsAt: info.resetsAt * 1000,
|
|
787
794
|
status: info.status,
|
|
@@ -792,6 +799,8 @@ function attachMessageProcessor(ctx) {
|
|
|
792
799
|
if (info.status === "allowed_warning" || info.status === "rejected") {
|
|
793
800
|
sendAndRecord(session, {
|
|
794
801
|
type: "rate_limit",
|
|
802
|
+
sessionId: session.localId,
|
|
803
|
+
vendor: session.vendor || "claude",
|
|
795
804
|
status: info.status,
|
|
796
805
|
resetsAt: info.resetsAt ? info.resetsAt * 1000 : null,
|
|
797
806
|
rateLimitType: info.rateLimitType || null,
|
|
@@ -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;
|
|
@@ -4,10 +4,8 @@ var buildShape = require("./session-spawn-mcp-server").buildShape;
|
|
|
4
4
|
|
|
5
5
|
var MEMORY_CONTRACT =
|
|
6
6
|
"The sticky-note board persists across sessions and is a user-facing artifact shared with people and Clay agents, not private agent scratch space. " +
|
|
7
|
-
"Default to not writing. Create a note proactively only when the user explicitly asks to remember or track something, or when all of these are true: it will remain useful after the current task and session, it is not already adequately recorded in the repository or another note, and the user would likely be glad to find it on the board a week later. " +
|
|
8
|
-
"
|
|
9
|
-
"Important exception for deferred defects: while doing code or technical work, actively create a sticky note when you discover a concrete defect, regression risk, security issue, or data-loss risk that is outside the current session goal and will remain unfixed when the turn ends. Do not wait for the user to ask. For a Project Driver session with Issues authority, search, reuse, or create the Issue first and put the primary details there: observable evidence, affected component, impact, and next action. Then make the sticky note only a concise title, one-line actionable cue, and the actual opaque issue: reference; do not duplicate the full defect details in the note. If you lack Issues authority, route through an eligible Project Driver; never invent an issue reference or drop the alert. Check active notes first when a duplicate is plausible. Do not create defect notes for speculation, general cleanup ideas, or problems you fixed in the current session. " +
|
|
10
|
-
"Important exception for deferred proposals: when you propose work (a fix, follow-up, improvement, or next step) and the user defers it rather than declining it (\"let's do that later\", \"next time\", \"after this\"), actively create a sticky note before the topic moves on. Do not wait to be asked. Capture what was proposed, why it mattered, and the agreed timing if any. Deferred agreements scroll out of chat history quickly and are hard to track; the board is where they survive. If the user declines the idea outright, do not write a note. " +
|
|
7
|
+
"Default to not writing. Create a note proactively only when the user explicitly asks to remember or track something, or when all of these are true: it will remain useful after the current task and session, it is not already adequately recorded in the repository, an Issue, or another note, and the user would likely be glad to find it on the board a week later. " +
|
|
8
|
+
"Use Sticky Notes for preferences, reminders, undeveloped ideas, or an explicit request to remember something; they are not mandatory duplicates of Project Issues. For concrete actionable bugs, improvements, or deferred implementation, an authorized Project Driver should use Project Issues. If you lack Issues authority, route the work through an eligible Project Driver. Check active notes first when a duplicate is plausible. " +
|
|
11
9
|
"Never create a note merely because work is important, lengthy, spans agents or restarts, or might help another agent. Do not record completed work, implementation details, test results, investigation logs, transient blockers, conversation summaries, or announcements of your own activity. When uncertain, do not write. " +
|
|
12
10
|
"Updates are visible too: update only when durable state materially changes. " +
|
|
13
11
|
"A note is an attention item, so finishing it means closing it, never erasing it: call close_note once the thing it is asking for is done, and the note leaves the active board while the record stays and can be reopened. Do not turn a note into a completion log instead of closing it. " +
|
|
@@ -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