clay-server 2.47.0-beta.6 → 3.0.0-beta.2
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-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 +20 -0
- package/lib/public/css/menus.css +83 -1
- package/lib/public/css/messages.css +14 -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/index.html +4 -0
- package/lib/public/modules/app-connection.js +6 -0
- package/lib/public/modules/app-favicon.js +5 -0
- package/lib/public/modules/app-header.js +101 -6
- package/lib/public/modules/app-messages.js +59 -6
- package/lib/public/modules/app-panels.js +49 -27
- package/lib/public/modules/app-rendering.js +11 -4
- package/lib/public/modules/pane-bridge.js +41 -0
- package/lib/public/modules/pane-session.js +18 -0
- 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/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 +2 -2
|
@@ -19,6 +19,8 @@ import { renderKnowledgeList, handleKnowledgeContent } from './mate-knowledge.js
|
|
|
19
19
|
import { renderMemoryList } from './mate-memory.js';
|
|
20
20
|
import { handlePaletteSessionSwitch, setPaletteVersion } from './command-palette.js';
|
|
21
21
|
import { handleFindInSessionResults } from './session-search.js';
|
|
22
|
+
import { syncPaneTitles, maybeRestoreSplitGroup, openGroup } from './split-view.js';
|
|
23
|
+
import { showPairDialog, handlePairCreated, handleSplitDelegation } from './split-pair-ui.js';
|
|
22
24
|
import { handleInputSync, autoResize, builtinCommands, setScheduleBtnDisabled } from './input.js';
|
|
23
25
|
import { startThinking, appendThinking, stopThinking, resetThinkingGroup, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, closeToolGroup, removeToolFromGroup, resetToolState, getTools, getPlanContent, setPlanContent, renderPlanBanner, renderPlanCard, getTodoTools, handleTodoWrite, handleTaskCreate, handleTaskUpdate, applyDeadSessionTodoCompaction, isPlanFilePath, enableMainInput, addTurnMeta, updateSubagentActivity, addSubagentToolEntry, markSubagentDone, initSubagentStop, updateSubagentProgress, updateSubagentTaskStatus, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionCancelled, markPermissionResolved, renderElicitationRequest, markElicitationResolved, renderUserDialogRequest, markUserDialogResolved, updateThinkingTokens } from './tools.js';
|
|
24
26
|
import { showDoneNotification, playDoneSound, isNotifAlertEnabled, isNotifSoundEnabled } from './notifications.js';
|
|
@@ -46,6 +48,7 @@ import { setActivity, startUrgentBlink, stopUrgentBlink, blinkSessionDot, update
|
|
|
46
48
|
import { setStatus } from './app-connection.js';
|
|
47
49
|
import { handleWhatsNewState, handleWhatsNewSeenResult, setKnownEntries as setWhatsNewKnownEntries } from './whats-new.js';
|
|
48
50
|
import { closeArticle as closeWhatsNewArticle } from './whats-new-article.js';
|
|
51
|
+
import { resolvePaneSession, resolveSwitchedVendor } from './pane-session.js';
|
|
49
52
|
import { getModelEffortLevels, accumulateUsage, updateUsagePanel, accumulateContext, updateContextPanel, renderCtxPopover, updateStatusPanel } from './app-panels.js';
|
|
50
53
|
import { updateProjectList, updateTitleBarProjectIdentity, resetClientState, showUpdateAvailable, handleRemoveProjectCheckResult, handleRemoveProjectResult, handleBrowseDirResult, handleAddProjectResult, handleCloneProgress } from './app-projects.js';
|
|
51
54
|
import { updateHistorySentinel, prependOlderHistory } from './app-header.js';
|
|
@@ -549,9 +552,42 @@ export function processMessage(msg) {
|
|
|
549
552
|
case "session_list":
|
|
550
553
|
renderMateSessionList(msg.sessions || []);
|
|
551
554
|
renderSessionList(msg.sessions || []);
|
|
555
|
+
syncPaneTitles();
|
|
556
|
+
var panePin = resolvePaneSession(store.get('paneMode'), store.get('panePinPending'), store.get('paneSessionId'), msg.sessions);
|
|
557
|
+
if (panePin.consumed) {
|
|
558
|
+
store.set({ panePinPending: false });
|
|
559
|
+
if (panePin.sessionId && getWs() && getWs().readyState === 1) {
|
|
560
|
+
getWs().send(JSON.stringify({ type: "switch_session", id: panePin.sessionId }));
|
|
561
|
+
}
|
|
562
|
+
}
|
|
552
563
|
handlePaletteSessionSwitch();
|
|
553
564
|
break;
|
|
554
565
|
|
|
566
|
+
case "split_groups":
|
|
567
|
+
store.set({ splitGroups: msg.groups || [] });
|
|
568
|
+
renderSessionList(null);
|
|
569
|
+
maybeRestoreSplitGroup();
|
|
570
|
+
syncPaneTitles();
|
|
571
|
+
break;
|
|
572
|
+
|
|
573
|
+
case "split_group_result":
|
|
574
|
+
if (!msg.ok && msg.error) showToast(msg.error, "error");
|
|
575
|
+
break;
|
|
576
|
+
|
|
577
|
+
case "pair_session_options":
|
|
578
|
+
showPairDialog(msg);
|
|
579
|
+
break;
|
|
580
|
+
|
|
581
|
+
case "pair_session_created":
|
|
582
|
+
var createdPairGroup = handlePairCreated(msg);
|
|
583
|
+
if (createdPairGroup) openGroup(createdPairGroup);
|
|
584
|
+
break;
|
|
585
|
+
|
|
586
|
+
case "split_delegation":
|
|
587
|
+
handleSplitDelegation(msg);
|
|
588
|
+
syncPaneTitles();
|
|
589
|
+
break;
|
|
590
|
+
|
|
555
591
|
case "session_presence":
|
|
556
592
|
updateSessionPresence(msg.presence || {});
|
|
557
593
|
break;
|
|
@@ -611,11 +647,14 @@ export function processMessage(msg) {
|
|
|
611
647
|
var _effectiveTerminalId = (typeof msg.runtimeTerminalId === "number")
|
|
612
648
|
? msg.runtimeTerminalId
|
|
613
649
|
: (typeof msg.terminalId === "number" ? msg.terminalId : null);
|
|
614
|
-
store.set({ activeSessionId: msg.id, cliSessionId: msg.cliSessionId || null, vendorCapabilities: msg.capabilities || {}, sessionIsProcessing: !!msg.isProcessing, activeSessionMode: _effectiveMode, activeTerminalId: _effectiveTerminalId, sessionHasHistory: !!msg.hasHistory });
|
|
650
|
+
store.set({ activeSessionId: msg.id, cliSessionId: msg.cliSessionId || null, currentModel: msg.model || store.get('currentModel'), currentEffort: msg.effort || store.get('currentEffort'), vendorCapabilities: msg.capabilities || {}, sessionIsProcessing: !!msg.isProcessing, activeSessionMode: _effectiveMode, activeTerminalId: _effectiveTerminalId, sessionHasHistory: !!msg.hasHistory, sessionFullAccess: msg.permissionMode === "bypassPermissions", currentMode: msg.permissionMode || store.get('currentMode') });
|
|
615
651
|
// TUI sessions swap the chat UI for an embedded xterm running
|
|
616
652
|
// `claude` inside a real PTY. Mount or tear down before the rest of
|
|
617
653
|
// the chat-side bookkeeping runs so we don't waste work on hidden DOM.
|
|
618
|
-
|
|
654
|
+
// With a split open the native app is hidden behind the pane iframes,
|
|
655
|
+
// but the TUI host is position:fixed on body -- attaching it here
|
|
656
|
+
// would overlay the split. Members render inside their own panes.
|
|
657
|
+
if (_effectiveMode === "tui" && typeof _effectiveTerminalId === "number" && !store.get('splitPanes')) {
|
|
619
658
|
attachTuiView(_effectiveTerminalId, msg.id, msg.vendor || null);
|
|
620
659
|
} else {
|
|
621
660
|
detachTuiView();
|
|
@@ -624,9 +663,10 @@ export function processMessage(msg) {
|
|
|
624
663
|
// (the composer is hidden; clicking Resume spawns claude --resume).
|
|
625
664
|
setTuiSuspendedView(!!msg.tuiSuspended, msg.id);
|
|
626
665
|
if (msg.vendor) {
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
666
|
+
// session_switched is authoritative. The lock rejects stale
|
|
667
|
+
// model_info responses; it must not block an intentional switch to
|
|
668
|
+
// a blank session owned by another vendor.
|
|
669
|
+
store.set({ currentVendor: resolveSwitchedVendor(store.get('currentVendor'), msg.vendor) });
|
|
630
670
|
// Sessions with history have their vendor structurally bound to
|
|
631
671
|
// the session: lock so a late-arriving default-adapter model_info
|
|
632
672
|
// can't flip the UI back. Previously this branch unlocked, which
|
|
@@ -703,6 +743,19 @@ export function processMessage(msg) {
|
|
|
703
743
|
if (!("ontouchstart" in window)) {
|
|
704
744
|
inputEl.focus();
|
|
705
745
|
}
|
|
746
|
+
// Reload survival: a restored active session that is a split-group
|
|
747
|
+
// member reopens its group (no-op when a split is already open).
|
|
748
|
+
maybeRestoreSplitGroup();
|
|
749
|
+
break;
|
|
750
|
+
|
|
751
|
+
case "session_full_access_changed":
|
|
752
|
+
if (msg.id === store.get('activeSessionId')) {
|
|
753
|
+
store.set({ sessionFullAccess: !!msg.enabled, currentMode: msg.permissionMode || "default" });
|
|
754
|
+
}
|
|
755
|
+
break;
|
|
756
|
+
|
|
757
|
+
case "session_full_access_result":
|
|
758
|
+
if (!msg.ok && msg.error) showToast(msg.error, "error");
|
|
706
759
|
break;
|
|
707
760
|
|
|
708
761
|
case "session_id":
|
|
@@ -733,7 +786,7 @@ export function processMessage(msg) {
|
|
|
733
786
|
renderPlanCard(msg.planContent);
|
|
734
787
|
addUserMessage("Execute the following plan. Do NOT re-enter plan mode — just implement it step by step.", msg.images || null, msg.pastes || null, msg.from, msg.fromName);
|
|
735
788
|
} else {
|
|
736
|
-
addUserMessage(msg.text, msg.images || null, msg.pastes || null, msg.from, msg.fromName);
|
|
789
|
+
addUserMessage(msg.text, msg.images || null, msg.pastes || null, msg.from, msg.fromName, msg.delegated ? msg : null);
|
|
737
790
|
}
|
|
738
791
|
break;
|
|
739
792
|
|
|
@@ -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) {
|
|
@@ -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,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
|
+
}
|
|
@@ -13,6 +13,9 @@ import { showConfirm } from './app-misc.js';
|
|
|
13
13
|
import { getUpcomingSchedules } from './scheduler.js';
|
|
14
14
|
import { refreshMobileChatSheet } from './sidebar-mobile.js';
|
|
15
15
|
import { VENDOR_AVATARS, VENDOR_NAMES, VENDOR_ORDER, VENDOR_HOMEPAGES } from './app-rendering.js';
|
|
16
|
+
import { openGroup, separateGroup } from './split-view.js';
|
|
17
|
+
import { groupedSessionIds } from './split-group-helpers.js';
|
|
18
|
+
import { openPairDialog } from './split-pair-ui.js';
|
|
16
19
|
|
|
17
20
|
|
|
18
21
|
// --- Session state ---
|
|
@@ -230,6 +233,9 @@ function collectItemSessionIds(item) {
|
|
|
230
233
|
}
|
|
231
234
|
return ids;
|
|
232
235
|
}
|
|
236
|
+
if (item.type === "split-group" && Array.isArray(item.members)) {
|
|
237
|
+
return item.members.map(function (session) { return session.id; });
|
|
238
|
+
}
|
|
233
239
|
return [];
|
|
234
240
|
}
|
|
235
241
|
|
|
@@ -407,6 +413,23 @@ function showNewSessionMenu(anchorBtn, defaultVendor) {
|
|
|
407
413
|
})(VENDOR_ORDER[i]);
|
|
408
414
|
}
|
|
409
415
|
|
|
416
|
+
if (installed.length > 0) {
|
|
417
|
+
var pairSep = document.createElement("div");
|
|
418
|
+
pairSep.className = "session-ctx-sep";
|
|
419
|
+
menu.appendChild(pairSep);
|
|
420
|
+
var pairItem = document.createElement("button");
|
|
421
|
+
pairItem.className = "session-ctx-item";
|
|
422
|
+
pairItem.type = "button";
|
|
423
|
+
pairItem.innerHTML = iconHtml("panel-left-close") + " <span>Pair session…</span>";
|
|
424
|
+
pairItem.title = "Create a Driver and Worker in a split view";
|
|
425
|
+
pairItem.addEventListener("click", function (e) {
|
|
426
|
+
e.stopPropagation();
|
|
427
|
+
closeSessionCtxMenu();
|
|
428
|
+
openPairDialog();
|
|
429
|
+
});
|
|
430
|
+
menu.appendChild(pairItem);
|
|
431
|
+
}
|
|
432
|
+
|
|
410
433
|
// Claude-only launch mode: a bare TUI shell with permission prompts off.
|
|
411
434
|
// Hidden in GUI mode (the default) because the item only ever produces a
|
|
412
435
|
// terminal session, which contradicts what the rest of the menu creates.
|
|
@@ -418,7 +441,7 @@ function showNewSessionMenu(anchorBtn, defaultVendor) {
|
|
|
418
441
|
var skipItem = document.createElement("button");
|
|
419
442
|
skipItem.className = "session-ctx-item";
|
|
420
443
|
skipItem.type = "button";
|
|
421
|
-
skipItem.innerHTML = iconHtml("shield-off") + " <span>Skip
|
|
444
|
+
skipItem.innerHTML = iconHtml("shield-off") + " <span>Skip Permissions (TUI)</span>";
|
|
422
445
|
skipItem.title = "Start a terminal session with --dangerously-skip-permissions";
|
|
423
446
|
skipItem.addEventListener("click", function (e) {
|
|
424
447
|
e.stopPropagation();
|
|
@@ -590,6 +613,7 @@ export function initSidebarSessions() {
|
|
|
590
613
|
if (state.lastVendor !== prev.lastVendor || state.installedVendors !== prev.installedVendors) {
|
|
591
614
|
renderSessionList(null);
|
|
592
615
|
}
|
|
616
|
+
if (state.splitPanes !== prev.splitPanes) renderSessionList(null);
|
|
593
617
|
});
|
|
594
618
|
|
|
595
619
|
// --- Resume session picker ---
|
|
@@ -656,6 +680,7 @@ function showSessionCtxMenu(anchorBtn, sessionId, title, cliSid, sessionData) {
|
|
|
656
680
|
});
|
|
657
681
|
menu.appendChild(renameItem);
|
|
658
682
|
|
|
683
|
+
|
|
659
684
|
// Session visibility toggle (only the session owner can change)
|
|
660
685
|
if (store.get('isMultiUserMode') && sessionData && sessionData.ownerId && sessionData.ownerId === store.get('myUserId')) {
|
|
661
686
|
var currentVis = (sessionData && sessionData.sessionVisibility) || "shared";
|
|
@@ -705,6 +730,45 @@ function showSessionCtxMenu(anchorBtn, sessionId, title, cliSid, sessionData) {
|
|
|
705
730
|
});
|
|
706
731
|
}
|
|
707
732
|
|
|
733
|
+
function showSplitGroupCtxMenu(anchor, group) {
|
|
734
|
+
closeSessionCtxMenu();
|
|
735
|
+
var menu = document.createElement("div");
|
|
736
|
+
menu.className = "session-ctx-menu";
|
|
737
|
+
|
|
738
|
+
var renameItem = document.createElement("button");
|
|
739
|
+
renameItem.className = "session-ctx-item";
|
|
740
|
+
renameItem.innerHTML = iconHtml("pencil") + " <span>Rename</span>";
|
|
741
|
+
renameItem.addEventListener("click", function (e) {
|
|
742
|
+
e.stopPropagation();
|
|
743
|
+
closeSessionCtxMenu();
|
|
744
|
+
startSplitGroupInlineRename(group);
|
|
745
|
+
});
|
|
746
|
+
menu.appendChild(renameItem);
|
|
747
|
+
|
|
748
|
+
var separateItem = document.createElement("button");
|
|
749
|
+
separateItem.className = "session-ctx-item";
|
|
750
|
+
separateItem.innerHTML = iconHtml("columns-2") + " <span>Separate</span>";
|
|
751
|
+
separateItem.addEventListener("click", function (e) {
|
|
752
|
+
e.stopPropagation();
|
|
753
|
+
closeSessionCtxMenu();
|
|
754
|
+
separateGroup(group);
|
|
755
|
+
});
|
|
756
|
+
menu.appendChild(separateItem);
|
|
757
|
+
|
|
758
|
+
document.body.appendChild(menu);
|
|
759
|
+
sessionCtxMenu = menu;
|
|
760
|
+
refreshIcons();
|
|
761
|
+
requestAnimationFrame(function () {
|
|
762
|
+
var rect = anchor.getBoundingClientRect();
|
|
763
|
+
menu.style.position = "fixed";
|
|
764
|
+
menu.style.top = (rect.bottom + 2) + "px";
|
|
765
|
+
menu.style.right = (window.innerWidth - rect.right) + "px";
|
|
766
|
+
menu.style.left = "auto";
|
|
767
|
+
var menuRect = menu.getBoundingClientRect();
|
|
768
|
+
if (menuRect.bottom > window.innerHeight - 8) menu.style.top = (rect.top - menuRect.height - 2) + "px";
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
|
|
708
772
|
function showLoopCtxMenu(anchorBtn, loopId, loopName, childCount) {
|
|
709
773
|
closeSessionCtxMenu();
|
|
710
774
|
|
|
@@ -796,6 +860,40 @@ function startInlineRename(sessionId, currentTitle) {
|
|
|
796
860
|
input.addEventListener("click", function (e) { e.stopPropagation(); });
|
|
797
861
|
}
|
|
798
862
|
|
|
863
|
+
function startSplitGroupInlineRename(group) {
|
|
864
|
+
var el = getSessionListEl().querySelector('.split-group-item[data-split-group-id="' + group.id + '"]');
|
|
865
|
+
if (!el) return;
|
|
866
|
+
var textSpan = el.querySelector(".split-group-name");
|
|
867
|
+
if (!textSpan) return;
|
|
868
|
+
var input = document.createElement("input");
|
|
869
|
+
input.type = "text";
|
|
870
|
+
input.className = "session-rename-input";
|
|
871
|
+
input.value = group.name || "Split";
|
|
872
|
+
var original = textSpan.textContent;
|
|
873
|
+
textSpan.innerHTML = "";
|
|
874
|
+
textSpan.appendChild(input);
|
|
875
|
+
input.focus();
|
|
876
|
+
input.select();
|
|
877
|
+
var finished = false;
|
|
878
|
+
function finish(commit) {
|
|
879
|
+
if (finished) return;
|
|
880
|
+
finished = true;
|
|
881
|
+
var name = input.value.trim();
|
|
882
|
+
if (commit && name && name !== group.name && getWs() && store.get('connected')) {
|
|
883
|
+
getWs().send(JSON.stringify({ type: "split_group_rename", id: group.id, name: name }));
|
|
884
|
+
textSpan.textContent = name;
|
|
885
|
+
} else {
|
|
886
|
+
textSpan.textContent = original;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
input.addEventListener("keydown", function (e) {
|
|
890
|
+
if (e.key === "Enter") { e.preventDefault(); finish(true); }
|
|
891
|
+
if (e.key === "Escape") { e.preventDefault(); finish(false); }
|
|
892
|
+
});
|
|
893
|
+
input.addEventListener("blur", function () { finish(true); });
|
|
894
|
+
input.addEventListener("click", function (e) { e.stopPropagation(); });
|
|
895
|
+
}
|
|
896
|
+
|
|
799
897
|
function startLoopInlineRename(loopId, currentName) {
|
|
800
898
|
var el = getSessionListEl().querySelector('.session-loop-group[data-loop-id="' + loopId + '"]');
|
|
801
899
|
if (!el) return;
|
|
@@ -1207,6 +1305,48 @@ function renderSessionItem(s) {
|
|
|
1207
1305
|
return el;
|
|
1208
1306
|
}
|
|
1209
1307
|
|
|
1308
|
+
function renderSplitGroupItem(group, members) {
|
|
1309
|
+
var el = document.createElement("div");
|
|
1310
|
+
var split = store.get('splitPanes');
|
|
1311
|
+
el.className = "session-item split-group-item" + (split && split.groupId === group.id ? " active" : "");
|
|
1312
|
+
el.dataset.splitGroupId = group.id;
|
|
1313
|
+
|
|
1314
|
+
var text = document.createElement("span");
|
|
1315
|
+
text.className = "session-item-text split-group-text";
|
|
1316
|
+
var html = '<span class="split-group-glyph" aria-hidden="true">' + iconHtml("columns-2") + '</span><span class="split-group-vendors">';
|
|
1317
|
+
for (var i = 0; i < members.length; i++) {
|
|
1318
|
+
var vendor = members[i].vendor || "claude";
|
|
1319
|
+
html += '<img class="session-vendor-icon split-group-vendor" src="' +
|
|
1320
|
+
(VENDOR_AVATARS[vendor] || VENDOR_AVATARS.claude) + '" alt="" title="' +
|
|
1321
|
+
escapeHtml(VENDOR_NAMES[vendor] || vendor) + '">';
|
|
1322
|
+
}
|
|
1323
|
+
html += "</span>";
|
|
1324
|
+
if (members[0].isProcessing || members[1].isProcessing) html += '<span class="session-processing"></span>';
|
|
1325
|
+
html += '<span class="split-group-name">' + highlightMatch(group.name || "Split", searchQuery) + "</span>";
|
|
1326
|
+
text.innerHTML = html;
|
|
1327
|
+
el.appendChild(text);
|
|
1328
|
+
|
|
1329
|
+
var unread = (members[0].unread || 0) + (members[1].unread || 0);
|
|
1330
|
+
var badge = document.createElement("span");
|
|
1331
|
+
badge.className = "session-unread-badge" + (unread > 0 ? " has-unread" : "");
|
|
1332
|
+
badge.dataset.splitGroupId = group.id;
|
|
1333
|
+
if (unread > 0) badge.textContent = unread > 99 ? "99+" : String(unread);
|
|
1334
|
+
el.appendChild(badge);
|
|
1335
|
+
|
|
1336
|
+
el.addEventListener("click", function () {
|
|
1337
|
+
if (openGroup(group)) {
|
|
1338
|
+
dismissOverlayPanels();
|
|
1339
|
+
closeSidebar();
|
|
1340
|
+
}
|
|
1341
|
+
});
|
|
1342
|
+
el.addEventListener("contextmenu", function (e) {
|
|
1343
|
+
e.preventDefault();
|
|
1344
|
+
e.stopPropagation();
|
|
1345
|
+
showSplitGroupCtxMenu(el, group);
|
|
1346
|
+
});
|
|
1347
|
+
return el;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1210
1350
|
// --- Main session list ---
|
|
1211
1351
|
|
|
1212
1352
|
export function renderSessionList(sessions) {
|
|
@@ -1219,10 +1359,15 @@ export function renderSessionList(sessions) {
|
|
|
1219
1359
|
|
|
1220
1360
|
// Partition: loop sessions vs normal sessions
|
|
1221
1361
|
// Group by loopId + date so all runs of the same task on the same day are merged
|
|
1362
|
+
var splitGroups = store.get('splitGroups') || [];
|
|
1363
|
+
var groupedIds = groupedSessionIds(splitGroups);
|
|
1364
|
+
var sessionsById = new Map();
|
|
1365
|
+
for (var si = 0; si < cachedSessions.length; si++) sessionsById.set(cachedSessions[si].id, cachedSessions[si]);
|
|
1222
1366
|
var loopGroups = {}; // groupKey -> [sessions]
|
|
1223
1367
|
var normalSessions = [];
|
|
1224
1368
|
for (var i = 0; i < cachedSessions.length; i++) {
|
|
1225
1369
|
var s = cachedSessions[i];
|
|
1370
|
+
if (groupedIds.has(s.id)) continue;
|
|
1226
1371
|
if (s.loop && s.loop.loopId && s.loop.role === "crafting" && s.loop.source !== "ralph" && s.loop.source !== "debate") {
|
|
1227
1372
|
// Task crafting sessions live in the scheduler calendar, not the main list (except debate)
|
|
1228
1373
|
continue;
|
|
@@ -1242,6 +1387,18 @@ export function renderSessionList(sessions) {
|
|
|
1242
1387
|
for (var j = 0; j < normalSessions.length; j++) {
|
|
1243
1388
|
items.push({ type: "session", data: normalSessions[j], lastActivity: normalSessions[j].lastActivity || 0 });
|
|
1244
1389
|
}
|
|
1390
|
+
for (var sg = 0; sg < splitGroups.length; sg++) {
|
|
1391
|
+
var splitGroup = splitGroups[sg];
|
|
1392
|
+
var leftMember = sessionsById.get(splitGroup.members[0]);
|
|
1393
|
+
var rightMember = sessionsById.get(splitGroup.members[1]);
|
|
1394
|
+
if (!leftMember || !rightMember) continue;
|
|
1395
|
+
items.push({
|
|
1396
|
+
type: "split-group",
|
|
1397
|
+
group: splitGroup,
|
|
1398
|
+
members: [leftMember, rightMember],
|
|
1399
|
+
lastActivity: Math.max(leftMember.lastActivity || 0, rightMember.lastActivity || 0),
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1245
1402
|
var groupKeys = Object.keys(loopGroups);
|
|
1246
1403
|
for (var k = 0; k < groupKeys.length; k++) {
|
|
1247
1404
|
var gk = groupKeys[k];
|
|
@@ -1265,6 +1422,8 @@ export function renderSessionList(sessions) {
|
|
|
1265
1422
|
if (item.type === "session" && item.data && !isSessionVisibleBySearch(item.data.id)) {
|
|
1266
1423
|
continue;
|
|
1267
1424
|
}
|
|
1425
|
+
if (item.type === "split-group" && searchMatchIds !== null &&
|
|
1426
|
+
!searchMatchIds.has(item.members[0].id) && !searchMatchIds.has(item.members[1].id)) continue;
|
|
1268
1427
|
if (item.type === "session" && item.data && item.data.bookmarked) {
|
|
1269
1428
|
bookmarkedItems.push(item);
|
|
1270
1429
|
} else {
|
|
@@ -1320,6 +1479,8 @@ export function renderSessionList(sessions) {
|
|
|
1320
1479
|
if (loopEl) {
|
|
1321
1480
|
regularContainer.appendChild(loopEl);
|
|
1322
1481
|
}
|
|
1482
|
+
} else if (item.type === "split-group") {
|
|
1483
|
+
regularContainer.appendChild(renderSplitGroupItem(item.group, item.members));
|
|
1323
1484
|
} else {
|
|
1324
1485
|
regularContainer.appendChild(renderSessionItem(item.data));
|
|
1325
1486
|
}
|
|
@@ -1400,7 +1561,20 @@ function renderPresenceAvatars(el, sessionId) {
|
|
|
1400
1561
|
// --- Session badge ---
|
|
1401
1562
|
|
|
1402
1563
|
export function updateSessionBadge(sessionId, count) {
|
|
1564
|
+
for (var i = 0; i < cachedSessions.length; i++) {
|
|
1565
|
+
if (cachedSessions[i].id === sessionId) cachedSessions[i].unread = count;
|
|
1566
|
+
}
|
|
1403
1567
|
var badge = document.querySelector('.session-unread-badge[data-session-id="' + sessionId + '"]');
|
|
1568
|
+
var groups = store.get('splitGroups') || [];
|
|
1569
|
+
for (var gi = 0; gi < groups.length; gi++) {
|
|
1570
|
+
if (groups[gi].members.indexOf(sessionId) === -1) continue;
|
|
1571
|
+
badge = document.querySelector('.session-unread-badge[data-split-group-id="' + groups[gi].id + '"]');
|
|
1572
|
+
count = 0;
|
|
1573
|
+
for (var mi = 0; mi < cachedSessions.length; mi++) {
|
|
1574
|
+
if (groups[gi].members.indexOf(cachedSessions[mi].id) !== -1) count += cachedSessions[mi].unread || 0;
|
|
1575
|
+
}
|
|
1576
|
+
break;
|
|
1577
|
+
}
|
|
1404
1578
|
if (!badge) return;
|
|
1405
1579
|
if (count > 0) {
|
|
1406
1580
|
badge.textContent = count > 99 ? "99+" : String(count);
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
getCurrentDmUserId
|
|
13
13
|
} from './sidebar-mates.js';
|
|
14
14
|
import { initSidebarMobile } from './sidebar-mobile.js';
|
|
15
|
+
import { store } from './store.js';
|
|
15
16
|
|
|
16
17
|
var ctx;
|
|
17
18
|
var _syncResizeHandles = null;
|
|
@@ -26,6 +27,7 @@ export function dismissOverlayPanels() {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
export function updatePageTitle() {
|
|
30
|
+
if (store.get('paneMode')) return;
|
|
29
31
|
var sessionTitle = "";
|
|
30
32
|
var activeItem = ctx.sessionListEl.querySelector(".session-item.active .session-item-text");
|
|
31
33
|
if (activeItem) sessionTitle = activeItem.textContent;
|