clay-server 3.4.0-beta.2 → 3.4.0-beta.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/daemon-projects.js +3 -3
- package/lib/daemon.js +59 -59
- package/lib/project-connection.js +35 -7
- package/lib/project-debate.js +4 -0
- package/lib/project-mate-interaction.js +21 -1
- package/lib/project-memory.js +3 -0
- package/lib/project-models.js +220 -0
- package/lib/project-session-handoff.js +162 -0
- package/lib/project-sessions.js +13 -45
- package/lib/project-worker-proposal.js +49 -14
- package/lib/project.js +38 -80
- package/lib/public/antigravity-avatar.png +0 -0
- package/lib/public/app.js +29 -0
- package/lib/public/copilot-avatar.svg +5 -0
- package/lib/public/css/command-palette.css +22 -2
- package/lib/public/css/icon-strip.css +29 -13
- package/lib/public/css/input.css +56 -0
- package/lib/public/css/mates.css +6 -0
- package/lib/public/css/menus.css +38 -25
- package/lib/public/css/messages.css +9 -0
- package/lib/public/css/pane.css +16 -3
- package/lib/public/css/pwa-mobile.css +17 -0
- package/lib/public/css/session-actions.css +98 -0
- package/lib/public/css/title-bar.css +19 -0
- package/lib/public/grok-avatar.svg +4 -0
- package/lib/public/index.html +29 -7
- package/lib/public/junie-avatar.svg +11 -0
- package/lib/public/kimi-avatar.svg +4 -0
- package/lib/public/modules/agent-config-selects.js +69 -0
- package/lib/public/modules/app-header.js +4 -22
- package/lib/public/modules/app-messages.js +54 -11
- package/lib/public/modules/app-panels.js +36 -83
- package/lib/public/modules/app-projects.js +3 -1
- package/lib/public/modules/app-rendering.js +19 -4
- package/lib/public/modules/background-tasks-ui.js +55 -0
- package/lib/public/modules/mate-sidebar.js +11 -3
- package/lib/public/modules/model-picker.js +263 -0
- package/lib/public/modules/notifications.js +7 -1
- package/lib/public/modules/pane-bridge.js +11 -0
- package/lib/public/modules/pane-links.js +23 -0
- package/lib/public/modules/project-switcher.js +7 -6
- package/lib/public/modules/session-actions.js +294 -0
- package/lib/public/modules/sidebar-mates.js +1 -1
- package/lib/public/modules/sidebar-mobile.js +2 -1
- package/lib/public/modules/sidebar-projects.js +34 -43
- package/lib/public/modules/sidebar-sessions.js +20 -6
- package/lib/public/modules/split-pair-ui.js +16 -77
- package/lib/public/modules/split-view.js +3 -0
- package/lib/public/modules/tools.js +6 -1
- package/lib/public/modules/vendor-priority.js +14 -0
- package/lib/public/modules/vendor-selection.js +20 -0
- package/lib/public/modules/worker-proposal.js +5 -1
- package/lib/public/modules/worktree-location.js +17 -0
- package/lib/public/qwen-avatar.svg +4 -0
- package/lib/public/style.css +4 -2
- package/lib/sdk-bridge.js +99 -56
- package/lib/sdk-message-processor.js +26 -4
- package/lib/session-handoff-context.js +110 -0
- package/lib/session-notes-mcp-server.js +8 -1
- package/lib/sessions.js +4 -0
- package/lib/worktree.js +9 -4
- package/lib/ws-schema.js +9 -1
- package/lib/yoke/acp-agent-profiles.js +64 -14
- package/lib/yoke/adapters/antigravity.js +417 -0
- package/lib/yoke/adapters/claude.js +34 -0
- package/lib/yoke/adapters/codex.js +101 -5
- package/lib/yoke/adapters/copilot.js +7 -0
- package/lib/yoke/adapters/grok.js +7 -0
- package/lib/yoke/adapters/junie.js +7 -0
- package/lib/yoke/adapters/kimi.js +7 -0
- package/lib/yoke/adapters/kiro.js +2 -2
- package/lib/yoke/adapters/qwen.js +7 -0
- package/lib/yoke/codex-app-server.js +25 -8
- package/lib/yoke/index.js +67 -28
- package/lib/yoke/instructions.js +3 -0
- package/lib/yoke/interface.js +12 -0
- package/lib/yoke/vendor-registry.js +61 -6
- package/package.json +1 -1
- package/lib/public/gemini-avatar.svg +0 -11
- package/lib/yoke/adapters/gemini.js +0 -7
|
@@ -13,33 +13,48 @@ import { showImageModal, showPasteModal } from './app-misc.js';
|
|
|
13
13
|
import { sendMessage, hasSendableContent } from './input.js';
|
|
14
14
|
import { getChatLayout } from './theme.js';
|
|
15
15
|
import { getScheduledMsgEl } from './app-rate-limit.js';
|
|
16
|
+
export { VENDOR_ORDER, EXPERIMENTAL_VENDORS, isExperimentalVendor } from './vendor-priority.js';
|
|
16
17
|
|
|
17
18
|
// Keep these client constants aligned with lib/yoke/vendor-registry.js. The
|
|
18
19
|
// browser cannot import the server's CommonJS registry directly.
|
|
19
20
|
export var VENDOR_AVATARS = {
|
|
20
21
|
claude: "/claude-code-avatar.png",
|
|
21
22
|
codex: "/codex-avatar.png",
|
|
22
|
-
|
|
23
|
+
antigravity: "/antigravity-avatar.png",
|
|
23
24
|
opencode: "/opencode-avatar.svg",
|
|
25
|
+
kimi: "/kimi-avatar.svg",
|
|
26
|
+
grok: "/grok-avatar.svg",
|
|
27
|
+
copilot: "/copilot-avatar.svg",
|
|
28
|
+
qwen: "/qwen-avatar.svg",
|
|
29
|
+
junie: "/junie-avatar.svg",
|
|
24
30
|
kiro: "/kiro-avatar.svg",
|
|
25
31
|
};
|
|
26
32
|
export var VENDOR_NAMES = {
|
|
27
33
|
claude: "Claude Code",
|
|
28
34
|
codex: "Codex",
|
|
29
|
-
|
|
35
|
+
antigravity: "Antigravity CLI",
|
|
30
36
|
opencode: "OpenCode",
|
|
37
|
+
kimi: "Kimi Code",
|
|
38
|
+
grok: "Grok Build",
|
|
39
|
+
copilot: "GitHub Copilot CLI",
|
|
40
|
+
qwen: "Qwen Code",
|
|
41
|
+
junie: "Junie CLI",
|
|
31
42
|
kiro: "Kiro CLI",
|
|
32
43
|
};
|
|
33
44
|
// Display order for every vendor Clay knows about, installed or not. Pickers
|
|
34
45
|
// render the full list so a missing CLI reads as "not installed yet" rather
|
|
35
46
|
// than "Clay doesn't support it".
|
|
36
|
-
export var VENDOR_ORDER = ["claude", "codex", "gemini", "opencode", "kiro"];
|
|
37
47
|
// Where to send the user when they pick a vendor whose CLI isn't installed.
|
|
38
48
|
export var VENDOR_HOMEPAGES = {
|
|
39
49
|
claude: "https://claude.com/product/claude-code",
|
|
40
50
|
codex: "https://openai.com/codex/",
|
|
41
|
-
|
|
51
|
+
antigravity: "https://antigravity.google/product/antigravity-cli",
|
|
42
52
|
opencode: "https://opencode.ai/",
|
|
53
|
+
kimi: "https://www.kimi.com/code/",
|
|
54
|
+
grok: "https://docs.x.ai/build/overview",
|
|
55
|
+
copilot: "https://github.com/features/copilot/cli",
|
|
56
|
+
qwen: "https://qwenlm.github.io/qwen-code-docs/",
|
|
57
|
+
junie: "https://junie.jetbrains.com/",
|
|
43
58
|
kiro: "https://kiro.dev/",
|
|
44
59
|
};
|
|
45
60
|
var NEW_MSG_BTN_DEFAULT = "\u2193 Latest";
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { getWs } from './ws-ref.js';
|
|
3
|
+
import { escapeHtml } from './utils.js';
|
|
4
|
+
|
|
5
|
+
var expanded = false;
|
|
6
|
+
|
|
7
|
+
export function initBackgroundTasksUi() {
|
|
8
|
+
store.subscribe(function(state, prev) {
|
|
9
|
+
if (state.activeBackgroundTasks !== prev.activeBackgroundTasks) renderBackgroundTasks();
|
|
10
|
+
});
|
|
11
|
+
renderBackgroundTasks();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function renderBackgroundTasks() {
|
|
15
|
+
var tasks = store.get('activeBackgroundTasks') || [];
|
|
16
|
+
var existing = document.getElementById('background-tasks-bar');
|
|
17
|
+
if (tasks.length === 0) {
|
|
18
|
+
if (existing) existing.remove();
|
|
19
|
+
expanded = false;
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
var inputArea = document.getElementById('input-area');
|
|
23
|
+
if (!inputArea || !inputArea.parentNode) return;
|
|
24
|
+
var bar = existing || document.createElement('div');
|
|
25
|
+
bar.id = 'background-tasks-bar';
|
|
26
|
+
bar.className = 'background-tasks-bar';
|
|
27
|
+
var taskLabel = tasks.length === 1 ? 'background task' : 'background tasks';
|
|
28
|
+
var html = '<button type="button" class="background-tasks-toggle" aria-expanded="' + expanded + '">' +
|
|
29
|
+
'<span aria-hidden="true">⏳</span><span class="background-tasks-chip">' + tasks.length + '</span>' +
|
|
30
|
+
'<span>' + tasks.length + ' ' + taskLabel + '</span></button>';
|
|
31
|
+
if (expanded) {
|
|
32
|
+
html += '<div class="background-tasks-list">';
|
|
33
|
+
for (var i = 0; i < tasks.length; i++) {
|
|
34
|
+
var task = tasks[i];
|
|
35
|
+
html += '<div class="background-task-row"><span class="background-task-description" title="' + escapeHtml(task.description || '') + '">' +
|
|
36
|
+
escapeHtml(task.description || 'Background task') + '</span><span class="background-task-type">' +
|
|
37
|
+
escapeHtml(task.task_type || 'other') + '</span><button type="button" class="background-task-stop" data-task-id="' +
|
|
38
|
+
escapeHtml(task.task_id || '') + '">Stop</button></div>';
|
|
39
|
+
}
|
|
40
|
+
html += '</div>';
|
|
41
|
+
}
|
|
42
|
+
bar.innerHTML = html;
|
|
43
|
+
if (!existing) inputArea.parentNode.insertBefore(bar, inputArea);
|
|
44
|
+
bar.querySelector('.background-tasks-toggle').addEventListener('click', function() {
|
|
45
|
+
expanded = !expanded;
|
|
46
|
+
renderBackgroundTasks();
|
|
47
|
+
});
|
|
48
|
+
var stopButtons = bar.querySelectorAll('.background-task-stop');
|
|
49
|
+
for (var j = 0; j < stopButtons.length; j++) {
|
|
50
|
+
stopButtons[j].addEventListener('click', function() {
|
|
51
|
+
var ws = getWs();
|
|
52
|
+
if (ws && ws.readyState === 1) ws.send(JSON.stringify({ type: 'stop_task', taskId: this.dataset.taskId }));
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -6,6 +6,7 @@ import { hideKnowledge } from './mate-knowledge.js';
|
|
|
6
6
|
import { isSchedulerOpen, closeScheduler } from './scheduler.js';
|
|
7
7
|
import { hideNotes } from './sticky-notes.js';
|
|
8
8
|
import { openSearch as openSessionSearch } from './session-search.js';
|
|
9
|
+
import { isExperimentalVendor } from './vendor-priority.js';
|
|
9
10
|
|
|
10
11
|
var getWs = null;
|
|
11
12
|
var currentMateId = null;
|
|
@@ -152,19 +153,26 @@ export function showMateSidebar(mateId, mateData) {
|
|
|
152
153
|
var vendorIcons = {
|
|
153
154
|
claude: "/claude-code-avatar.png",
|
|
154
155
|
codex: "/codex-avatar.png",
|
|
155
|
-
|
|
156
|
+
antigravity: "/antigravity-avatar.png",
|
|
156
157
|
opencode: "/opencode-avatar.svg",
|
|
158
|
+
kimi: "/kimi-avatar.svg",
|
|
159
|
+
grok: "/grok-avatar.svg",
|
|
160
|
+
copilot: "/copilot-avatar.svg",
|
|
161
|
+
qwen: "/qwen-avatar.svg",
|
|
162
|
+
junie: "/junie-avatar.svg",
|
|
157
163
|
kiro: "/kiro-avatar.svg",
|
|
158
164
|
};
|
|
159
|
-
var vendorNames = { claude: "Claude Code", codex: "Codex",
|
|
160
|
-
var vendorKeys = ["claude", "codex", "
|
|
165
|
+
var vendorNames = { claude: "Claude Code", codex: "Codex", grok: "Grok Build", kimi: "Kimi Code", copilot: "GitHub Copilot CLI", qwen: "Qwen Code", junie: "Junie CLI", antigravity: "Antigravity CLI", opencode: "OpenCode", kiro: "Kiro CLI" };
|
|
166
|
+
var vendorKeys = ["claude", "codex", "grok", "kimi", "copilot", "qwen", "junie", "antigravity", "opencode", "kiro"];
|
|
161
167
|
mateVendorWrap.innerHTML = "";
|
|
162
168
|
for (var vi = 0; vi < vendorKeys.length; vi++) {
|
|
163
169
|
var vk = vendorKeys[vi];
|
|
164
170
|
var vBtn = document.createElement("button");
|
|
165
171
|
vBtn.className = "mate-vendor-btn" + (vk === mateVendor ? " active" : "");
|
|
172
|
+
if (isExperimentalVendor(vk)) vBtn.classList.add("experimental");
|
|
166
173
|
if (available.indexOf(vk) === -1) vBtn.classList.add("disabled");
|
|
167
174
|
vBtn.dataset.vendor = vk;
|
|
175
|
+
vBtn.title = vendorNames[vk] + (isExperimentalVendor(vk) ? " · Experimental integration; not yet validated through direct use testing" : "");
|
|
168
176
|
vBtn.innerHTML = '<img src="' + vendorIcons[vk] + '" class="mate-vendor-icon" alt="' + vendorNames[vk] + '"><span class="mate-vendor-label">' + vendorNames[vk] + '</span>';
|
|
169
177
|
vBtn.addEventListener("click", function() {
|
|
170
178
|
var v = this.dataset.vendor;
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { getWs } from './ws-ref.js';
|
|
3
|
+
|
|
4
|
+
var configModelList = null;
|
|
5
|
+
var configPopover = null;
|
|
6
|
+
var configChip = null;
|
|
7
|
+
var requestCounter = 0;
|
|
8
|
+
var selectionCounter = 0;
|
|
9
|
+
var requestTimer = null;
|
|
10
|
+
|
|
11
|
+
export function modelEntryValue(entry) {
|
|
12
|
+
if (!entry) return "";
|
|
13
|
+
if (typeof entry === "string") return entry;
|
|
14
|
+
return entry.value || entry.id || "";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function modelEntryMatches(entry, value) {
|
|
18
|
+
if (!entry || !value) return false;
|
|
19
|
+
if (typeof entry === "string") return entry === value;
|
|
20
|
+
return entry.value === value || entry.id === value || entry.resolvedModel === value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function modelDisplayName(value, models) {
|
|
24
|
+
if (!value) return "";
|
|
25
|
+
var list = models || [];
|
|
26
|
+
for (var i = 0; i < list.length; i++) {
|
|
27
|
+
if (modelEntryMatches(list[i], value)) {
|
|
28
|
+
return typeof list[i] === "string" ? list[i] : (list[i].displayName || modelEntryValue(list[i]));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function vendorDisplayName(vendor) {
|
|
35
|
+
var info = store.get('vendorInfo') || {};
|
|
36
|
+
return info[vendor] && info[vendor].displayName ? info[vendor].displayName : vendor;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function clearRequestTimer() {
|
|
40
|
+
if (!requestTimer) return;
|
|
41
|
+
clearTimeout(requestTimer);
|
|
42
|
+
requestTimer = null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function setupModelPicker() {
|
|
46
|
+
configModelList = document.getElementById("config-model-list");
|
|
47
|
+
configPopover = document.getElementById("config-popover");
|
|
48
|
+
configChip = document.getElementById("config-chip");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function resetModelPickerState() {
|
|
52
|
+
clearRequestTimer();
|
|
53
|
+
store.set({
|
|
54
|
+
modelListVendor: "",
|
|
55
|
+
modelListStatus: "idle",
|
|
56
|
+
modelListError: "",
|
|
57
|
+
modelRequestId: "",
|
|
58
|
+
modelSelectionPending: null,
|
|
59
|
+
modelSelectionError: "",
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function requestVendorModels(vendor, force) {
|
|
64
|
+
if (!vendor) return null;
|
|
65
|
+
var s = store.snap();
|
|
66
|
+
if (!force && s.modelListVendor === vendor) {
|
|
67
|
+
if (s.modelListStatus === "loading") return s.modelRequestId || null;
|
|
68
|
+
if (s.modelListStatus === "ready" && s.currentModels && s.currentModels.length > 0) return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
var ws = getWs();
|
|
72
|
+
if (!ws || ws.readyState !== 1) {
|
|
73
|
+
store.set({
|
|
74
|
+
modelListVendor: vendor,
|
|
75
|
+
modelListStatus: "error",
|
|
76
|
+
modelListError: "The connection is not ready. Reconnect and retry.",
|
|
77
|
+
});
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
requestCounter++;
|
|
82
|
+
var requestId = "models-" + Date.now().toString(36) + "-" + requestCounter;
|
|
83
|
+
clearRequestTimer();
|
|
84
|
+
store.set({
|
|
85
|
+
modelListVendor: vendor,
|
|
86
|
+
modelListStatus: "loading",
|
|
87
|
+
modelListError: "",
|
|
88
|
+
modelRequestId: requestId,
|
|
89
|
+
});
|
|
90
|
+
ws.send(JSON.stringify({ type: "get_vendor_models", vendor: vendor, requestId: requestId }));
|
|
91
|
+
requestTimer = setTimeout(function() {
|
|
92
|
+
var current = store.snap();
|
|
93
|
+
if (current.modelRequestId !== requestId || current.modelListStatus !== "loading") return;
|
|
94
|
+
store.set({
|
|
95
|
+
modelListStatus: "error",
|
|
96
|
+
modelListError: vendorDisplayName(vendor) + " model loading timed out. Retry to try again.",
|
|
97
|
+
});
|
|
98
|
+
}, 35000);
|
|
99
|
+
return requestId;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function prepareModelPickerOpen() {
|
|
103
|
+
var s = store.snap();
|
|
104
|
+
var vendor = s.currentVendor || "claude";
|
|
105
|
+
var needsModels = !s.currentModels || s.currentModels.length === 0;
|
|
106
|
+
var wrongVendor = s.modelListVendor !== vendor;
|
|
107
|
+
var retryable = s.modelListStatus === "error" || s.modelListStatus === "empty" || s.modelListStatus === "idle";
|
|
108
|
+
requestVendorModels(vendor, needsModels || wrongVendor || retryable);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function getModelInfoUpdate(msg) {
|
|
112
|
+
var vendor = msg.vendor || store.get('currentVendor') || "claude";
|
|
113
|
+
var currentVendor = store.get('currentVendor');
|
|
114
|
+
if (currentVendor && vendor !== currentVendor) return null;
|
|
115
|
+
if (msg.sessionId != null && msg.sessionId !== store.get('activeSessionId')) return null;
|
|
116
|
+
|
|
117
|
+
var s = store.snap();
|
|
118
|
+
if (msg.requestId && s.modelRequestId && msg.requestId !== s.modelRequestId) return null;
|
|
119
|
+
|
|
120
|
+
var models = Array.isArray(msg.models) ? msg.models : [];
|
|
121
|
+
var unsolicitedEmptyWhileLoading = s.modelListStatus === "loading"
|
|
122
|
+
&& s.modelListVendor === vendor
|
|
123
|
+
&& !msg.requestId
|
|
124
|
+
&& !msg.modelStatus
|
|
125
|
+
&& !msg.error
|
|
126
|
+
&& models.length === 0;
|
|
127
|
+
if (unsolicitedEmptyWhileLoading) {
|
|
128
|
+
return {
|
|
129
|
+
modelListVendor: vendor,
|
|
130
|
+
modelListStatus: "loading",
|
|
131
|
+
modelListError: "",
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
clearRequestTimer();
|
|
136
|
+
var status = msg.modelStatus || (msg.error ? "error" : (models.length > 0 ? "ready" : "empty"));
|
|
137
|
+
return {
|
|
138
|
+
modelListVendor: vendor,
|
|
139
|
+
modelListStatus: status,
|
|
140
|
+
modelListError: msg.error || "",
|
|
141
|
+
modelRequestId: "",
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function requestModelSelection(model) {
|
|
146
|
+
var ws = getWs();
|
|
147
|
+
if (!ws || ws.readyState !== 1) {
|
|
148
|
+
store.set({ modelSelectionError: "The connection is not ready. Reconnect before selecting a model." });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
var s = store.snap();
|
|
152
|
+
selectionCounter++;
|
|
153
|
+
var requestId = "model-select-" + Date.now().toString(36) + "-" + selectionCounter;
|
|
154
|
+
store.set({
|
|
155
|
+
currentModel: model,
|
|
156
|
+
modelSelectionPending: { requestId: requestId, model: model, previousModel: s.currentModel || "" },
|
|
157
|
+
modelSelectionError: "",
|
|
158
|
+
});
|
|
159
|
+
ws.send(JSON.stringify({
|
|
160
|
+
type: "set_model",
|
|
161
|
+
model: model,
|
|
162
|
+
vendor: s.currentVendor || "claude",
|
|
163
|
+
requestId: requestId,
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function handleModelSelectionResult(msg) {
|
|
168
|
+
var pending = store.get('modelSelectionPending');
|
|
169
|
+
if (!pending || (msg.requestId && msg.requestId !== pending.requestId)) return false;
|
|
170
|
+
if (msg.ok) {
|
|
171
|
+
store.set({
|
|
172
|
+
currentModel: msg.model || pending.model,
|
|
173
|
+
modelSelectionPending: null,
|
|
174
|
+
modelSelectionError: "",
|
|
175
|
+
});
|
|
176
|
+
if (configPopover) configPopover.classList.add("hidden");
|
|
177
|
+
if (configChip) configChip.classList.remove("active");
|
|
178
|
+
} else {
|
|
179
|
+
store.set({
|
|
180
|
+
currentModel: pending.previousModel,
|
|
181
|
+
modelSelectionPending: null,
|
|
182
|
+
modelSelectionError: msg.error || "The model could not be selected.",
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function appendState(title, detail, retry) {
|
|
189
|
+
var stateEl = document.createElement("div");
|
|
190
|
+
stateEl.className = "config-model-state";
|
|
191
|
+
if (title === "Loading models…") {
|
|
192
|
+
var spinner = document.createElement("span");
|
|
193
|
+
spinner.className = "config-model-spinner";
|
|
194
|
+
spinner.setAttribute("aria-hidden", "true");
|
|
195
|
+
stateEl.appendChild(spinner);
|
|
196
|
+
}
|
|
197
|
+
var textWrap = document.createElement("div");
|
|
198
|
+
var titleEl = document.createElement("div");
|
|
199
|
+
titleEl.className = "config-model-state-title";
|
|
200
|
+
titleEl.textContent = title;
|
|
201
|
+
textWrap.appendChild(titleEl);
|
|
202
|
+
if (detail) {
|
|
203
|
+
var detailEl = document.createElement("div");
|
|
204
|
+
detailEl.className = "config-model-state-detail";
|
|
205
|
+
detailEl.textContent = detail;
|
|
206
|
+
textWrap.appendChild(detailEl);
|
|
207
|
+
}
|
|
208
|
+
stateEl.appendChild(textWrap);
|
|
209
|
+
if (retry) {
|
|
210
|
+
var retryBtn = document.createElement("button");
|
|
211
|
+
retryBtn.type = "button";
|
|
212
|
+
retryBtn.className = "config-model-retry";
|
|
213
|
+
retryBtn.textContent = "Retry";
|
|
214
|
+
retryBtn.addEventListener("click", function() {
|
|
215
|
+
requestVendorModels(store.get('currentVendor') || "claude", true);
|
|
216
|
+
});
|
|
217
|
+
stateEl.appendChild(retryBtn);
|
|
218
|
+
}
|
|
219
|
+
configModelList.appendChild(stateEl);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function renderModelPicker() {
|
|
223
|
+
if (!configModelList) return;
|
|
224
|
+
var s = store.snap();
|
|
225
|
+
var vendor = s.currentVendor || "claude";
|
|
226
|
+
var status = s.modelListVendor === vendor ? s.modelListStatus : "idle";
|
|
227
|
+
configModelList.innerHTML = "";
|
|
228
|
+
|
|
229
|
+
if (status === "loading") {
|
|
230
|
+
appendState("Loading models…", "Starting " + vendorDisplayName(vendor) + " and requesting its model catalog.", false);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (status === "error") {
|
|
234
|
+
appendState("Couldn’t load models", s.modelListError || "An unexpected error occurred.", true);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (status === "empty" || !s.currentModels || s.currentModels.length === 0) {
|
|
238
|
+
appendState("No models available", s.modelListError || "The vendor returned an empty model catalog.", true);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
for (var i = 0; i < s.currentModels.length; i++) {
|
|
243
|
+
var item = s.currentModels[i];
|
|
244
|
+
var value = modelEntryValue(item);
|
|
245
|
+
if (!value) continue;
|
|
246
|
+
var button = document.createElement("button");
|
|
247
|
+
button.className = "config-radio-item";
|
|
248
|
+
if (modelEntryMatches(item, s.currentModel)) button.classList.add("active");
|
|
249
|
+
button.dataset.model = value;
|
|
250
|
+
button.textContent = typeof item === "string" ? item : (item.displayName || value);
|
|
251
|
+
if (s.modelSelectionPending && s.modelSelectionPending.model === value) {
|
|
252
|
+
button.classList.add("pending");
|
|
253
|
+
button.textContent += " · Selecting…";
|
|
254
|
+
}
|
|
255
|
+
button.disabled = !!s.modelSelectionPending;
|
|
256
|
+
button.addEventListener("click", function() { requestModelSelection(this.dataset.model); });
|
|
257
|
+
configModelList.appendChild(button);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (s.modelSelectionError) {
|
|
261
|
+
appendState("Model selection failed", s.modelSelectionError, false);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
@@ -72,6 +72,7 @@ export function initNotifications(_ctx) {
|
|
|
72
72
|
if (window.visualViewport) {
|
|
73
73
|
var layout = $("layout");
|
|
74
74
|
var mobileTabBar = document.getElementById("mobile-tab-bar");
|
|
75
|
+
var keyboardWasOpen = false;
|
|
75
76
|
// Capture initial viewport height before any keyboard interaction.
|
|
76
77
|
// In iOS PWA standalone mode, window.innerHeight shrinks when the
|
|
77
78
|
// keyboard opens, so we need a stable baseline for comparison.
|
|
@@ -86,7 +87,12 @@ export function initNotifications(_ctx) {
|
|
|
86
87
|
// Toggle class so CSS can remove the tab-bar bottom padding while keyboard is up
|
|
87
88
|
var keyboardOpen = vv.height < stableViewportHeight - 100;
|
|
88
89
|
document.body.classList.toggle("keyboard-open", keyboardOpen);
|
|
89
|
-
if (
|
|
90
|
+
if (keyboardOpen && !keyboardWasOpen && ctx.forceScrollToBottom) {
|
|
91
|
+
requestAnimationFrame(function() { ctx.forceScrollToBottom(); });
|
|
92
|
+
} else if (!keyboardOpen) {
|
|
93
|
+
ctx.scrollToBottom();
|
|
94
|
+
}
|
|
95
|
+
keyboardWasOpen = keyboardOpen;
|
|
90
96
|
// Hide tab bar when software keyboard is open
|
|
91
97
|
if (mobileTabBar) {
|
|
92
98
|
if (keyboardOpen) {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { store } from './store.js';
|
|
7
7
|
import { applyContextView, getContextView } from './app-panels.js';
|
|
8
|
+
import { forceExternalLinkToNewTab } from './pane-links.js';
|
|
8
9
|
|
|
9
10
|
var panelOpen = false;
|
|
10
11
|
|
|
@@ -29,9 +30,19 @@ function togglePaneContextPanel() {
|
|
|
29
30
|
applyContextView(panelOpen ? "panel" : "off");
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
function preparePaneLink(event) {
|
|
34
|
+
var target = event.target;
|
|
35
|
+
if (!target || typeof target.closest !== "function") return;
|
|
36
|
+
var anchor = target.closest("a[href]");
|
|
37
|
+
if (!anchor || anchor.hasAttribute("download")) return;
|
|
38
|
+
forceExternalLinkToNewTab(anchor, window.location.href);
|
|
39
|
+
}
|
|
40
|
+
|
|
32
41
|
export function initPaneBridge() {
|
|
33
42
|
if (!store.get('paneMode')) return;
|
|
34
43
|
panelOpen = getContextView() === "panel";
|
|
44
|
+
document.addEventListener("click", preparePaneLink, true);
|
|
45
|
+
document.addEventListener("auxclick", preparePaneLink, true);
|
|
35
46
|
window.addEventListener("message", function (event) {
|
|
36
47
|
if (event.origin !== window.location.origin) return;
|
|
37
48
|
var msg = event.data;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function isExternalWebLink(href, currentHref) {
|
|
2
|
+
if (!href) return false;
|
|
3
|
+
try {
|
|
4
|
+
var url = new URL(href, currentHref);
|
|
5
|
+
var currentUrl = new URL(currentHref);
|
|
6
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
7
|
+
return url.origin !== currentUrl.origin;
|
|
8
|
+
} catch (err) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function forceExternalLinkToNewTab(anchor, currentHref) {
|
|
14
|
+
if (!anchor || !isExternalWebLink(anchor.href, currentHref)) return false;
|
|
15
|
+
|
|
16
|
+
var rel = (anchor.getAttribute("rel") || "").split(/\s+/).filter(Boolean);
|
|
17
|
+
if (rel.indexOf("noopener") === -1) rel.push("noopener");
|
|
18
|
+
if (rel.indexOf("noreferrer") === -1) rel.push("noreferrer");
|
|
19
|
+
|
|
20
|
+
anchor.setAttribute("target", "_blank");
|
|
21
|
+
anchor.setAttribute("rel", rel.join(" "));
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
@@ -19,6 +19,7 @@ import { getCachedProjects, switchProject } from './app-projects.js';
|
|
|
19
19
|
import { openDm } from './app-dm.js';
|
|
20
20
|
import { mateAvatarUrl } from './avatar.js';
|
|
21
21
|
import { parseEmojis } from './markdown.js';
|
|
22
|
+
import { isExternalWorktree, externalWorktreeTooltip, appendExternalWorktreeBadge } from './worktree-location.js';
|
|
22
23
|
|
|
23
24
|
var _projectMru = []; // project slugs, most recent first
|
|
24
25
|
var _mateMru = []; // mate ids (mate_XXX), most recent first
|
|
@@ -257,18 +258,16 @@ function buildProjectEntries() {
|
|
|
257
258
|
// Keep natural (server-provided) order so the list looks the same
|
|
258
259
|
// every time the switcher opens. MRU only drives initial highlight.
|
|
259
260
|
return projects.map(function (p) {
|
|
260
|
-
|
|
261
|
-
// unmounted or moved); those are effectively unreachable from
|
|
262
|
-
// this session and shouldn't be switchable.
|
|
263
|
-
var unreachable = p.isWorktree && p.worktreeAccessible === false;
|
|
261
|
+
var external = isExternalWorktree(p);
|
|
264
262
|
return {
|
|
265
263
|
key: p.slug,
|
|
266
264
|
title: p.title || p.project || p.slug,
|
|
267
265
|
icon: p.icon || null,
|
|
268
266
|
fallbackIcon: 'folder',
|
|
269
267
|
isCurrent: p.slug === store.get('currentSlug'),
|
|
270
|
-
|
|
271
|
-
|
|
268
|
+
external: external,
|
|
269
|
+
disabled: false,
|
|
270
|
+
disabledReason: null,
|
|
272
271
|
};
|
|
273
272
|
});
|
|
274
273
|
}
|
|
@@ -336,6 +335,7 @@ function buildEntryNode(entry, index) {
|
|
|
336
335
|
item.className = 'cmd-palette-item project-switcher-item';
|
|
337
336
|
if (entry.disabled) item.classList.add('disabled');
|
|
338
337
|
if (entry.disabled && entry.disabledReason) item.title = entry.disabledReason;
|
|
338
|
+
if (entry.external) item.title = externalWorktreeTooltip(entry.title);
|
|
339
339
|
item.dataset.index = String(index);
|
|
340
340
|
|
|
341
341
|
var iconWrap = document.createElement('div');
|
|
@@ -353,6 +353,7 @@ function buildEntryNode(entry, index) {
|
|
|
353
353
|
fallback.setAttribute('data-lucide', entry.fallbackIcon || 'folder');
|
|
354
354
|
iconWrap.appendChild(fallback);
|
|
355
355
|
}
|
|
356
|
+
if (entry.external) appendExternalWorktreeBadge(iconWrap, 'project-switcher-external-badge');
|
|
356
357
|
item.appendChild(iconWrap);
|
|
357
358
|
|
|
358
359
|
var body = document.createElement('div');
|