clay-server 2.47.0-beta.5 → 3.0.0-beta.1
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.js +6 -27
- package/lib/project-connection.js +21 -8
- package/lib/project-http.js +5 -2
- package/lib/project-session-pair.js +312 -0
- package/lib/project-session-spawn.js +8 -4
- package/lib/project-sessions.js +108 -10
- package/lib/project-user-message.js +18 -8
- package/lib/project.js +33 -0
- package/lib/public/app.js +24 -1
- package/lib/public/css/icon-strip.css +0 -190
- package/lib/public/css/menus.css +83 -1
- package/lib/public/css/messages.css +14 -0
- package/lib/public/css/mobile-nav.css +4 -0
- package/lib/public/css/pane.css +420 -0
- package/lib/public/css/sidebar.css +40 -0
- package/lib/public/css/sticky-notes.css +2 -0
- package/lib/public/css/title-bar.css +169 -0
- package/lib/public/index.html +10 -0
- package/lib/public/modules/app-connection.js +6 -0
- package/lib/public/modules/app-favicon.js +6 -10
- package/lib/public/modules/app-header.js +101 -6
- package/lib/public/modules/app-home-hub.js +2 -0
- package/lib/public/modules/app-messages.js +63 -13
- package/lib/public/modules/app-panels.js +49 -27
- package/lib/public/modules/app-projects.js +43 -30
- package/lib/public/modules/app-rendering.js +11 -4
- package/lib/public/modules/branch-switcher.js +178 -0
- package/lib/public/modules/dom-refs.js +0 -1
- package/lib/public/modules/pane-bridge.js +41 -0
- package/lib/public/modules/pane-session.js +18 -0
- package/lib/public/modules/project-switcher.js +2 -1
- package/lib/public/modules/sidebar-mobile.js +19 -6
- package/lib/public/modules/sidebar-projects.js +63 -236
- package/lib/public/modules/sidebar-sessions.js +175 -1
- package/lib/public/modules/sidebar.js +2 -0
- package/lib/public/modules/split-group-helpers.js +18 -0
- package/lib/public/modules/split-pair-ui.js +324 -0
- package/lib/public/modules/split-view.js +499 -0
- package/lib/public/modules/sticky-notes.js +32 -91
- package/lib/public/modules/worktree-family.js +60 -0
- package/lib/public/style.css +1 -0
- package/lib/sdk-bridge.js +111 -20
- package/lib/sdk-message-processor.js +2 -2
- package/lib/server.js +24 -4
- package/lib/session-hygiene.js +100 -0
- package/lib/session-pair-mcp-server.js +28 -0
- package/lib/session-split-groups.js +298 -0
- package/lib/sessions.js +163 -34
- package/lib/ws-request.js +14 -0
- package/lib/ws-schema.js +13 -0
- package/lib/yoke/adapters/claude.js +32 -0
- package/lib/yoke/adapters/codex.js +100 -77
- package/lib/yoke/adapters/kiro.js +34 -35
- package/lib/yoke/codex-app-server.js +56 -6
- package/lib/yoke/index.js +1 -0
- package/lib/yoke/skill-discovery.js +196 -0
- package/lib/yoke/vendor-registry.js +34 -0
- package/package.json +1 -1
|
@@ -6,6 +6,7 @@ import { escapeHtml, showToast } from "./utils.js";
|
|
|
6
6
|
import { store } from './store.js';
|
|
7
7
|
import { getWs } from './ws-ref.js';
|
|
8
8
|
import { VENDOR_NAMES } from './app-rendering.js';
|
|
9
|
+
import { reportPaneContext } from './pane-bridge.js';
|
|
9
10
|
|
|
10
11
|
// --- Module-owned state (not in store) ---
|
|
11
12
|
var sessionUsage = { cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
|
|
@@ -150,12 +151,30 @@ function modeDisplayName(value) {
|
|
|
150
151
|
return value;
|
|
151
152
|
}
|
|
152
153
|
|
|
153
|
-
function effortDisplayName(value) {
|
|
154
|
+
export function effortDisplayName(value) {
|
|
154
155
|
if (!value) return "";
|
|
155
156
|
if (value === "xhigh") return "X-High";
|
|
156
157
|
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
157
158
|
}
|
|
158
159
|
|
|
160
|
+
// Effort levels for an arbitrary vendor/model pair (pure; the pair dialog
|
|
161
|
+
// uses this for sessions other than the active one). Falls back from the
|
|
162
|
+
// model's own supportedEffortLevels to the vendor table.
|
|
163
|
+
export function effortLevelsFor(vendor, models, modelValue) {
|
|
164
|
+
var defaults = EFFORT_LEVELS_BY_VENDOR[vendor] || EFFORT_LEVELS;
|
|
165
|
+
var list = models || [];
|
|
166
|
+
for (var i = 0; i < list.length; i++) {
|
|
167
|
+
var value = typeof list[i] === "string" ? list[i] : (list[i] && (list[i].value || list[i].id)) || "";
|
|
168
|
+
if (value === modelValue) {
|
|
169
|
+
if (list[i].supportedEffortLevels && list[i].supportedEffortLevels.length > 0) {
|
|
170
|
+
return list[i].supportedEffortLevels;
|
|
171
|
+
}
|
|
172
|
+
return defaults;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return defaults;
|
|
176
|
+
}
|
|
177
|
+
|
|
159
178
|
function thinkingDisplayName(value) {
|
|
160
179
|
if (value === "disabled") return "Off";
|
|
161
180
|
if (value === "adaptive") return "Adaptive";
|
|
@@ -179,22 +198,25 @@ function hasBeta(name) {
|
|
|
179
198
|
|
|
180
199
|
function rebuildModelList() {
|
|
181
200
|
if (!configModelList) return;
|
|
182
|
-
// Picker
|
|
183
|
-
// Claude
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
201
|
+
// Picker lock policy is capability-based (yoke midSessionModelSwitch):
|
|
202
|
+
// Claude -> free. The SDK supports mid-thread swaps on a live query
|
|
203
|
+
// (query.setModel: "Change the model used for subsequent
|
|
204
|
+
// responses"), and sdk-bridge already routes set_model to
|
|
205
|
+
// queryInstance.setModel. Cache re-priming on switch is a
|
|
206
|
+
// cost, not a breakage.
|
|
207
|
+
// Codex / Kiro -> locked once the session has history: their threads
|
|
208
|
+
// bind the model at creation and a mid-thread change would be
|
|
209
|
+
// silently ignored. Picks made before the first message still
|
|
210
|
+
// work everywhere (sdk-bridge stores into sm.currentModel when
|
|
211
|
+
// there's no active queryInstance).
|
|
191
212
|
var modelSection = configModelList.parentElement;
|
|
192
213
|
var s = store.snap();
|
|
193
214
|
var vendor = s.currentVendor || "claude";
|
|
194
215
|
if (modelSection) modelSection.style.display = "";
|
|
195
216
|
configModelList.innerHTML = "";
|
|
196
217
|
|
|
197
|
-
var
|
|
218
|
+
var caps = s.vendorCapabilities || {};
|
|
219
|
+
var lockedForGui = s.activeSessionMode === "gui" && !!s.sessionHasHistory && !caps.midSessionModelSwitch;
|
|
198
220
|
|
|
199
221
|
var list = s.currentModels.length > 0 ? s.currentModels : (s.currentModel ? [{ value: s.currentModel, displayName: s.currentModel }] : []);
|
|
200
222
|
for (var i = 0; i < list.length; i++) {
|
|
@@ -210,7 +232,7 @@ function rebuildModelList() {
|
|
|
210
232
|
btn.textContent = label;
|
|
211
233
|
if (lockedForGui) {
|
|
212
234
|
btn.disabled = true;
|
|
213
|
-
btn.title = "
|
|
235
|
+
btn.title = "This vendor binds the model when the session starts. Start a new session to change it.";
|
|
214
236
|
} else {
|
|
215
237
|
btn.addEventListener("click", function () {
|
|
216
238
|
var model = this.dataset.model;
|
|
@@ -228,7 +250,7 @@ function rebuildModelList() {
|
|
|
228
250
|
if (lockedForGui) {
|
|
229
251
|
var hint = document.createElement("div");
|
|
230
252
|
hint.className = "config-model-hint";
|
|
231
|
-
hint.textContent = "Locked after first message
|
|
253
|
+
hint.textContent = "Locked after first message. Start a new session to change models.";
|
|
232
254
|
configModelList.appendChild(hint);
|
|
233
255
|
}
|
|
234
256
|
}
|
|
@@ -471,6 +493,9 @@ export function initPanels() {
|
|
|
471
493
|
}
|
|
472
494
|
updateContextPanel();
|
|
473
495
|
}
|
|
496
|
+
// Split opened/closed -> show or hide the title-bar context gauge
|
|
497
|
+
if (state.splitPanes !== prev.splitPanes) updateContextPanel();
|
|
498
|
+
|
|
474
499
|
// Vendor changed -> switch model list and current model to match
|
|
475
500
|
if (state.currentVendor !== prev.currentVendor && state.currentVendor) {
|
|
476
501
|
var ws = getWs();
|
|
@@ -670,18 +695,7 @@ export function getModelSupportsEffort() {
|
|
|
670
695
|
|
|
671
696
|
export function getModelEffortLevels() {
|
|
672
697
|
var s = store.snap();
|
|
673
|
-
|
|
674
|
-
var defaultLevels = EFFORT_LEVELS_BY_VENDOR[vendor] || EFFORT_LEVELS;
|
|
675
|
-
if (!s.currentModels || s.currentModels.length === 0) return defaultLevels;
|
|
676
|
-
for (var i = 0; i < s.currentModels.length; i++) {
|
|
677
|
-
if (s.currentModels[i].value === s.currentModel) {
|
|
678
|
-
if (s.currentModels[i].supportedEffortLevels && s.currentModels[i].supportedEffortLevels.length > 0) {
|
|
679
|
-
return s.currentModels[i].supportedEffortLevels;
|
|
680
|
-
}
|
|
681
|
-
return defaultLevels;
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
return defaultLevels;
|
|
698
|
+
return effortLevelsFor(s.currentVendor || "claude", s.currentModels || [], s.currentModel);
|
|
685
699
|
}
|
|
686
700
|
|
|
687
701
|
// --- Usage panel ---
|
|
@@ -807,6 +821,7 @@ export function updateContextPanel() {
|
|
|
807
821
|
var win = contextData.contextWindow;
|
|
808
822
|
var pct = win > 0 ? Math.min(100, (used / win) * 100) : 0;
|
|
809
823
|
var cls = contextPctClass(pct);
|
|
824
|
+
reportPaneContext({ pct: pct, used: used, win: win, cls: cls, model: contextData.model, cost: contextData.cost });
|
|
810
825
|
// Panel bar
|
|
811
826
|
contextBarFill.style.width = pct.toFixed(1) + "%";
|
|
812
827
|
contextBarFill.className = "context-bar-fill" + cls;
|
|
@@ -819,8 +834,15 @@ export function updateContextPanel() {
|
|
|
819
834
|
if (contextMiniLabel) {
|
|
820
835
|
contextMiniLabel.textContent = (win > 0 ? formatTokens(used) + "/" + formatTokens(win) : "0%");
|
|
821
836
|
}
|
|
822
|
-
// Header bar
|
|
823
|
-
|
|
837
|
+
// Header bar. A split group has no meaningful group-level context usage:
|
|
838
|
+
// the two members have separate windows, and the parent shell's own numbers
|
|
839
|
+
// are whatever it last viewed natively. Each pane header carries its own
|
|
840
|
+
// chip instead, so hide the title-bar gauge while a split is open.
|
|
841
|
+
var splitOpen = !!store.get('splitPanes');
|
|
842
|
+
var headerCtxEl = store.get('headerContextEl');
|
|
843
|
+
if (headerCtxEl) headerCtxEl.classList.toggle("hidden", splitOpen);
|
|
844
|
+
if (contextMini) contextMini.classList.toggle("split-hidden", splitOpen);
|
|
845
|
+
if (pct > 0 && !splitOpen) {
|
|
824
846
|
var statusArea = document.querySelector(".title-bar-content .status");
|
|
825
847
|
var hCtxEl = store.get('headerContextEl');
|
|
826
848
|
if (statusArea && !hCtxEl) {
|
|
@@ -32,6 +32,7 @@ import { resetRateLimitState } from './app-rate-limit.js';
|
|
|
32
32
|
import { closeSessionInfoPopover } from './app-header.js';
|
|
33
33
|
import { resetDebateState } from './debate.js';
|
|
34
34
|
import { removeDebateBottomBar } from './app-debate-ui.js';
|
|
35
|
+
import { familyOf as deriveFamily, displayProject } from './worktree-family.js';
|
|
35
36
|
|
|
36
37
|
// --- Module-owned state ---
|
|
37
38
|
var cachedProjects = [];
|
|
@@ -201,6 +202,32 @@ export function setCachedProjectCount(v) { cachedProjectCount = v; }
|
|
|
201
202
|
export function getCachedRemovedProjects() { return cachedRemovedProjects; }
|
|
202
203
|
export function setCachedRemovedProjects(v) { cachedRemovedProjects = v; }
|
|
203
204
|
|
|
205
|
+
export function familyOf(slug) { return deriveFamily(cachedProjects, slug); }
|
|
206
|
+
export function currentFamily() { return familyOf(store.get('currentSlug')); }
|
|
207
|
+
|
|
208
|
+
export function updateTitleBarProjectIdentity(fallbackName, fallbackIcon) {
|
|
209
|
+
if (store.get('dmMode') || store.get('mateProjectSlug')) {
|
|
210
|
+
var dmNameEl = document.getElementById("title-bar-project-name");
|
|
211
|
+
if (dmNameEl && fallbackName) dmNameEl.textContent = fallbackName;
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
var identity = displayProject(cachedProjects, store.get('currentSlug'));
|
|
215
|
+
var name = identity ? (identity.title || identity.project || identity.slug) : fallbackName;
|
|
216
|
+
var icon = identity ? (identity.icon || null) : (fallbackIcon || null);
|
|
217
|
+
var nameEl = document.getElementById("title-bar-project-name");
|
|
218
|
+
if (nameEl && name) nameEl.textContent = name;
|
|
219
|
+
var iconEl = document.getElementById("title-bar-project-icon");
|
|
220
|
+
if (!iconEl) return;
|
|
221
|
+
if (icon) {
|
|
222
|
+
iconEl.textContent = icon;
|
|
223
|
+
parseEmojis(iconEl);
|
|
224
|
+
iconEl.classList.add("has-icon");
|
|
225
|
+
} else {
|
|
226
|
+
iconEl.textContent = "";
|
|
227
|
+
iconEl.classList.remove("has-icon");
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
204
231
|
// --- Functions ---
|
|
205
232
|
|
|
206
233
|
export function updateProjectList(msg) {
|
|
@@ -215,6 +242,7 @@ export function updateProjectList(msg) {
|
|
|
215
242
|
_lastProjectsJson = projectsJson;
|
|
216
243
|
}
|
|
217
244
|
cachedProjects = msg.projects;
|
|
245
|
+
store.set({ projects: cachedProjects });
|
|
218
246
|
}
|
|
219
247
|
if (msg.removedProjects) cachedRemovedProjects = msg.removedProjects;
|
|
220
248
|
else if (msg.removedProjects === undefined) { /* keep cached */ }
|
|
@@ -326,31 +354,12 @@ export function renderProjectList() {
|
|
|
326
354
|
});
|
|
327
355
|
var st = store.snap();
|
|
328
356
|
var iconStripActiveSlug = (st.mateProjectSlug && st.savedMainSlug) ? st.savedMainSlug : st.currentSlug;
|
|
357
|
+
var activeFamily = deriveFamily(cachedProjects, iconStripActiveSlug);
|
|
358
|
+
if (activeFamily.parent) iconStripActiveSlug = activeFamily.parent.slug;
|
|
329
359
|
renderIconStrip(iconStripProjects, iconStripActiveSlug);
|
|
330
360
|
// Update title bar project name and icon if it changed
|
|
331
361
|
if (!st.mateProjectSlug) {
|
|
332
|
-
|
|
333
|
-
if (cachedProjects[pi].slug === st.currentSlug) {
|
|
334
|
-
var updatedName = cachedProjects[pi].title || cachedProjects[pi].project;
|
|
335
|
-
var tbName = document.getElementById("title-bar-project-name");
|
|
336
|
-
if (tbName && updatedName) tbName.textContent = updatedName;
|
|
337
|
-
var tbIcon = document.getElementById("title-bar-project-icon");
|
|
338
|
-
if (tbIcon) {
|
|
339
|
-
var pIcon = cachedProjects[pi].icon || null;
|
|
340
|
-
if (pIcon) {
|
|
341
|
-
tbIcon.textContent = pIcon;
|
|
342
|
-
parseEmojis(tbIcon);
|
|
343
|
-
tbIcon.classList.add("has-icon");
|
|
344
|
-
try { localStorage.setItem("clay-project-icon-" + (st.currentSlug || "default"), pIcon); } catch (e) {}
|
|
345
|
-
} else {
|
|
346
|
-
tbIcon.textContent = "";
|
|
347
|
-
tbIcon.classList.remove("has-icon");
|
|
348
|
-
try { localStorage.removeItem("clay-project-icon-" + (st.currentSlug || "default")); } catch (e) {}
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
break;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
362
|
+
updateTitleBarProjectIdentity(store.get('projectName'), null);
|
|
354
363
|
}
|
|
355
364
|
// Re-apply current socket status to the active icon's dot
|
|
356
365
|
var dot = getStatusDot();
|
|
@@ -557,9 +566,13 @@ function showRemoveProjectTaskDialog(slug, name, taskCount) {
|
|
|
557
566
|
export function handleRemoveProjectResult(msg) {
|
|
558
567
|
if (msg.ok) {
|
|
559
568
|
var currentSlug = store.get('currentSlug');
|
|
569
|
+
var removedProj = null;
|
|
570
|
+
for (var ri = 0; ri < cachedProjects.length; ri++) {
|
|
571
|
+
if (cachedProjects[ri].slug === msg.slug) { removedProj = cachedProjects[ri]; break; }
|
|
572
|
+
}
|
|
573
|
+
var isWorktree = removedProj ? !!removedProj.isWorktree : msg.slug.indexOf("--") !== -1;
|
|
560
574
|
if (msg.slug === currentSlug) {
|
|
561
|
-
var
|
|
562
|
-
var parentSlug = isWorktree ? msg.slug.split("--")[0] : null;
|
|
575
|
+
var parentSlug = isWorktree ? ((removedProj && removedProj.parentSlug) || msg.slug.split("--")[0]) : null;
|
|
563
576
|
|
|
564
577
|
showToast(isWorktree ? "Worktree removed" : "Project removed", "success");
|
|
565
578
|
|
|
@@ -570,10 +583,6 @@ export function handleRemoveProjectResult(msg) {
|
|
|
570
583
|
store.set({ connected: false });
|
|
571
584
|
document.getElementById("connect-overlay").classList.add("hidden");
|
|
572
585
|
if (!isWorktree) {
|
|
573
|
-
var removedProj = null;
|
|
574
|
-
for (var ri = 0; ri < cachedProjects.length; ri++) {
|
|
575
|
-
if (cachedProjects[ri].slug === msg.slug) { removedProj = cachedProjects[ri]; break; }
|
|
576
|
-
}
|
|
577
586
|
if (removedProj) {
|
|
578
587
|
cachedRemovedProjects.push({
|
|
579
588
|
path: removedProj.path || "",
|
|
@@ -585,7 +594,7 @@ export function handleRemoveProjectResult(msg) {
|
|
|
585
594
|
}
|
|
586
595
|
cachedProjects = cachedProjects.filter(function (p) { return p.slug !== msg.slug; });
|
|
587
596
|
cachedProjectCount = cachedProjects.length;
|
|
588
|
-
store.set({ currentSlug: null });
|
|
597
|
+
store.set({ currentSlug: null, projects: cachedProjects });
|
|
589
598
|
renderProjectList();
|
|
590
599
|
resetClientState();
|
|
591
600
|
|
|
@@ -595,7 +604,11 @@ export function handleRemoveProjectResult(msg) {
|
|
|
595
604
|
showHomeHub();
|
|
596
605
|
}
|
|
597
606
|
} else {
|
|
598
|
-
showToast(
|
|
607
|
+
showToast(isWorktree ? "Worktree removed" : "Project removed", "success");
|
|
608
|
+
cachedProjects = cachedProjects.filter(function (p) { return p.slug !== msg.slug; });
|
|
609
|
+
cachedProjectCount = cachedProjects.length;
|
|
610
|
+
store.set({ projects: cachedProjects });
|
|
611
|
+
renderProjectList();
|
|
599
612
|
}
|
|
600
613
|
} else {
|
|
601
614
|
showToast(msg.error || "Failed to remove project", "error");
|
|
@@ -416,12 +416,14 @@ export function finalizeAssistantBlock() {
|
|
|
416
416
|
store.set({ currentMsgEl: null, currentFullText: "" });
|
|
417
417
|
}
|
|
418
418
|
|
|
419
|
-
export function addUserMessage(text, images, pastes, fromUserId, fromUserName) {
|
|
419
|
+
export function addUserMessage(text, images, pastes, fromUserId, fromUserName, delegatedMeta) {
|
|
420
420
|
if (!text && (!images || images.length === 0) && (!pastes || pastes.length === 0)) return;
|
|
421
421
|
var myUserId = store.get('myUserId');
|
|
422
|
-
var
|
|
422
|
+
var isDelegated = !!delegatedMeta;
|
|
423
|
+
var isOtherUser = isDelegated || (fromUserId && fromUserId !== myUserId);
|
|
423
424
|
var div = document.createElement("div");
|
|
424
425
|
div.className = "msg-user" + (isOtherUser ? " msg-user-other" : "");
|
|
426
|
+
if (isDelegated) div.classList.add("msg-user-delegated");
|
|
425
427
|
div.dataset.turn = ++turnCounter;
|
|
426
428
|
if (shouldGroupMessage("msg-user")) div.classList.add("grouped");
|
|
427
429
|
var bubble = document.createElement("div");
|
|
@@ -482,7 +484,10 @@ export function addUserMessage(text, images, pastes, fromUserId, fromUserName) {
|
|
|
482
484
|
var cachedAllUsers = store.get('cachedAllUsers');
|
|
483
485
|
var _targetUser;
|
|
484
486
|
var _displayName;
|
|
485
|
-
if (
|
|
487
|
+
if (isDelegated) {
|
|
488
|
+
_targetUser = null;
|
|
489
|
+
_displayName = "from " + (delegatedMeta.delegatedByTitle || "Driver");
|
|
490
|
+
} else if (isOtherUser) {
|
|
486
491
|
_targetUser = cachedAllUsers.find(function (u) { return u.id === fromUserId; });
|
|
487
492
|
_displayName = fromUserName || (_targetUser && (_targetUser.displayName || _targetUser.username)) || "User";
|
|
488
493
|
} else {
|
|
@@ -498,7 +503,9 @@ export function addUserMessage(text, images, pastes, fromUserId, fromUserName) {
|
|
|
498
503
|
|
|
499
504
|
var avi = document.createElement("img");
|
|
500
505
|
avi.className = "dm-bubble-avatar" + (isOtherUser ? " dm-bubble-avatar-other" : " dm-bubble-avatar-me");
|
|
501
|
-
avi.src =
|
|
506
|
+
avi.src = isDelegated
|
|
507
|
+
? (VENDOR_AVATARS[delegatedMeta.delegatedByVendor] || VENDOR_AVATARS.claude)
|
|
508
|
+
: isOtherUser
|
|
502
509
|
? userAvatarUrl(_targetUser || { id: fromUserId }, 36)
|
|
503
510
|
: (document.body.dataset.myAvatarUrl || userAvatarUrl(_targetUser || { id: myUserId }, 36));
|
|
504
511
|
div.appendChild(avi);
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// Title-bar branch switcher for a project and its worktrees.
|
|
2
|
+
|
|
3
|
+
import { store } from './store.js';
|
|
4
|
+
import { iconHtml, refreshIcons } from './icons.js';
|
|
5
|
+
import { escapeHtml } from './utils.js';
|
|
6
|
+
import { familyOf } from './worktree-family.js';
|
|
7
|
+
import { switchProject, confirmRemoveProject } from './app-projects.js';
|
|
8
|
+
import { showWorktreeModal } from './sidebar-projects.js';
|
|
9
|
+
|
|
10
|
+
var chip = null;
|
|
11
|
+
var label = null;
|
|
12
|
+
var menu = null;
|
|
13
|
+
var menuOpen = false;
|
|
14
|
+
var defaultBranches = {};
|
|
15
|
+
// slug -> true/false once /api/branches answers; undefined while unknown.
|
|
16
|
+
// Non-git projects hide the chip (no worktrees to manage there).
|
|
17
|
+
var gitRepoBySlug = {};
|
|
18
|
+
|
|
19
|
+
function projectName(project) {
|
|
20
|
+
return project ? (project.title || project.project || project.name || project.slug) : "";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function currentProject(projects) {
|
|
24
|
+
var slug = store.get('currentSlug');
|
|
25
|
+
for (var i = 0; i < projects.length; i++) {
|
|
26
|
+
if (projects[i].slug === slug) return projects[i];
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function defaultBranch(parent) {
|
|
32
|
+
return defaultBranches[parent.slug] || "default";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function requestDefaultBranch(parent) {
|
|
36
|
+
if (!parent || Object.prototype.hasOwnProperty.call(defaultBranches, parent.slug)) return;
|
|
37
|
+
defaultBranches[parent.slug] = null;
|
|
38
|
+
fetch("/p/" + encodeURIComponent(parent.slug) + "/api/branches")
|
|
39
|
+
.then(function (response) { return response.json(); })
|
|
40
|
+
.then(function (data) {
|
|
41
|
+
defaultBranches[parent.slug] = data.defaultBranch || "default";
|
|
42
|
+
gitRepoBySlug[parent.slug] = data.isGitRepo !== false;
|
|
43
|
+
renderBranchSwitcher();
|
|
44
|
+
})
|
|
45
|
+
.catch(function () {
|
|
46
|
+
defaultBranches[parent.slug] = "default";
|
|
47
|
+
renderBranchSwitcher();
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function closeMenu() {
|
|
52
|
+
menuOpen = false;
|
|
53
|
+
if (menu) menu.classList.add("hidden");
|
|
54
|
+
if (chip) {
|
|
55
|
+
chip.classList.remove("open");
|
|
56
|
+
chip.setAttribute("aria-expanded", "false");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function positionMenu() {
|
|
61
|
+
if (!chip || !menu || !menuOpen) return;
|
|
62
|
+
var rect = chip.getBoundingClientRect();
|
|
63
|
+
var left = Math.min(rect.left, window.innerWidth - menu.offsetWidth - 8);
|
|
64
|
+
var top = rect.bottom + 6;
|
|
65
|
+
if (top + menu.offsetHeight > window.innerHeight - 8) top = rect.top - menu.offsetHeight - 6;
|
|
66
|
+
menu.style.left = Math.max(8, left) + "px";
|
|
67
|
+
menu.style.top = Math.max(8, top) + "px";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function branchLabel(project, parent) {
|
|
71
|
+
return project.isWorktree ? (project.branch || projectName(project)) : defaultBranch(parent);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function appendProjectRow(container, project, parent, isCurrent) {
|
|
75
|
+
var row = document.createElement("div");
|
|
76
|
+
row.className = "branch-chip-row" + (isCurrent ? " current" : "");
|
|
77
|
+
var inaccessible = project.isWorktree && project.worktreeAccessible === false;
|
|
78
|
+
var main = document.createElement("button");
|
|
79
|
+
main.className = "branch-chip-row-main";
|
|
80
|
+
main.disabled = inaccessible;
|
|
81
|
+
if (inaccessible) main.title = "Outside project path";
|
|
82
|
+
main.innerHTML = iconHtml("git-branch") + '<span class="branch-chip-row-label">' +
|
|
83
|
+
escapeHtml(branchLabel(project, parent)) + "</span>";
|
|
84
|
+
if (project.isProcessing) {
|
|
85
|
+
var dot = document.createElement("span");
|
|
86
|
+
dot.className = "branch-chip-processing";
|
|
87
|
+
main.appendChild(dot);
|
|
88
|
+
}
|
|
89
|
+
if (project.pendingPermissions > 0) {
|
|
90
|
+
var badge = document.createElement("span");
|
|
91
|
+
badge.className = "branch-chip-pending";
|
|
92
|
+
badge.textContent = project.pendingPermissions > 99 ? "99+" : String(project.pendingPermissions);
|
|
93
|
+
main.appendChild(badge);
|
|
94
|
+
}
|
|
95
|
+
if (isCurrent) main.insertAdjacentHTML("beforeend", iconHtml("check"));
|
|
96
|
+
if (!inaccessible) {
|
|
97
|
+
main.addEventListener("click", function () { closeMenu(); switchProject(project.slug); });
|
|
98
|
+
}
|
|
99
|
+
row.appendChild(main);
|
|
100
|
+
|
|
101
|
+
if (project.isWorktree && (!store.get('permissions') || store.get('permissions').deleteProject !== false)) {
|
|
102
|
+
var remove = document.createElement("button");
|
|
103
|
+
remove.className = "branch-chip-row-action";
|
|
104
|
+
remove.title = "Remove worktree";
|
|
105
|
+
remove.innerHTML = iconHtml("trash-2");
|
|
106
|
+
remove.addEventListener("click", function (event) {
|
|
107
|
+
event.stopPropagation();
|
|
108
|
+
closeMenu();
|
|
109
|
+
confirmRemoveProject(project.slug, projectName(project));
|
|
110
|
+
});
|
|
111
|
+
row.appendChild(remove);
|
|
112
|
+
}
|
|
113
|
+
container.appendChild(row);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function renderMenu(family, current) {
|
|
117
|
+
menu.innerHTML = "";
|
|
118
|
+
if (family.parent) appendProjectRow(menu, family.parent, family.parent, current.slug === family.parent.slug);
|
|
119
|
+
for (var i = 0; i < family.worktrees.length; i++) {
|
|
120
|
+
appendProjectRow(menu, family.worktrees[i], family.parent || family.worktrees[i], current.slug === family.worktrees[i].slug);
|
|
121
|
+
}
|
|
122
|
+
if (family.parent) {
|
|
123
|
+
var footer = document.createElement("button");
|
|
124
|
+
footer.className = "branch-chip-new";
|
|
125
|
+
footer.innerHTML = iconHtml("plus") + " <span>New worktree...</span>";
|
|
126
|
+
footer.addEventListener("click", function () {
|
|
127
|
+
closeMenu();
|
|
128
|
+
showWorktreeModal(family.parent.slug, projectName(family.parent));
|
|
129
|
+
});
|
|
130
|
+
menu.appendChild(footer);
|
|
131
|
+
}
|
|
132
|
+
refreshIcons();
|
|
133
|
+
positionMenu();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function renderBranchSwitcher() {
|
|
137
|
+
if (!chip) return;
|
|
138
|
+
var projects = store.get('projects') || [];
|
|
139
|
+
var current = currentProject(projects);
|
|
140
|
+
var family = current ? familyOf(projects, current.slug) : { parent: null, worktrees: [] };
|
|
141
|
+
// The chip is the project's worktree management surface, so it shows even
|
|
142
|
+
// with zero worktrees (the menu then offers the default branch and "New
|
|
143
|
+
// worktree"). Hidden where there is no project context to manage, and for
|
|
144
|
+
// non-git projects (optimistically visible until /api/branches answers).
|
|
145
|
+
var gateSlug = family.parent ? family.parent.slug : (current ? current.slug : null);
|
|
146
|
+
var hidden = !current || store.get('dmMode') || store.get('mateProjectSlug') ||
|
|
147
|
+
store.get('homeHubVisible') || (gateSlug && gitRepoBySlug[gateSlug] === false);
|
|
148
|
+
chip.classList.toggle("hidden", hidden);
|
|
149
|
+
if (hidden) { closeMenu(); return; }
|
|
150
|
+
if (family.parent) requestDefaultBranch(family.parent);
|
|
151
|
+
label.textContent = branchLabel(current, family.parent || current);
|
|
152
|
+
if (menuOpen) renderMenu(family, current);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function initBranchSwitcher() {
|
|
156
|
+
chip = document.getElementById("branch-chip");
|
|
157
|
+
label = document.getElementById("branch-chip-label");
|
|
158
|
+
menu = document.getElementById("branch-chip-menu");
|
|
159
|
+
if (!chip || !label || !menu) return;
|
|
160
|
+
chip.addEventListener("click", function (event) {
|
|
161
|
+
event.stopPropagation();
|
|
162
|
+
menuOpen = !menuOpen;
|
|
163
|
+
chip.classList.toggle("open", menuOpen);
|
|
164
|
+
chip.setAttribute("aria-expanded", menuOpen ? "true" : "false");
|
|
165
|
+
menu.classList.toggle("hidden", !menuOpen);
|
|
166
|
+
renderBranchSwitcher();
|
|
167
|
+
});
|
|
168
|
+
document.addEventListener("click", function (event) {
|
|
169
|
+
if (menuOpen && !menu.contains(event.target) && !chip.contains(event.target)) closeMenu();
|
|
170
|
+
});
|
|
171
|
+
window.addEventListener("resize", positionMenu);
|
|
172
|
+
store.subscribe(function (state, prev) {
|
|
173
|
+
if (state.projects !== prev.projects || state.currentSlug !== prev.currentSlug ||
|
|
174
|
+
state.dmMode !== prev.dmMode || state.mateProjectSlug !== prev.mateProjectSlug ||
|
|
175
|
+
state.homeHubVisible !== prev.homeHubVisible) renderBranchSwitcher();
|
|
176
|
+
});
|
|
177
|
+
renderBranchSwitcher();
|
|
178
|
+
}
|
|
@@ -16,6 +16,5 @@ export function getSessionListEl() { return ref("session-list"); }
|
|
|
16
16
|
|
|
17
17
|
export function getStatusDot() {
|
|
18
18
|
return document.querySelector("#icon-strip-projects .icon-strip-item.active .icon-strip-status") ||
|
|
19
|
-
document.querySelector("#icon-strip-projects .icon-strip-wt-item.active .icon-strip-status") ||
|
|
20
19
|
document.querySelector("#icon-strip-users .icon-strip-mate.active .icon-strip-status");
|
|
21
20
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Pane-side postMessage bridge for the split-view shell. A chrome-less pane
|
|
2
|
+
// iframe reports its context usage up to the parent (which renders a chip in
|
|
3
|
+
// the pane header) and honors panel-toggle requests coming back down.
|
|
4
|
+
// Same-origin messages only, both directions.
|
|
5
|
+
|
|
6
|
+
import { store } from './store.js';
|
|
7
|
+
import { applyContextView, getContextView } from './app-panels.js';
|
|
8
|
+
|
|
9
|
+
var panelOpen = false;
|
|
10
|
+
|
|
11
|
+
export function reportPaneContext(data) {
|
|
12
|
+
if (!store.get('paneMode') || window.parent === window) return;
|
|
13
|
+
window.parent.postMessage({
|
|
14
|
+
type: "clay-pane-context",
|
|
15
|
+
sessionId: store.get('activeSessionId'),
|
|
16
|
+
pct: data.pct,
|
|
17
|
+
used: data.used,
|
|
18
|
+
win: data.win,
|
|
19
|
+
cls: data.cls,
|
|
20
|
+
model: data.model,
|
|
21
|
+
cost: data.cost,
|
|
22
|
+
}, window.location.origin);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Toggle without setContextView: panes share localStorage with the main app,
|
|
26
|
+
// so persisting here would silently rewrite the user's main-view preference.
|
|
27
|
+
function togglePaneContextPanel() {
|
|
28
|
+
panelOpen = !panelOpen;
|
|
29
|
+
applyContextView(panelOpen ? "panel" : "off");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function initPaneBridge() {
|
|
33
|
+
if (!store.get('paneMode')) return;
|
|
34
|
+
panelOpen = getContextView() === "panel";
|
|
35
|
+
window.addEventListener("message", function (event) {
|
|
36
|
+
if (event.origin !== window.location.origin) return;
|
|
37
|
+
var msg = event.data;
|
|
38
|
+
if (!msg || msg.type !== "clay-pane-toggle-context") return;
|
|
39
|
+
togglePaneContextPanel();
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Pure resolution for the one-shot session pin used by pane-mode clients.
|
|
2
|
+
|
|
3
|
+
export function resolvePaneSession(paneMode, pinPending, pendingSessionId, sessions) {
|
|
4
|
+
if (!paneMode || !pinPending || !pendingSessionId) return { consumed: false, sessionId: null };
|
|
5
|
+
var list = sessions || [];
|
|
6
|
+
for (var i = 0; i < list.length; i++) {
|
|
7
|
+
if (list[i].id === pendingSessionId) {
|
|
8
|
+
return { consumed: true, sessionId: pendingSessionId };
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return { consumed: true, sessionId: null };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// session_switched is authoritative even when the previous session left the
|
|
15
|
+
// vendor selector locked. The lock only applies to late model metadata.
|
|
16
|
+
export function resolveSwitchedVendor(currentVendor, sessionVendor) {
|
|
17
|
+
return sessionVendor || currentVendor || "claude";
|
|
18
|
+
}
|
|
@@ -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 { switcherProjectName } from './worktree-family.js';
|
|
22
23
|
|
|
23
24
|
var _projectMru = []; // project slugs, most recent first
|
|
24
25
|
var _mateMru = []; // mate ids (mate_XXX), most recent first
|
|
@@ -263,7 +264,7 @@ function buildProjectEntries() {
|
|
|
263
264
|
var unreachable = p.isWorktree && p.worktreeAccessible === false;
|
|
264
265
|
return {
|
|
265
266
|
key: p.slug,
|
|
266
|
-
title:
|
|
267
|
+
title: switcherProjectName(projects, p),
|
|
267
268
|
icon: p.icon || null,
|
|
268
269
|
fallbackIcon: 'folder',
|
|
269
270
|
isCurrent: p.slug === store.get('currentSlug'),
|
|
@@ -38,6 +38,7 @@ import { showHomeHub } from './app-home-hub.js';
|
|
|
38
38
|
import { openTerminal } from './terminal.js';
|
|
39
39
|
import { requestKnowledgeList } from './mate-knowledge.js';
|
|
40
40
|
import { loadRootDirectory } from './filebrowser.js';
|
|
41
|
+
import { aggregateFamily, familyOf, parentProjects } from './worktree-family.js';
|
|
41
42
|
|
|
42
43
|
// --- Mobile state ---
|
|
43
44
|
var mobileChatSheetOpen = false;
|
|
@@ -147,10 +148,22 @@ function closeMobileSheet() {
|
|
|
147
148
|
}
|
|
148
149
|
|
|
149
150
|
function renderSheetProjects(listEl) {
|
|
150
|
-
|
|
151
|
+
var projects = getCachedProjectList();
|
|
152
|
+
var parents = parentProjects(projects);
|
|
153
|
+
var activeFamily = familyOf(projects, getCachedCurrentSlug());
|
|
154
|
+
var activeSlug = activeFamily.parent ? activeFamily.parent.slug : getCachedCurrentSlug();
|
|
155
|
+
|
|
156
|
+
for (var i = 0; i < parents.length; i++) {
|
|
151
157
|
(function (p) {
|
|
158
|
+
var family = familyOf(projects, p.slug);
|
|
159
|
+
var displayProject = aggregateFamily(p, family.worktrees);
|
|
160
|
+
var hasWorktreeUnread = family.worktrees.some(function (worktree) { return worktree.unread > 0; });
|
|
161
|
+
var hasWorktreePending = family.worktrees.some(function (worktree) { return worktree.pendingPermissions > 0; });
|
|
152
162
|
var el = document.createElement("button");
|
|
153
|
-
el.className = "mobile-project-item" + (p.slug ===
|
|
163
|
+
el.className = "mobile-project-item" + (p.slug === activeSlug ? " active" : "");
|
|
164
|
+
if ((displayProject.pendingPermissions > 0 && p.slug !== activeSlug) || hasWorktreePending) {
|
|
165
|
+
el.classList.add("has-pending-perm");
|
|
166
|
+
}
|
|
154
167
|
|
|
155
168
|
var abbrev = document.createElement("span");
|
|
156
169
|
abbrev.className = "mobile-project-abbrev";
|
|
@@ -167,16 +180,16 @@ function renderSheetProjects(listEl) {
|
|
|
167
180
|
name.textContent = p.name;
|
|
168
181
|
el.appendChild(name);
|
|
169
182
|
|
|
170
|
-
if (
|
|
183
|
+
if (displayProject.isProcessing) {
|
|
171
184
|
var dot = document.createElement("span");
|
|
172
185
|
dot.className = "mobile-project-processing";
|
|
173
186
|
el.appendChild(dot);
|
|
174
187
|
}
|
|
175
188
|
|
|
176
|
-
if (
|
|
189
|
+
if (displayProject.unread > 0 && (p.slug !== activeSlug || hasWorktreeUnread)) {
|
|
177
190
|
var mBadge = document.createElement("span");
|
|
178
191
|
mBadge.className = "mobile-project-unread";
|
|
179
|
-
mBadge.textContent =
|
|
192
|
+
mBadge.textContent = displayProject.unread > 99 ? "99+" : String(displayProject.unread);
|
|
180
193
|
el.appendChild(mBadge);
|
|
181
194
|
}
|
|
182
195
|
|
|
@@ -186,7 +199,7 @@ function renderSheetProjects(listEl) {
|
|
|
186
199
|
});
|
|
187
200
|
|
|
188
201
|
listEl.appendChild(el);
|
|
189
|
-
})(
|
|
202
|
+
})(parents[i]);
|
|
190
203
|
}
|
|
191
204
|
}
|
|
192
205
|
|