clay-server 4.1.0-beta.6 → 4.1.0-beta.7
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/LICENSE +661 -21
- package/LICENSES/MIT-legacy.txt +21 -0
- package/LICENSES/README.md +7 -0
- package/README.md +2 -2
- package/bin/cli.js +0 -2
- package/lib/daemon.js +0 -2
- package/lib/default-ai-runtime.js +117 -0
- package/lib/git-cli.js +1 -0
- package/lib/issue-work-session.js +10 -0
- package/lib/issues-comments.js +40 -0
- package/lib/issues-commit-verification.js +30 -0
- package/lib/issues-mcp-server.js +27 -0
- package/lib/issues-schema.js +119 -0
- package/lib/issues-service.js +339 -0
- package/lib/issues-store.js +210 -0
- package/lib/model-entry.js +15 -0
- package/lib/project-capsule-turn.js +1 -0
- package/lib/project-connection.js +2 -0
- package/lib/project-delegated-session.js +1 -0
- package/lib/project-filesystem.js +10 -10
- package/lib/project-issue-launch.js +154 -0
- package/lib/project-issues.js +58 -0
- package/lib/project-logs-mcp-server.js +8 -12
- package/lib/project-message-delivery.js +4 -2
- package/lib/project-models.js +71 -3
- package/lib/project-pending-message-queue.js +264 -0
- package/lib/project-scheduled-tasks.js +2 -4
- package/lib/project-sessions.js +82 -31
- package/lib/project-tool-llm-runtime.js +48 -0
- package/lib/project-user-message.js +178 -42
- package/lib/project-workspace-query.js +25 -0
- package/lib/project.js +102 -18
- package/lib/public/app.js +38 -4
- package/lib/public/css/default-ai.css +33 -0
- package/lib/public/css/input.css +89 -5
- package/lib/public/css/issues.css +101 -0
- package/lib/public/css/messages.css +19 -0
- package/lib/public/css/pending-message-queue.css +187 -0
- package/lib/public/css/scheduled-tasks.css +3 -4
- package/lib/public/index.html +25 -17
- package/lib/public/modules/app-connection.js +3 -0
- package/lib/public/modules/app-cursors.js +29 -12
- package/lib/public/modules/app-message-router.js +8 -0
- package/lib/public/modules/app-messages.js +12 -3
- package/lib/public/modules/clay-file-links.js +111 -0
- package/lib/public/modules/clay-issue-links.js +131 -0
- package/lib/public/modules/clay-log-links.js +2 -0
- package/lib/public/modules/cursor-sharing-preference.js +38 -0
- package/lib/public/modules/default-ai.js +377 -0
- package/lib/public/modules/filebrowser.js +43 -2
- package/lib/public/modules/input.js +7 -17
- package/lib/public/modules/issues-context.js +117 -0
- package/lib/public/modules/issues-render.js +29 -0
- package/lib/public/modules/issues.js +270 -0
- package/lib/public/modules/markdown.js +4 -0
- package/lib/public/modules/message-delivery.js +5 -3
- package/lib/public/modules/pending-message-image-editor.js +143 -0
- package/lib/public/modules/pending-message-queue-model.js +22 -0
- package/lib/public/modules/pending-message-queue.js +449 -0
- package/lib/public/modules/scheduled-task-engine.js +0 -50
- package/lib/public/modules/scheduled-tasks.js +7 -16
- package/lib/public/modules/sidebar-mobile.js +3 -0
- package/lib/public/modules/sticky-note-markdown.js +1 -0
- package/lib/public/modules/terminal.js +2 -0
- package/lib/public/modules/tool-palette-order.js +1 -0
- package/lib/public/style.css +2 -0
- package/lib/sdk-bridge.js +34 -11
- package/lib/server-cursor-sharing.js +65 -0
- package/lib/server-default-ai.js +265 -0
- package/lib/server-home-chat.js +29 -9
- package/lib/server-home-clay-entry.js +4 -1
- package/lib/server-home-debate-planning.js +32 -14
- package/lib/server-home-mate-creation.js +30 -11
- package/lib/server-home-models.js +14 -6
- package/lib/server-issue-links.js +18 -0
- package/lib/server.js +41 -0
- package/lib/session-notes-mcp-server.js +1 -1
- package/lib/session-search.js +4 -1
- package/lib/tool-llm.js +1 -0
- package/lib/users-default-ai-preferences.js +80 -0
- package/lib/users-preferences.js +35 -0
- package/lib/users.js +10 -0
- package/lib/workspace-query-projection.js +29 -0
- package/lib/workspace-query-service.js +19 -29
- package/lib/ws-schema.js +36 -1
- package/lib/yoke/adapters/claude-worker.js +4 -2
- package/lib/yoke/adapters/claude.js +5 -1
- package/lib/yoke/package.json +1 -1
- package/package.json +5 -6
- package/bin/claude-relay.js +0 -6
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Shared local-file link rendering and activation for every Markdown surface.
|
|
2
|
+
|
|
3
|
+
import { escapeHtml } from './utils.js';
|
|
4
|
+
import { refreshIcons } from './icons.js';
|
|
5
|
+
import { store } from './store.js';
|
|
6
|
+
|
|
7
|
+
var openFileHandler = null;
|
|
8
|
+
var delegatedBinding = false;
|
|
9
|
+
|
|
10
|
+
function decode(value) {
|
|
11
|
+
try { return decodeURIComponent(value); } catch (e) { return value; }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function splitLocation(value) {
|
|
15
|
+
var raw = String(value || "");
|
|
16
|
+
var fragment = "";
|
|
17
|
+
var hash = raw.indexOf("#");
|
|
18
|
+
if (hash !== -1) {
|
|
19
|
+
fragment = decode(raw.slice(hash + 1));
|
|
20
|
+
raw = raw.slice(0, hash);
|
|
21
|
+
}
|
|
22
|
+
var decoded = decode(raw);
|
|
23
|
+
var line = null;
|
|
24
|
+
var column = null;
|
|
25
|
+
var lineMatch = /^L(\d+)(?:-L?(\d+))?(?:C(\d+))?$/i.exec(fragment);
|
|
26
|
+
if (lineMatch) {
|
|
27
|
+
line = Number(lineMatch[1]);
|
|
28
|
+
column = lineMatch[3] ? Number(lineMatch[3]) : null;
|
|
29
|
+
}
|
|
30
|
+
var suffixMatch = /^(.*?):(\d+)(?::(\d+))?$/.exec(decoded);
|
|
31
|
+
if (suffixMatch && !/^[A-Za-z]:$/.test(suffixMatch[1])) {
|
|
32
|
+
decoded = suffixMatch[1];
|
|
33
|
+
line = Number(suffixMatch[2]);
|
|
34
|
+
column = suffixMatch[3] ? Number(suffixMatch[3]) : column;
|
|
35
|
+
}
|
|
36
|
+
return { path: decoded, line: line, column: column };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function localFileTarget(href) {
|
|
40
|
+
if (typeof href !== "string" || !href || href.charAt(0) === "#") return null;
|
|
41
|
+
var raw = href;
|
|
42
|
+
var pathValue = raw;
|
|
43
|
+
if (/^(?:https?|mailto|tel|data|javascript|log|session|issue):/i.test(raw)) return null;
|
|
44
|
+
if (/^file:\/\//i.test(raw)) {
|
|
45
|
+
try {
|
|
46
|
+
var url = new URL(raw);
|
|
47
|
+
if (url.hostname && url.hostname !== "localhost") return null;
|
|
48
|
+
pathValue = url.pathname + (url.hash || "");
|
|
49
|
+
if (/^\/[A-Za-z]:/.test(pathValue)) pathValue = pathValue.slice(1);
|
|
50
|
+
} catch (e) { return null; }
|
|
51
|
+
}
|
|
52
|
+
var target = splitLocation(pathValue);
|
|
53
|
+
if (!target.path) return null;
|
|
54
|
+
if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(target.path) || target.path.indexOf("//") === 0) return null;
|
|
55
|
+
target.href = href;
|
|
56
|
+
return target;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function renderLocalFileLink(href, title, text) {
|
|
60
|
+
var target = localFileTarget(href);
|
|
61
|
+
if (!target) return null;
|
|
62
|
+
var label = text || target.path;
|
|
63
|
+
var tooltip = target.path + (target.line ? ":" + target.line + (target.column ? ":" + target.column : "") : "");
|
|
64
|
+
var state = store.snap();
|
|
65
|
+
var projectAttr = state.currentSlug ? ' data-file-project-slug="' + escapeHtml(String(state.currentSlug)) + '"' : '';
|
|
66
|
+
var sessionAttr = state.activeSessionId != null ? ' data-file-session-id="' + escapeHtml(String(state.activeSessionId)) + '"' : '';
|
|
67
|
+
return '<button type="button" class="clay-file-link" data-file-path="' + escapeHtml(target.path) + '"' +
|
|
68
|
+
projectAttr + sessionAttr +
|
|
69
|
+
(target.line ? ' data-file-line="' + target.line + '"' : '') +
|
|
70
|
+
(target.column ? ' data-file-column="' + target.column + '"' : '') +
|
|
71
|
+
' title="' + escapeHtml(tooltip) + '" aria-label="Open file ' + escapeHtml(tooltip) + '">' +
|
|
72
|
+
'<span class="clay-file-link-icon" data-lucide="file-code-2" aria-hidden="true"></span>' +
|
|
73
|
+
'<span class="clay-file-link-label">' + label + '</span></button>';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function registerClayFileLinkOpener(handler) {
|
|
77
|
+
openFileHandler = handler;
|
|
78
|
+
bindDelegatedHandler();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function showUnavailable(button) {
|
|
82
|
+
var notice = document.createElement("div");
|
|
83
|
+
notice.className = "clay-file-link-notice";
|
|
84
|
+
notice.setAttribute("role", "status");
|
|
85
|
+
notice.textContent = "File browser is unavailable for " + (button.dataset.filePath || "this file") + ".";
|
|
86
|
+
button.parentElement.appendChild(notice);
|
|
87
|
+
setTimeout(function () { if (notice.parentNode) notice.parentNode.removeChild(notice); }, 5000);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function enhanceClayFileLinks(root) {
|
|
91
|
+
if (!root || typeof document === "undefined") return;
|
|
92
|
+
bindDelegatedHandler();
|
|
93
|
+
refreshIcons();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function bindDelegatedHandler() {
|
|
97
|
+
if (delegatedBinding || typeof document === "undefined") return;
|
|
98
|
+
delegatedBinding = true;
|
|
99
|
+
document.addEventListener("click", function (event) {
|
|
100
|
+
var button = event.target && event.target.closest ? event.target.closest(".clay-file-link") : null;
|
|
101
|
+
if (!button) return;
|
|
102
|
+
if (!openFileHandler) { showUnavailable(button); return; }
|
|
103
|
+
var result = openFileHandler(button.dataset.filePath, {
|
|
104
|
+
line: button.dataset.fileLine ? Number(button.dataset.fileLine) : null,
|
|
105
|
+
column: button.dataset.fileColumn ? Number(button.dataset.fileColumn) : null,
|
|
106
|
+
projectSlug: button.dataset.fileProjectSlug || null,
|
|
107
|
+
sessionId: button.dataset.fileSessionId || null
|
|
108
|
+
});
|
|
109
|
+
if (result === false) showUnavailable(button);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { getWs } from './ws-ref.js';
|
|
3
|
+
function accountKey(state) { return state.isMultiUserMode ? 'user:' + String(state.myUserId || '') : 'single'; }
|
|
4
|
+
function requestContext(state) { return { accountKey: accountKey(state), projectSlug: state.currentSlug || null, epoch: state.issuesEpoch || 0 }; }
|
|
5
|
+
function sameContext(request, state) {
|
|
6
|
+
var current = requestContext(state);
|
|
7
|
+
return !!request && request.accountKey === current.accountKey && request.projectSlug === current.projectSlug && request.epoch === current.epoch;
|
|
8
|
+
}
|
|
9
|
+
export function parseClayIssueReferences(text) {
|
|
10
|
+
var pattern = /\[clayos\/(issue:[A-Za-z0-9_-]{24})(?:\s+[\u2014-]\s+([^\]\n]{1,80}))?\]|(^|[^A-Za-z0-9_/:])(issue:[A-Za-z0-9_-]{24})(?![A-Za-z0-9_-])/g;
|
|
11
|
+
var results = [];
|
|
12
|
+
var match;
|
|
13
|
+
while ((match = pattern.exec(text || ""))) {
|
|
14
|
+
results.push({
|
|
15
|
+
start: match.index,
|
|
16
|
+
end: pattern.lastIndex,
|
|
17
|
+
prefix: match[3] || "",
|
|
18
|
+
ref: match[1] || match[4],
|
|
19
|
+
label: match[2] ? match[2].trim() : "",
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return results;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isExactClayIssueReference(text) {
|
|
26
|
+
return /^issue:[A-Za-z0-9_-]{24}$/.test((text || "").trim());
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function createIssueLink(ref, labelText) {
|
|
30
|
+
var button = document.createElement("button");
|
|
31
|
+
button.type = "button";
|
|
32
|
+
button.className = "clayos-issue-link";
|
|
33
|
+
button.contentEditable = "false";
|
|
34
|
+
button.dataset.issueRef = ref;
|
|
35
|
+
button.setAttribute("aria-label", "Open Project Issue" + (labelText ? " " + labelText : ""));
|
|
36
|
+
var label = document.createElement("span");
|
|
37
|
+
label.textContent = "Issue";
|
|
38
|
+
button.appendChild(label);
|
|
39
|
+
if (labelText) {
|
|
40
|
+
var meta = document.createElement("small");
|
|
41
|
+
meta.textContent = labelText;
|
|
42
|
+
button.appendChild(meta);
|
|
43
|
+
}
|
|
44
|
+
var cache = store.get('issuesCache') || {};
|
|
45
|
+
if (cache[ref]) { button.textContent = cache[ref].status.replace(/_/g, ' ') + ' · ' + cache[ref].title; button.dataset.status = cache[ref].status; }
|
|
46
|
+
setTimeout(function () { resolve(ref); }, 0);
|
|
47
|
+
return button;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function enhanceClayIssueLinks(root) {
|
|
51
|
+
var codeNodes = root.querySelectorAll("code");
|
|
52
|
+
for (var codeIndex = 0; codeIndex < codeNodes.length; codeIndex++) {
|
|
53
|
+
var code = codeNodes[codeIndex];
|
|
54
|
+
var codeText = (code.textContent || "").trim();
|
|
55
|
+
if (code.closest("pre") || !isExactClayIssueReference(codeText)) continue;
|
|
56
|
+
code.parentNode.replaceChild(createIssueLink(codeText, ""), code);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
var walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
60
|
+
var nodes = [];
|
|
61
|
+
var node;
|
|
62
|
+
while ((node = walker.nextNode())) {
|
|
63
|
+
if (!node.parentElement || node.parentElement.closest("code, pre, a, button")) continue;
|
|
64
|
+
if (parseClayIssueReferences(node.nodeValue).length) nodes.push(node);
|
|
65
|
+
}
|
|
66
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
67
|
+
var text = nodes[i].nodeValue || "";
|
|
68
|
+
var matches = parseClayIssueReferences(text);
|
|
69
|
+
var fragment = document.createDocumentFragment();
|
|
70
|
+
var offset = 0;
|
|
71
|
+
for (var j = 0; j < matches.length; j++) {
|
|
72
|
+
var match = matches[j];
|
|
73
|
+
fragment.appendChild(document.createTextNode(text.slice(offset, match.start)));
|
|
74
|
+
if (match.prefix) fragment.appendChild(document.createTextNode(match.prefix));
|
|
75
|
+
fragment.appendChild(createIssueLink(match.ref, match.label));
|
|
76
|
+
offset = match.end;
|
|
77
|
+
}
|
|
78
|
+
fragment.appendChild(document.createTextNode(text.slice(offset)));
|
|
79
|
+
nodes[i].parentNode.replaceChild(fragment, nodes[i]);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function resolve(ref) {
|
|
84
|
+
var ws = getWs();
|
|
85
|
+
var requests = store.get('issueLinkRequests') || {};
|
|
86
|
+
if (!ws || ws.readyState !== 1 || requests[ref]) return;
|
|
87
|
+
var next = Object.assign({}, requests);
|
|
88
|
+
var requestId = crypto.randomUUID();
|
|
89
|
+
next[ref] = Object.assign({ requestId: requestId }, requestContext(store.snap()));
|
|
90
|
+
store.set({ issueLinkRequests: next });
|
|
91
|
+
ws.send(JSON.stringify({ type: 'issue_reference_resolve', ref: ref, requestId: requestId }));
|
|
92
|
+
}
|
|
93
|
+
export function handleIssueReference(msg) {
|
|
94
|
+
if (msg.type !== 'issue_reference_result') return false;
|
|
95
|
+
var requests = store.get('issueLinkRequests') || {};
|
|
96
|
+
var request = requests[msg.ref];
|
|
97
|
+
if (!request || request.requestId !== msg.requestId || !sameContext(request, store.snap())) return true;
|
|
98
|
+
var next = Object.assign({}, requests); delete next[msg.ref]; store.set({ issueLinkRequests: next });
|
|
99
|
+
if (msg.target) {
|
|
100
|
+
var cache = Object.assign({}, store.get('issuesCache') || {}); cache[msg.ref] = msg.target; store.set({ issuesCache: cache });
|
|
101
|
+
}
|
|
102
|
+
document.querySelectorAll('[data-issue-ref]').forEach(function (button) {
|
|
103
|
+
if (button.dataset.issueRef !== msg.ref) return;
|
|
104
|
+
if (msg.target) { button.textContent = msg.target.status.replace(/_/g, ' ') + ' · ' + msg.target.title; button.dataset.status = msg.target.status; }
|
|
105
|
+
else button.title = msg.error || 'Issue unavailable';
|
|
106
|
+
});
|
|
107
|
+
if (store.get('issueLinkOpenRef') === msg.ref) {
|
|
108
|
+
store.set({ issueLinkOpenRef: null });
|
|
109
|
+
if (msg.target) navigate(msg.target);
|
|
110
|
+
}
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
function navigate(target) {
|
|
114
|
+
if (target.projectSlug !== store.get('currentSlug')) { window.location.assign('/p/' + encodeURIComponent(target.projectSlug) + '/?issue=' + encodeURIComponent(target.ref)); return; }
|
|
115
|
+
import('./issues.js').then(function (module) { module.openIssues(target.ref); });
|
|
116
|
+
}
|
|
117
|
+
export function initIssueLinks() {
|
|
118
|
+
document.addEventListener('click', function (event) {
|
|
119
|
+
var button = event.target.closest('[data-issue-ref]');
|
|
120
|
+
if (!button) return;
|
|
121
|
+
event.preventDefault(); event.stopPropagation();
|
|
122
|
+
var ref = button.dataset.issueRef;
|
|
123
|
+
var target = (store.get('issuesCache') || {})[ref];
|
|
124
|
+
if (!target) { store.set({ issueLinkOpenRef: ref }); resolve(ref); return; }
|
|
125
|
+
navigate(target);
|
|
126
|
+
});
|
|
127
|
+
store.subscribe(function (state, previous) {
|
|
128
|
+
if (state.currentSlug !== previous.currentSlug || state.myUserId !== previous.myUserId || state.isMultiUserMode !== previous.isMultiUserMode ||
|
|
129
|
+
(previous.connected && !state.connected)) store.set({ issueLinkRequests: {}, issueLinkOpenRef: null, issuesCache: {} });
|
|
130
|
+
});
|
|
131
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { enhanceClayIssueLinks } from './clay-issue-links.js';
|
|
1
2
|
export function parseClayLogReferences(text) {
|
|
2
3
|
var pattern = /\[clayos\/(log:[A-Za-z0-9_-]{24})(?:\s+[\u2014-]\s+([^\]\n]{1,80}))?\]|(^|[^A-Za-z0-9_/:])(log:[A-Za-z0-9_-]{24})(?![A-Za-z0-9_-])/g;
|
|
3
4
|
var results = [];
|
|
@@ -37,6 +38,7 @@ function createLogLink(ref, labelText) {
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
export function enhanceClayLogLinks(root) {
|
|
41
|
+
enhanceClayIssueLinks(root);
|
|
40
42
|
var codeNodes = root.querySelectorAll("code");
|
|
41
43
|
for (var codeIndex = 0; codeIndex < codeNodes.length; codeIndex++) {
|
|
42
44
|
var code = codeNodes[codeIndex];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Client state and server synchronization for the cursor sharing preference.
|
|
2
|
+
|
|
3
|
+
import { store } from './store.js';
|
|
4
|
+
import { getWs } from './ws-ref.js';
|
|
5
|
+
import { showToast } from './utils.js';
|
|
6
|
+
|
|
7
|
+
export function handleCursorSharingMessage(msg) {
|
|
8
|
+
if (!msg || msg.type !== "cursor_sharing_state") return false;
|
|
9
|
+
function clearPresence() {
|
|
10
|
+
var ws = getWs();
|
|
11
|
+
if (ws && ws.readyState === 1) {
|
|
12
|
+
ws.send(JSON.stringify({ type: "cursor_leave" }));
|
|
13
|
+
ws.send(JSON.stringify({ type: "text_select", ranges: [] }));
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (msg.ready !== true || msg.accountAvailable === false || typeof msg.cursorSharing !== "boolean") {
|
|
17
|
+
if (store.get("cursorSharingHydrated") && store.get("cursorSharingEnabled")) clearPresence();
|
|
18
|
+
store.set({ cursorSharingEnabled: false, cursorSharingHydrated: false, cursorSharingAccountId: null });
|
|
19
|
+
showToast(msg.error || "Cursor sharing is unavailable.", "warn");
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
var knownUserId = store.get("myUserId");
|
|
23
|
+
if (knownUserId && msg.accountId && knownUserId !== msg.accountId) {
|
|
24
|
+
if (store.get("cursorSharingHydrated") && store.get("cursorSharingEnabled")) clearPresence();
|
|
25
|
+
store.set({ cursorSharingEnabled: false, cursorSharingHydrated: false, cursorSharingAccountId: null });
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
var wasEnabled = store.get("cursorSharingHydrated") && store.get("cursorSharingEnabled");
|
|
29
|
+
store.set({ cursorSharingEnabled: msg.cursorSharing, cursorSharingHydrated: true, cursorSharingAccountId: msg.accountId || null });
|
|
30
|
+
if (wasEnabled && !msg.cursorSharing) {
|
|
31
|
+
var ws = getWs();
|
|
32
|
+
if (ws && ws.readyState === 1) {
|
|
33
|
+
ws.send(JSON.stringify({ type: "cursor_leave" }));
|
|
34
|
+
ws.send(JSON.stringify({ type: "text_select", ranges: [] }));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
// User-island Default AI picker. Canonical state and the unsaved draft are separate.
|
|
2
|
+
|
|
3
|
+
import { store } from './store.js';
|
|
4
|
+
import { getWs } from './ws-ref.js';
|
|
5
|
+
import { VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
|
|
6
|
+
import { refreshIcons } from './icons.js';
|
|
7
|
+
|
|
8
|
+
var sequence = 0;
|
|
9
|
+
var opener = null;
|
|
10
|
+
var popover = null;
|
|
11
|
+
|
|
12
|
+
function nextRequestId() {
|
|
13
|
+
sequence += 1;
|
|
14
|
+
return "default-ai-" + Date.now() + "-" + sequence;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function send(payload) {
|
|
18
|
+
var ws = getWs();
|
|
19
|
+
if (!ws || ws.readyState !== 1) return false;
|
|
20
|
+
ws.send(JSON.stringify(payload));
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function entryValue(entry) {
|
|
25
|
+
return typeof entry === "string" ? entry : entry && (entry.value || entry.id) || "";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function entryLabel(entry) {
|
|
29
|
+
return typeof entry === "string" ? entry : entry && (entry.displayName || entry.name || entryValue(entry)) || "";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function addOption(select, value, text, disabled) {
|
|
33
|
+
var option = document.createElement("option");
|
|
34
|
+
option.value = value;
|
|
35
|
+
option.textContent = text;
|
|
36
|
+
option.disabled = disabled === true;
|
|
37
|
+
select.appendChild(option);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function close() {
|
|
41
|
+
if (!popover) return;
|
|
42
|
+
popover.classList.add("hidden");
|
|
43
|
+
if (opener) {
|
|
44
|
+
opener.setAttribute("aria-expanded", "false");
|
|
45
|
+
opener.focus({ preventScroll: true });
|
|
46
|
+
}
|
|
47
|
+
opener = null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function position() {
|
|
51
|
+
if (!popover || !opener) return;
|
|
52
|
+
var rect = opener.getBoundingClientRect();
|
|
53
|
+
var width = Math.min(390, Math.max(0, window.innerWidth - 24));
|
|
54
|
+
popover.style.width = width + "px";
|
|
55
|
+
popover.style.left = Math.max(12, Math.min(window.innerWidth - width - 12, rect.right - width)) + "px";
|
|
56
|
+
var desiredTop = rect.top - popover.offsetHeight - 10;
|
|
57
|
+
var maximumTop = Math.max(12, window.innerHeight - popover.offsetHeight - 12);
|
|
58
|
+
popover.style.top = Math.max(12, Math.min(maximumTop, desiredTop)) + "px";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function requestCatalog(vendor) {
|
|
62
|
+
if (!vendor) return;
|
|
63
|
+
var state = store.get("defaultAiState") || {};
|
|
64
|
+
var id = nextRequestId();
|
|
65
|
+
var catalogs = Object.assign({}, state.catalogs || {});
|
|
66
|
+
var requests = Object.assign({}, state.catalogRequestIds || {});
|
|
67
|
+
catalogs[vendor] = Object.assign({}, catalogs[vendor] || {}, { loading: true });
|
|
68
|
+
requests[vendor] = id;
|
|
69
|
+
store.set({ defaultAiState: Object.assign({}, state, { catalogs: catalogs, catalogRequestIds: requests, error: "" }) });
|
|
70
|
+
if (!send({ type: "default_ai_catalog_get", requestId: id, vendor: vendor })) {
|
|
71
|
+
store.set({ defaultAiState: Object.assign({}, store.get("defaultAiState") || {}, { error: "Clay is offline. Reconnect and try again." }) });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function currentEfforts(models, modelValue, catalog) {
|
|
76
|
+
for (var i = 0; i < models.length; i++) {
|
|
77
|
+
if (entryValue(models[i]) !== modelValue) continue;
|
|
78
|
+
if (models[i] && Array.isArray(models[i].supportedEffortLevels)) return models[i].supportedEffortLevels;
|
|
79
|
+
}
|
|
80
|
+
return catalog && Array.isArray(catalog.effortLevels) ? catalog.effortLevels : [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function addStatus(text, error) {
|
|
84
|
+
var status = document.createElement("div");
|
|
85
|
+
status.className = "default-ai-status" + (error ? " default-ai-error" : "");
|
|
86
|
+
if (error) status.setAttribute("role", "alert");
|
|
87
|
+
status.textContent = text;
|
|
88
|
+
popover.appendChild(status);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function expectedAccountId() {
|
|
92
|
+
if (store.get("isMultiUserMode")) return store.get("myUserId") || "";
|
|
93
|
+
return "default";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function addFieldLabel(text, control) {
|
|
97
|
+
var label = document.createElement("label");
|
|
98
|
+
label.className = "default-ai-label";
|
|
99
|
+
label.textContent = text;
|
|
100
|
+
label.appendChild(control);
|
|
101
|
+
popover.appendChild(label);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function render() {
|
|
105
|
+
if (!popover) return;
|
|
106
|
+
var state = store.get("defaultAiState") || {};
|
|
107
|
+
var draft = store.get("defaultAiDraft") || {};
|
|
108
|
+
var vendors = state.installedVendors || [];
|
|
109
|
+
var catalog = (state.catalogs || {})[draft.vendor] || null;
|
|
110
|
+
var models = catalog && catalog.models || [];
|
|
111
|
+
|
|
112
|
+
popover.innerHTML = '<div class="default-ai-title">Default AI</div><div class="default-ai-copy">Used for new Clay interviews and built-in AI assistance.</div>';
|
|
113
|
+
if (state.loading && !vendors.length) {
|
|
114
|
+
addStatus("Loading available AI providers…", false);
|
|
115
|
+
position();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (state.accountAvailable === false) {
|
|
119
|
+
addStatus(state.error || "Your account is no longer available.", true);
|
|
120
|
+
position();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
var vendor = document.createElement("select");
|
|
125
|
+
vendor.className = "default-ai-select";
|
|
126
|
+
vendor.setAttribute("aria-label", "Default AI vendor");
|
|
127
|
+
for (var vi = 0; vi < vendors.length; vi++) addOption(vendor, vendors[vi], VENDOR_NAMES[vendors[vi]] || vendors[vi], false);
|
|
128
|
+
if (draft.vendor && vendors.indexOf(draft.vendor) === -1) addOption(vendor, draft.vendor, "Unavailable: " + (VENDOR_NAMES[draft.vendor] || draft.vendor), true);
|
|
129
|
+
vendor.value = draft.vendor || vendors[0] || "";
|
|
130
|
+
vendor.disabled = !vendors.length || state.saving === true;
|
|
131
|
+
|
|
132
|
+
var model = document.createElement("select");
|
|
133
|
+
model.className = "default-ai-select";
|
|
134
|
+
model.setAttribute("aria-label", "Default AI model");
|
|
135
|
+
for (var mi = 0; mi < models.length; mi++) addOption(model, entryValue(models[mi]), entryLabel(models[mi]), false);
|
|
136
|
+
if (draft.model && !models.some(function (entry) { return entryValue(entry) === draft.model; })) addOption(model, draft.model, "Unavailable: " + draft.model, true);
|
|
137
|
+
if (!models.length) addOption(model, "", catalog && catalog.loading ? "Loading models…" : "Models unavailable", true);
|
|
138
|
+
model.value = draft.model || "";
|
|
139
|
+
model.disabled = !models.length || state.saving === true;
|
|
140
|
+
|
|
141
|
+
var effort = document.createElement("select");
|
|
142
|
+
effort.className = "default-ai-select";
|
|
143
|
+
effort.setAttribute("aria-label", "Default AI reasoning effort");
|
|
144
|
+
addOption(effort, "", "Provider default", false);
|
|
145
|
+
var efforts = currentEfforts(models, model.value, catalog);
|
|
146
|
+
for (var ei = 0; ei < efforts.length; ei++) addOption(effort, entryValue(efforts[ei]), entryLabel(efforts[ei]), false);
|
|
147
|
+
if (draft.effort && !efforts.some(function (entry) { return entryValue(entry) === draft.effort; })) addOption(effort, draft.effort, "Unavailable: " + draft.effort, true);
|
|
148
|
+
effort.value = draft.effort || "";
|
|
149
|
+
effort.disabled = !models.length || state.saving === true;
|
|
150
|
+
|
|
151
|
+
vendor.addEventListener("change", function () {
|
|
152
|
+
store.set({ defaultAiDraft: { vendor: vendor.value, model: "", effort: "" }, defaultAiDraftDirty: true });
|
|
153
|
+
requestCatalog(vendor.value);
|
|
154
|
+
render();
|
|
155
|
+
});
|
|
156
|
+
model.addEventListener("change", function () {
|
|
157
|
+
store.set({ defaultAiDraft: { vendor: vendor.value, model: model.value, effort: "" }, defaultAiDraftDirty: true });
|
|
158
|
+
render();
|
|
159
|
+
});
|
|
160
|
+
effort.addEventListener("change", function () {
|
|
161
|
+
store.set({ defaultAiDraft: { vendor: vendor.value, model: model.value, effort: effort.value }, defaultAiDraftDirty: true });
|
|
162
|
+
render();
|
|
163
|
+
});
|
|
164
|
+
addFieldLabel("Provider", vendor);
|
|
165
|
+
addFieldLabel("Model", model);
|
|
166
|
+
addFieldLabel("Reasoning effort", effort);
|
|
167
|
+
|
|
168
|
+
var selectedModel = models.some(function (entry) { return entryValue(entry) === model.value; });
|
|
169
|
+
var selectedEffort = !effort.value || efforts.some(function (entry) { return entryValue(entry) === effort.value; });
|
|
170
|
+
var save = document.createElement("button");
|
|
171
|
+
save.type = "button";
|
|
172
|
+
save.className = "default-ai-save";
|
|
173
|
+
save.textContent = state.saving ? "Saving…" : "Save default";
|
|
174
|
+
save.disabled = state.saving === true || vendors.indexOf(vendor.value) === -1 || !selectedModel || !selectedEffort;
|
|
175
|
+
save.addEventListener("click", function () {
|
|
176
|
+
var id = nextRequestId();
|
|
177
|
+
var latest = store.get("defaultAiState") || {};
|
|
178
|
+
store.set({ defaultAiState: Object.assign({}, latest, { saving: true, saveRequestId: id, error: "" }) });
|
|
179
|
+
if (!send({ type: "default_ai_set", requestId: id, vendor: vendor.value, model: model.value, effort: effort.value })) {
|
|
180
|
+
store.set({ defaultAiState: Object.assign({}, store.get("defaultAiState") || {}, { saving: false, saveRequestId: null, error: "Clay is offline. Reconnect and try again." }) });
|
|
181
|
+
}
|
|
182
|
+
render();
|
|
183
|
+
});
|
|
184
|
+
popover.appendChild(save);
|
|
185
|
+
if (state.error) addStatus(state.error, true);
|
|
186
|
+
position();
|
|
187
|
+
refreshIcons();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function requestDefaultAi() {
|
|
191
|
+
var state = store.get("defaultAiState") || {};
|
|
192
|
+
var id = nextRequestId();
|
|
193
|
+
store.set({ defaultAiState: Object.assign({}, state, { loading: true, refreshRequestId: id, accountId: expectedAccountId(), error: "" }) });
|
|
194
|
+
if (!send({ type: "default_ai_get", requestId: id })) {
|
|
195
|
+
store.set({ defaultAiState: Object.assign({}, store.get("defaultAiState") || {}, { loading: false, refreshRequestId: null, error: "Clay is offline. Reconnect and try again." }) });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function beginDefaultAiConnection() {
|
|
200
|
+
var state = store.get("defaultAiState") || {};
|
|
201
|
+
var dirty = store.get("defaultAiDraftDirty") === true;
|
|
202
|
+
var update = {
|
|
203
|
+
defaultAiState: Object.assign({}, state, {
|
|
204
|
+
loading: false,
|
|
205
|
+
saving: false,
|
|
206
|
+
refreshRequestId: null,
|
|
207
|
+
saveRequestId: null,
|
|
208
|
+
catalogRequestIds: {},
|
|
209
|
+
catalogs: {},
|
|
210
|
+
preference: null,
|
|
211
|
+
selection: null,
|
|
212
|
+
installedVendors: [],
|
|
213
|
+
accountAvailable: !!expectedAccountId(),
|
|
214
|
+
accountId: expectedAccountId(),
|
|
215
|
+
serverEpoch: null,
|
|
216
|
+
canonicalRevision: 0,
|
|
217
|
+
error: "",
|
|
218
|
+
}),
|
|
219
|
+
};
|
|
220
|
+
if (!dirty) update.defaultAiDraft = { vendor: "", model: "", effort: "" };
|
|
221
|
+
store.set(update);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function applyCatalogMessage(msg, state) {
|
|
225
|
+
var requests = Object.assign({}, state.catalogRequestIds || {});
|
|
226
|
+
if (!msg.vendor || requests[msg.vendor] !== msg.requestId) return;
|
|
227
|
+
if (typeof msg.canonicalRevision === "number" && msg.canonicalRevision < (state.canonicalRevision || 0)) return;
|
|
228
|
+
delete requests[msg.vendor];
|
|
229
|
+
var catalogs = Object.assign({}, state.catalogs || {});
|
|
230
|
+
catalogs[msg.vendor] = Object.assign({}, msg.catalog || {}, { loading: false });
|
|
231
|
+
var next = Object.assign({}, state, {
|
|
232
|
+
catalogRequestIds: requests,
|
|
233
|
+
catalogs: catalogs,
|
|
234
|
+
installedVendors: msg.installedVendors || state.installedVendors || [],
|
|
235
|
+
accountAvailable: msg.accountAvailable !== false,
|
|
236
|
+
accountId: msg.accountId || state.accountId || null,
|
|
237
|
+
serverEpoch: msg.serverEpoch || state.serverEpoch || null,
|
|
238
|
+
canonicalRevision: typeof msg.canonicalRevision === "number" ? msg.canonicalRevision : state.canonicalRevision || 0,
|
|
239
|
+
error: msg.error || "",
|
|
240
|
+
});
|
|
241
|
+
var draft = store.get("defaultAiDraft") || {};
|
|
242
|
+
if (draft.vendor === msg.vendor && !draft.model && msg.selection && msg.selection.model) {
|
|
243
|
+
draft = Object.assign({}, draft, { model: msg.selection.model, effort: msg.selection.effort || "" });
|
|
244
|
+
}
|
|
245
|
+
store.set({ defaultAiState: next, defaultAiDraft: draft });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function applyStateMessage(msg, state) {
|
|
249
|
+
var isRefresh = !!msg.requestId && msg.requestId === state.refreshRequestId;
|
|
250
|
+
var isSave = !!msg.requestId && msg.requestId === state.saveRequestId;
|
|
251
|
+
if (msg.requestId && !isRefresh && !isSave) return;
|
|
252
|
+
if (typeof msg.canonicalRevision === "number" && msg.canonicalRevision < (state.canonicalRevision || 0)) return;
|
|
253
|
+
var next = Object.assign({}, state);
|
|
254
|
+
if (isRefresh) { next.loading = false; next.refreshRequestId = null; }
|
|
255
|
+
if (isSave) { next.saving = false; next.saveRequestId = null; }
|
|
256
|
+
next.accountAvailable = msg.accountAvailable !== false;
|
|
257
|
+
if (msg.accountId) next.accountId = msg.accountId;
|
|
258
|
+
if (msg.serverEpoch) next.serverEpoch = msg.serverEpoch;
|
|
259
|
+
if (typeof msg.canonicalRevision === "number") next.canonicalRevision = msg.canonicalRevision;
|
|
260
|
+
next.error = msg.error || "";
|
|
261
|
+
next.installedVendors = msg.installedVendors || state.installedVendors || [];
|
|
262
|
+
if (Object.prototype.hasOwnProperty.call(msg, "preference")) next.preference = msg.preference || null;
|
|
263
|
+
if (Object.prototype.hasOwnProperty.call(msg, "selection")) next.selection = msg.selection || null;
|
|
264
|
+
if (msg.catalog) {
|
|
265
|
+
var catalogVendor = msg.selection && msg.selection.vendor || msg.preference && msg.preference.vendor || "";
|
|
266
|
+
var catalogs = Object.assign({}, state.catalogs || {});
|
|
267
|
+
if (catalogVendor) catalogs[catalogVendor] = Object.assign({}, msg.catalog, { loading: false, error: msg.error || msg.catalog.error || "" });
|
|
268
|
+
next.catalogs = catalogs;
|
|
269
|
+
}
|
|
270
|
+
var dirty = store.get("defaultAiDraftDirty") === true;
|
|
271
|
+
var draft = store.get("defaultAiDraft") || {};
|
|
272
|
+
if (!dirty && msg.preference) draft = Object.assign({}, msg.preference);
|
|
273
|
+
else if (!dirty && msg.ready && msg.selection) draft = Object.assign({}, msg.selection);
|
|
274
|
+
if (isSave && msg.ready && msg.selection) { draft = Object.assign({}, msg.preference || msg.selection); dirty = false; }
|
|
275
|
+
store.set({ defaultAiState: next, defaultAiDraft: draft, defaultAiDraftDirty: dirty });
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function handleDefaultAiMessage(msg) {
|
|
279
|
+
if (!msg || ["default_ai_state", "default_ai_catalog"].indexOf(msg.type) === -1) return false;
|
|
280
|
+
var state = Object.assign({}, store.get("defaultAiState") || {});
|
|
281
|
+
var expected = expectedAccountId();
|
|
282
|
+
if (msg.accountId && msg.accountId !== expected) return true;
|
|
283
|
+
if (state.accountId && state.accountId !== expected) return true;
|
|
284
|
+
if (msg.serverEpoch && msg.serverEpoch !== state.serverEpoch) {
|
|
285
|
+
var establishesEpoch = msg.type === "default_ai_state" && !!msg.requestId && msg.requestId === state.refreshRequestId;
|
|
286
|
+
if (state.serverEpoch || !establishesEpoch) return true;
|
|
287
|
+
state = Object.assign({}, state, { serverEpoch: msg.serverEpoch, canonicalRevision: 0, catalogs: {}, catalogRequestIds: {} });
|
|
288
|
+
}
|
|
289
|
+
if (msg.type === "default_ai_catalog") applyCatalogMessage(msg, state);
|
|
290
|
+
else applyStateMessage(msg, state);
|
|
291
|
+
render();
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function initDefaultAi() {
|
|
296
|
+
var actions = document.querySelector(".user-island-actions");
|
|
297
|
+
if (!actions || document.getElementById("default-ai-btn")) return;
|
|
298
|
+
var button = document.createElement("button");
|
|
299
|
+
button.id = "default-ai-btn";
|
|
300
|
+
button.className = "default-ai-button";
|
|
301
|
+
button.title = "Default AI";
|
|
302
|
+
button.setAttribute("aria-label", "Default AI");
|
|
303
|
+
button.setAttribute("aria-haspopup", "dialog");
|
|
304
|
+
button.setAttribute("aria-expanded", "false");
|
|
305
|
+
button.innerHTML = '<span class="default-ai-avatar"></span>';
|
|
306
|
+
var settings = document.getElementById("user-settings-btn");
|
|
307
|
+
if (settings) actions.insertBefore(button, settings);
|
|
308
|
+
else actions.appendChild(button);
|
|
309
|
+
popover = document.createElement("section");
|
|
310
|
+
popover.id = "default-ai-popover";
|
|
311
|
+
popover.className = "default-ai-popover hidden";
|
|
312
|
+
popover.setAttribute("role", "dialog");
|
|
313
|
+
popover.setAttribute("aria-label", "Default AI settings");
|
|
314
|
+
document.body.appendChild(popover);
|
|
315
|
+
button.addEventListener("click", function () {
|
|
316
|
+
if (!popover.classList.contains("hidden")) { close(); return; }
|
|
317
|
+
opener = button;
|
|
318
|
+
button.setAttribute("aria-expanded", "true");
|
|
319
|
+
popover.classList.remove("hidden");
|
|
320
|
+
requestDefaultAi();
|
|
321
|
+
render();
|
|
322
|
+
});
|
|
323
|
+
document.addEventListener("click", function (event) {
|
|
324
|
+
var path = typeof event.composedPath === "function" ? event.composedPath() : [];
|
|
325
|
+
var clickedButton = path.indexOf(button) !== -1 || button.contains(event.target);
|
|
326
|
+
var clickedPopover = path.indexOf(popover) !== -1 || popover.contains(event.target);
|
|
327
|
+
if (!popover || popover.classList.contains("hidden") || clickedPopover || clickedButton) return;
|
|
328
|
+
close();
|
|
329
|
+
});
|
|
330
|
+
document.addEventListener("keydown", function (event) {
|
|
331
|
+
if (event.key !== "Escape" || !popover || popover.classList.contains("hidden")) return;
|
|
332
|
+
event.preventDefault();
|
|
333
|
+
close();
|
|
334
|
+
});
|
|
335
|
+
window.addEventListener("resize", position);
|
|
336
|
+
window.addEventListener("scroll", position, true);
|
|
337
|
+
updateBadge();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export function openDefaultAi() {
|
|
341
|
+
var button = document.getElementById("default-ai-btn");
|
|
342
|
+
if (button && popover && popover.classList.contains("hidden")) button.click();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function updateBadge() {
|
|
346
|
+
var avatar = document.querySelector("#default-ai-btn .default-ai-avatar");
|
|
347
|
+
var selection = (store.get("defaultAiState") || {}).selection;
|
|
348
|
+
if (!avatar) return;
|
|
349
|
+
var source = selection && VENDOR_AVATARS[selection.vendor];
|
|
350
|
+
var image = avatar.querySelector("img");
|
|
351
|
+
if (!source) {
|
|
352
|
+
if (image) image.remove();
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (!image) {
|
|
356
|
+
image = document.createElement("img");
|
|
357
|
+
image.alt = "";
|
|
358
|
+
avatar.appendChild(image);
|
|
359
|
+
}
|
|
360
|
+
if (image.getAttribute("src") !== source) image.setAttribute("src", source);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
store.subscribe(function (state, previous) {
|
|
364
|
+
if (state.myUserId !== previous.myUserId || state.isMultiUserMode !== previous.isMultiUserMode) {
|
|
365
|
+
var accountId = state.isMultiUserMode ? state.myUserId || "" : "default";
|
|
366
|
+
store.set({
|
|
367
|
+
defaultAiState: { loading: false, saving: false, refreshRequestId: null, saveRequestId: null, catalogRequestIds: {}, catalogs: {}, preference: null, selection: null, installedVendors: [], accountAvailable: !!accountId, accountId: accountId, serverEpoch: null, canonicalRevision: 0, error: "" },
|
|
368
|
+
defaultAiDraft: { vendor: "", model: "", effort: "" },
|
|
369
|
+
defaultAiDraftDirty: false,
|
|
370
|
+
});
|
|
371
|
+
if (accountId && state.connected) requestDefaultAi();
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (state.defaultAiState === previous.defaultAiState) return;
|
|
375
|
+
updateBadge();
|
|
376
|
+
if (popover && !popover.classList.contains("hidden")) render();
|
|
377
|
+
});
|