clay-server 3.4.0-beta.12 → 3.4.0-beta.14

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 CHANGED
@@ -1041,64 +1041,6 @@ function shutdownProjects() {
1041
1041
 
1042
1042
  // Worktree tracking extracted to daemon-projects.js
1043
1043
 
1044
- // --- Register projects ---
1045
- var projects = config.projects || [];
1046
- for (var i = 0; i < projects.length; i++) {
1047
- var p = projects[i];
1048
- if (fs.existsSync(p.path)) {
1049
- console.log("[daemon] Adding project:", p.slug, "→", p.path);
1050
- relay.addProject(p.path, p.slug, p.title, p.icon, p.ownerId);
1051
- // Discover and register worktrees for this project
1052
- scanAndRegisterWorktrees(relay, p.path, p.slug, p.icon, p.ownerId);
1053
- } else {
1054
- console.log("[daemon] Skipping missing project:", p.path);
1055
- }
1056
- }
1057
-
1058
- // Migrate legacy mates storage before registration
1059
- mates.migrateLegacyMates();
1060
-
1061
- // Register existing mates as projects
1062
- if (usersModule.isMultiUser()) {
1063
- // Multi-user mode: iterate over all users and load each user's mates
1064
- var allUsers = usersModule.getAllUsers();
1065
- for (var ui = 0; ui < allUsers.length; ui++) {
1066
- var mateCtx = mates.buildMateCtx(allUsers[ui].id);
1067
- var userMates = mates.getAllMates(mateCtx);
1068
- for (var mi = 0; mi < userMates.length; mi++) {
1069
- var m = userMates[mi];
1070
- var mateDir = mates.getMateDir(mateCtx, m.id);
1071
- var mateSlug = "mate-" + m.id;
1072
- var mateName = (m.profile && m.profile.displayName) || m.name || "New Mate";
1073
- if (fs.existsSync(mateDir)) {
1074
- var mateDef = m.builtinKey ? require("./builtin-mates").getBuiltinByKey(m.builtinKey) : null;
1075
- var isHost = !!(mateDef && mateDef.hostAgent);
1076
- console.log("[daemon] Adding mate project:", mateSlug + (isHost ? " (host agent)" : ""));
1077
- relay.addProject(mateDir, mateSlug, mateName, null, m.createdBy, null, { isMate: true, mateDisplayName: mateName, isHostAgent: isHost });
1078
- }
1079
- }
1080
- }
1081
- } else {
1082
- // Single-user mode: use null ctx
1083
- var mateCtx = mates.buildMateCtx(null);
1084
- var allMates = mates.getAllMates(mateCtx);
1085
- for (var mi = 0; mi < allMates.length; mi++) {
1086
- var m = allMates[mi];
1087
- var mateDir = mates.getMateDir(mateCtx, m.id);
1088
- var mateSlug = "mate-" + m.id;
1089
- var mateName = (m.profile && m.profile.displayName) || m.name || "New Mate";
1090
- if (fs.existsSync(mateDir)) {
1091
- var mateDef2 = m.builtinKey ? require("./builtin-mates").getBuiltinByKey(m.builtinKey) : null;
1092
- var isHost2 = !!(mateDef2 && mateDef2.hostAgent);
1093
- console.log("[daemon] Adding mate project:", mateSlug + (isHost2 ? " (host agent)" : ""));
1094
- relay.addProject(mateDir, mateSlug, mateName, null, m.createdBy, null, { isMate: true, mateDisplayName: mateName, isHostAgent: isHost2 });
1095
- }
1096
- }
1097
- }
1098
-
1099
- // Sync ~/.clayrc on startup
1100
- try { syncClayrc(config.projects); } catch (e) {}
1101
-
1102
1044
  // --- IPC server ---
1103
1045
  // Clean up stale socket/config left by a previously killed daemon
1104
1046
  var existingConfig = loadConfig();
@@ -1334,6 +1276,64 @@ var ipc = createIPCServer(socketPath(), function (msg) {
1334
1276
  }
1335
1277
  });
1336
1278
 
1279
+ // --- Register projects ---
1280
+ var projects = config.projects || [];
1281
+ for (var i = 0; i < projects.length; i++) {
1282
+ var p = projects[i];
1283
+ if (fs.existsSync(p.path)) {
1284
+ console.log("[daemon] Adding project:", p.slug, "→", p.path);
1285
+ relay.addProject(p.path, p.slug, p.title, p.icon, p.ownerId);
1286
+ // Discover and register worktrees for this project
1287
+ scanAndRegisterWorktrees(relay, p.path, p.slug, p.icon, p.ownerId);
1288
+ } else {
1289
+ console.log("[daemon] Skipping missing project:", p.path);
1290
+ }
1291
+ }
1292
+
1293
+ // Migrate legacy mates storage before registration
1294
+ mates.migrateLegacyMates();
1295
+
1296
+ // Register existing mates as projects
1297
+ if (usersModule.isMultiUser()) {
1298
+ // Multi-user mode: iterate over all users and load each user's mates
1299
+ var allUsers = usersModule.getAllUsers();
1300
+ for (var ui = 0; ui < allUsers.length; ui++) {
1301
+ var mateCtx = mates.buildMateCtx(allUsers[ui].id);
1302
+ var userMates = mates.getAllMates(mateCtx);
1303
+ for (var mi = 0; mi < userMates.length; mi++) {
1304
+ var m = userMates[mi];
1305
+ var mateDir = mates.getMateDir(mateCtx, m.id);
1306
+ var mateSlug = "mate-" + m.id;
1307
+ var mateName = (m.profile && m.profile.displayName) || m.name || "New Mate";
1308
+ if (fs.existsSync(mateDir)) {
1309
+ var mateDef = m.builtinKey ? require("./builtin-mates").getBuiltinByKey(m.builtinKey) : null;
1310
+ var isHost = !!(mateDef && mateDef.hostAgent);
1311
+ console.log("[daemon] Adding mate project:", mateSlug + (isHost ? " (host agent)" : ""));
1312
+ relay.addProject(mateDir, mateSlug, mateName, null, m.createdBy, null, { isMate: true, mateDisplayName: mateName, isHostAgent: isHost });
1313
+ }
1314
+ }
1315
+ }
1316
+ } else {
1317
+ // Single-user mode: use null ctx
1318
+ var mateCtx = mates.buildMateCtx(null);
1319
+ var allMates = mates.getAllMates(mateCtx);
1320
+ for (var mi = 0; mi < allMates.length; mi++) {
1321
+ var m = allMates[mi];
1322
+ var mateDir = mates.getMateDir(mateCtx, m.id);
1323
+ var mateSlug = "mate-" + m.id;
1324
+ var mateName = (m.profile && m.profile.displayName) || m.name || "New Mate";
1325
+ if (fs.existsSync(mateDir)) {
1326
+ var mateDef2 = m.builtinKey ? require("./builtin-mates").getBuiltinByKey(m.builtinKey) : null;
1327
+ var isHost2 = !!(mateDef2 && mateDef2.hostAgent);
1328
+ console.log("[daemon] Adding mate project:", mateSlug + (isHost2 ? " (host agent)" : ""));
1329
+ relay.addProject(mateDir, mateSlug, mateName, null, m.createdBy, null, { isMate: true, mateDisplayName: mateName, isHostAgent: isHost2 });
1330
+ }
1331
+ }
1332
+ }
1333
+
1334
+ // Sync ~/.clayrc on startup
1335
+ try { syncClayrc(config.projects); } catch (e) {}
1336
+
1337
1337
  // --- Cleanup stale SDK warmup CLI session files ---
1338
1338
  //
1339
1339
  // On adapter init the SDK is given a dummy "hi" prompt to discover models,
@@ -0,0 +1,162 @@
1
+ var yoke = require("./yoke");
2
+ var contextBuilder = require("./session-handoff-context");
3
+
4
+ function hasUserContext(session) {
5
+ var history = (session && session.history) || [];
6
+ for (var i = 0; i < history.length; i++) {
7
+ if (history[i] && history[i].type === "user_message" && history[i].text) return true;
8
+ if (history[i] && history[i].type === "handoff_context" && history[i].request) return true;
9
+ }
10
+ return false;
11
+ }
12
+
13
+ function attachSessionHandoff(ctx) {
14
+ var sm = ctx.sm;
15
+
16
+ function sendResult(ws, ok, details) {
17
+ ctx.sendTo(ws, Object.assign({ type: "session_handoff_result", ok: ok }, details || {}));
18
+ }
19
+
20
+ function validate(ws, source, targetVendor) {
21
+ if (ctx.isMate) throw new Error("Session handoff is only available in projects.");
22
+ if (!source) throw new Error("No active session was found.");
23
+ if (ctx.splitStore && ctx.splitStore.groupForMember(source.localId)) {
24
+ throw new Error("Open the session by itself before continuing it in another agent.");
25
+ }
26
+ if (source.isProcessing || source._queryStarting) {
27
+ throw new Error("Wait for the current response to finish before continuing in another agent.");
28
+ }
29
+ if (!hasUserContext(source)) throw new Error("This session does not have any conversation to continue.");
30
+ if (!targetVendor) throw new Error("Choose an agent for the handoff.");
31
+ if (!ctx.adapters[targetVendor] || (sm.installedVendors || []).indexOf(targetVendor) === -1) {
32
+ throw new Error("The selected coding agent is not installed.");
33
+ }
34
+ var linuxUser = ctx.getLinuxUserForSession(source);
35
+ var vendorInfo = yoke.getVendorInfo(targetVendor);
36
+ if (linuxUser && vendorInfo && vendorInfo.osUserIsolation === false) {
37
+ throw new Error((vendorInfo.displayName || targetVendor) + " is not available for OS-isolated users.");
38
+ }
39
+ var ownerId = ws._clayUser && ctx.usersModule.isMultiUser() ? ws._clayUser.id : null;
40
+ if ((source.ownerId || null) !== ownerId) throw new Error("Session access denied.");
41
+ return linuxUser;
42
+ }
43
+
44
+ function quietRecord(session, entry) {
45
+ session.history.push(entry);
46
+ sm.appendToSessionFile(session, entry);
47
+ }
48
+
49
+ function handoff(ws, msg) {
50
+ var source = sm.sessions.get(ws._clayActiveSession) || null;
51
+ var targetVendor = typeof msg.targetVendor === "string" ? msg.targetVendor.trim() : "";
52
+ var targetModel = typeof msg.model === "string" ? msg.model.trim() : "";
53
+ var linuxUser;
54
+ try {
55
+ linuxUser = validate(ws, source, targetVendor);
56
+ } catch (err) {
57
+ sendResult(ws, false, { error: err.message || String(err) });
58
+ return;
59
+ }
60
+
61
+ var targetEffort = yoke.clampEffort(
62
+ targetVendor,
63
+ msg.effort || (sm.currentEffortByVendor && sm.currentEffortByVendor[targetVendor]) || sm.currentEffort || "medium"
64
+ ) || null;
65
+ var target = sm.createSessionRaw({
66
+ ownerId: source.ownerId || null,
67
+ sessionVisibility: source.sessionVisibility || "shared",
68
+ vendor: targetVendor,
69
+ model: targetModel || null,
70
+ effort: targetEffort,
71
+ });
72
+ var targetName = (yoke.getVendorInfo(targetVendor) || {}).displayName || targetVendor;
73
+ target.title = (source.title || "Continued session") + " · " + targetName;
74
+ target.handoff = {
75
+ sourceSessionId: source.localId,
76
+ sourceVendor: source.vendor || sm.defaultVendor || "claude",
77
+ createdAt: Date.now(),
78
+ mode: "context",
79
+ sourceHistoryIndex: source.history.length,
80
+ };
81
+
82
+ var sourceEntry = {
83
+ type: "handoff_created",
84
+ targetSessionId: target.localId,
85
+ targetVendor: targetVendor,
86
+ targetTitle: target.title,
87
+ _ts: Date.now(),
88
+ };
89
+ var targetEntry = {
90
+ type: "handoff_context",
91
+ sourceSessionId: source.localId,
92
+ sourceVendor: target.handoff.sourceVendor,
93
+ sourceTitle: source.title || "Untitled session",
94
+ targetVendor: targetVendor,
95
+ request: contextBuilder.latestUserRequest(source.history),
96
+ _ts: Date.now(),
97
+ };
98
+ quietRecord(source, sourceEntry);
99
+ quietRecord(target, targetEntry);
100
+
101
+ var prompt = contextBuilder.buildHandoffContext({
102
+ cwd: ctx.cwd,
103
+ source: source,
104
+ targetVendor: targetVendor,
105
+ });
106
+ target.isProcessing = true;
107
+ target.lastActivity = Date.now();
108
+ target.sentToolResults = {};
109
+ sm.switchSession(target.localId, ws);
110
+ if (typeof ctx.onProcessingChanged === "function") ctx.onProcessingChanged();
111
+ sendResult(ws, true, {
112
+ sourceSessionId: source.localId,
113
+ targetSessionId: target.localId,
114
+ targetVendor: targetVendor,
115
+ });
116
+
117
+ var sdk = ctx.getSdk();
118
+ function failStart(err) {
119
+ target.isProcessing = false;
120
+ sm.sendAndRecord(target, { type: "error", text: "Could not start the handoff session: " + (err.message || String(err)) });
121
+ if (typeof ctx.onProcessingChanged === "function") ctx.onProcessingChanged();
122
+ sm.broadcastSessionList();
123
+ }
124
+ if (!sdk || typeof sdk.startQuery !== "function") {
125
+ failStart(new Error("SDK bridge is not ready."));
126
+ return;
127
+ }
128
+ target._queryStartTs = Date.now();
129
+ var startPromise;
130
+ try {
131
+ startPromise = sdk.startQuery(target, prompt, undefined, linuxUser);
132
+ } catch (err) {
133
+ failStart(err);
134
+ return;
135
+ }
136
+ Promise.resolve(startPromise).catch(failStart);
137
+ }
138
+
139
+ function handleMessage(ws, msg) {
140
+ if (msg.type === "handoff_session_options") {
141
+ ctx.sendTo(ws, {
142
+ type: "handoff_session_options",
143
+ installedVendors: sm.installedVendors || [],
144
+ modelsByVendor: sm.modelsByVendor || {},
145
+ capabilitiesByVendor: sm.capabilitiesByVendor || {},
146
+ });
147
+ return true;
148
+ }
149
+ if (msg.type === "handoff_session") {
150
+ handoff(ws, msg);
151
+ return true;
152
+ }
153
+ return false;
154
+ }
155
+
156
+ return { handleMessage: handleMessage };
157
+ }
158
+
159
+ module.exports = {
160
+ attachSessionHandoff: attachSessionHandoff,
161
+ hasUserContext: hasUserContext,
162
+ };
package/lib/project.js CHANGED
@@ -36,6 +36,7 @@ var { createLocalMcp } = require("./mcp-local");
36
36
  var { attachEmail: attachEmailModule } = require("./project-email");
37
37
  var { attachSessionSpawn } = require("./project-session-spawn");
38
38
  var { attachSessionPair } = require("./project-session-pair");
39
+ var { attachSessionHandoff } = require("./project-session-handoff");
39
40
  var { attachSessionNotes, composeSystemPrompts } = require("./project-session-notes");
40
41
  var { attachSessionDocument } = require("./project-session-document");
41
42
  var { attachSplitGroups } = require("./session-split-groups");
@@ -570,6 +571,18 @@ function createProjectContext(opts) {
570
571
  getLinuxUserForSession: getLinuxUserForSession,
571
572
  getPairToolDefs: function (boundSession) { return _sessionPair.getToolDefs(boundSession); },
572
573
  });
574
+ var _sessionHandoff = attachSessionHandoff({
575
+ cwd: cwd,
576
+ sm: sm,
577
+ isMate: isMate,
578
+ splitStore: _splitGroups.store,
579
+ getSdk: function () { return sdk; },
580
+ sendTo: sendTo,
581
+ usersModule: usersModule,
582
+ adapters: adapters,
583
+ getLinuxUserForSession: getLinuxUserForSession,
584
+ onProcessingChanged: onProcessingChanged,
585
+ });
573
586
  var _debate = null;
574
587
  var _debateProposal = attachDebateProposal({
575
588
  cwd: cwd,
@@ -1101,6 +1114,7 @@ function createProjectContext(opts) {
1101
1114
 
1102
1115
  // --- Sessions, config, project mgmt (delegated to project-sessions.js) ---
1103
1116
  if (_sessionPair.handleMessage(ws, msg)) return;
1117
+ if (_sessionHandoff.handleMessage(ws, msg)) return;
1104
1118
  if (_splitGroups.handleMessage(ws, msg)) return;
1105
1119
  if (_models.handleMessage(ws, msg)) return;
1106
1120
  if (_sessions.handleSessionsMessage(ws, msg)) return;
package/lib/public/app.js CHANGED
@@ -60,6 +60,7 @@ import { initRateLimit, handleRateLimitEvent as _rlHandleRateLimitEvent, updateR
60
60
  import { initCursors, handleRemoteCursorMove as _curHandleRemoteCursorMove, handleRemoteCursorLeave as _curHandleRemoteCursorLeave, handleRemoteSelection as _curHandleRemoteSelection, clearRemoteCursors as _curClearRemoteCursors, initCursorToggle } from './modules/app-cursors.js';
61
61
  import { initFavicon, updateFavicon as _favUpdateFavicon, setSendBtnMode as _favSetSendBtnMode, blinkIO as _favBlinkIO, blinkSessionDot as _favBlinkSessionDot, updateCrossProjectBlink as _favUpdateCrossProjectBlink, startUrgentBlink as _favStartUrgentBlink, stopUrgentBlink as _favStopUrgentBlink, setActivity as _favSetActivity, drawFaviconAnimFrame as _favDrawFaviconAnimFrame } from './modules/app-favicon.js';
62
62
  import { initHeader, closeSessionInfoPopover as _hdrCloseSessionInfoPopover, updateHistorySentinel as _hdrUpdateHistorySentinel, requestMoreHistory as _hdrRequestMoreHistory, prependOlderHistory as _hdrPrependOlderHistory } from './modules/app-header.js';
63
+ import { initSessionActions } from './modules/session-actions.js';
63
64
  import { initMisc, flushPendingExtMessages, showImageModal as _miscShowImageModal, closeImageModal as _miscCloseImageModal, showPasteModal as _miscShowPasteModal, closePasteModal as _miscClosePasteModal, showConfirm as _miscShowConfirm, hideConfirm as _miscHideConfirm, showForceChangePinOverlay as _miscShowForceChangePinOverlay, sendExtensionCommand as _miscSendExtensionCommand, handleExtensionResult as _miscHandleExtensionResult } from './modules/app-misc.js';
64
65
  import { initSkillInstall, requireSkills as _siRequireSkills, requireClayMateInterview as _siRequireClayMateInterview, handleSkillInstallWs as _siHandleSkillInstallWs } from './modules/app-skills-install.js';
65
66
  import { initDebateUi, showDebateConcludeConfirm as _debShowDebateConcludeConfirm, exitDebateConcludeMode as _debExitDebateConcludeMode, handleDebateConcludeSend as _debHandleDebateConcludeSend, showDebateEndedMode as _debShowDebateEndedMode, exitDebateEndedMode as _debExitDebateEndedMode, showDebateUserFloor as _debShowDebateUserFloor, exitDebateFloorMode as _debExitDebateFloorMode, handleDebateFloorSend as _debHandleDebateFloorSend, renderDebateUserFloorDone as _debRenderDebateUserFloorDone, showDebateSticky as _debShowDebateSticky, showDebateBottomBar as _debShowDebateBottomBar, removeDebateBottomBar as _debRemoveDebateBottomBar, sendDebateStickyComment as _debSendDebateStickyComment, updateDebateRound as _debUpdateDebateRound } from './modules/app-debate-ui.js';
@@ -639,6 +640,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
639
640
 
640
641
  // --- Header module (rename, info popover, history) ---
641
642
  initHeader();
643
+ initSessionActions();
642
644
 
643
645
  // --- Skill Install module ---
644
646
  initSkillInstall();
@@ -43,62 +43,6 @@
43
43
  #header-rename-btn:hover { color: var(--text-secondary); background: rgba(var(--overlay-rgb),0.04); border-color: var(--border); }
44
44
  #header-rename-btn .lucide { width: 13px; height: 13px; }
45
45
 
46
- #header-add-worker-btn {
47
- display: inline-flex;
48
- align-items: center;
49
- justify-content: center;
50
- gap: 7px;
51
- width: auto;
52
- height: 28px;
53
- border: 1px solid color-mix(in srgb, var(--accent2) 28%, var(--border));
54
- background: var(--accent2-8);
55
- color: var(--accent2);
56
- cursor: pointer;
57
- border-radius: 8px;
58
- flex-shrink: 0;
59
- padding: 0 9px 0 7px;
60
- margin-left: 0;
61
- font-family: inherit;
62
- font-size: 11px;
63
- font-weight: 700;
64
- line-height: 1;
65
- white-space: nowrap;
66
- transition: color 0.15s, background 0.15s, border-color 0.15s;
67
- }
68
- #header-add-worker-btn:hover {
69
- color: var(--accent2-hover);
70
- background: var(--accent2-15);
71
- border-color: color-mix(in srgb, var(--accent2) 48%, var(--border));
72
- }
73
- .header-worker-icon {
74
- position: relative;
75
- display: inline-flex;
76
- align-items: center;
77
- justify-content: center;
78
- width: 16px;
79
- height: 16px;
80
- flex-shrink: 0;
81
- }
82
- .header-worker-icon > .lucide { width: 15px; height: 15px; }
83
- .header-worker-add-badge {
84
- position: absolute;
85
- right: -4px;
86
- bottom: -3px;
87
- display: inline-flex;
88
- align-items: center;
89
- justify-content: center;
90
- width: 9px;
91
- height: 9px;
92
- border-radius: 50%;
93
- background: var(--accent2);
94
- color: var(--bg);
95
- box-shadow: 0 0 0 2px var(--bg);
96
- }
97
- .header-worker-add-badge .lucide { width: 7px; height: 7px; stroke-width: 3; }
98
- .header-worker-label { letter-spacing: 0.01em; }
99
- #header-add-worker-btn.hidden { display: none; }
100
- body.dm-mode #header-add-worker-btn { display: none; }
101
-
102
46
  /* "Close terminal" button for a live TUI session. Matches the session-control
103
47
  chrome; reds out on hover since it stops the running agent while preserving
104
48
  the resumable session. */
@@ -100,7 +100,8 @@ body.pane-mode #input-area {
100
100
  }
101
101
 
102
102
  .split-pane-title {
103
- flex: 1;
103
+ flex: 0 1 auto;
104
+ max-width: 46%;
104
105
  min-width: 0;
105
106
  overflow: hidden;
106
107
  text-overflow: ellipsis;
@@ -283,12 +284,17 @@ button.split-pair-role:hover { filter: brightness(1.25); }
283
284
  color: var(--text-dimmer);
284
285
  cursor: pointer;
285
286
  flex-shrink: 0;
287
+ margin-left: auto;
286
288
  }
287
289
 
288
290
  .split-pane-context.has-data {
289
291
  display: inline-flex;
290
292
  }
291
293
 
294
+ .split-pane-context:not(.has-data) + .split-pane-close {
295
+ margin-left: auto;
296
+ }
297
+
292
298
  .split-pane-context:hover {
293
299
  background: var(--bg-alt);
294
300
  color: var(--text);
@@ -0,0 +1,98 @@
1
+ #header-session-actions-btn {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ justify-content: center;
5
+ width: 28px;
6
+ height: 28px;
7
+ padding: 0;
8
+ border: 1px solid transparent;
9
+ border-radius: 8px;
10
+ background: transparent;
11
+ color: var(--text-dimmer);
12
+ cursor: pointer;
13
+ flex-shrink: 0;
14
+ transition: color 0.15s, background 0.15s, border-color 0.15s;
15
+ }
16
+
17
+ #header-session-actions-btn:hover,
18
+ #header-session-actions-btn[aria-expanded="true"] {
19
+ color: var(--text);
20
+ background: rgba(var(--overlay-rgb), 0.05);
21
+ border-color: var(--border);
22
+ }
23
+
24
+ #header-session-actions-btn.hidden { display: none; }
25
+ #header-session-actions-btn .lucide { width: 16px; height: 16px; }
26
+
27
+ .session-actions-menu {
28
+ position: fixed;
29
+ z-index: 10020;
30
+ width: 286px;
31
+ padding: 6px;
32
+ border: 1px solid var(--border);
33
+ border-radius: 12px;
34
+ background: color-mix(in srgb, var(--bg) 96%, transparent);
35
+ box-shadow: 0 14px 38px rgba(var(--shadow-rgb), 0.32), 0 2px 8px rgba(var(--shadow-rgb), 0.2);
36
+ backdrop-filter: blur(18px);
37
+ }
38
+
39
+ .session-actions-row {
40
+ width: 100%;
41
+ border: 0;
42
+ background: transparent;
43
+ color: var(--text);
44
+ font: inherit;
45
+ text-align: left;
46
+ cursor: pointer;
47
+ }
48
+
49
+ .session-actions-row {
50
+ display: grid;
51
+ grid-template-columns: 30px minmax(0, 1fr) 16px;
52
+ align-items: center;
53
+ gap: 8px;
54
+ min-height: 54px;
55
+ padding: 7px 8px;
56
+ border-radius: 8px;
57
+ }
58
+
59
+ .session-actions-row:hover { background: var(--bg-alt); }
60
+ .session-actions-row:disabled { cursor: default; opacity: 0.42; }
61
+ .session-actions-row:disabled:hover { background: transparent; }
62
+
63
+ .session-actions-row-icon {
64
+ display: grid;
65
+ place-items: center;
66
+ width: 28px;
67
+ height: 28px;
68
+ border: 1px solid var(--border-subtle);
69
+ border-radius: 8px;
70
+ background: rgba(var(--overlay-rgb), 0.035);
71
+ color: var(--accent2);
72
+ }
73
+
74
+ .session-actions-row-icon .lucide { width: 15px; height: 15px; }
75
+ .session-actions-row-copy { display: flex; min-width: 0; flex-direction: column; gap: 2px; }
76
+ .session-actions-row-copy strong { font-size: 12px; font-weight: 750; }
77
+ .session-actions-row-copy > span { color: var(--text-dimmer); font-size: 10px; line-height: 1.25; }
78
+ .session-actions-row-chevron { color: var(--text-dimmer); }
79
+ .session-actions-row-chevron .lucide { width: 13px; height: 13px; }
80
+
81
+ .handoff-modal { width: min(420px, calc(100vw - 32px)); }
82
+ .handoff-modal-desc { margin: -10px 0 14px; color: var(--text-dimmer); font-size: 12px; line-height: 1.45; }
83
+ .handoff-modal-source {
84
+ display: flex;
85
+ align-items: center;
86
+ gap: 10px;
87
+ margin-bottom: 14px;
88
+ padding: 10px;
89
+ border: 1px solid var(--border-subtle);
90
+ border-radius: 9px;
91
+ background: var(--bg-alt);
92
+ }
93
+ .handoff-modal-source > img { width: 26px; height: 26px; object-fit: contain; border-radius: 6px; }
94
+ .handoff-modal-source > span { display: grid; min-width: 0; gap: 1px; }
95
+ .handoff-modal-source strong { overflow: hidden; color: var(--text); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
96
+ .handoff-modal-source span span { color: var(--text-dimmer); font-size: 10px; }
97
+ .handoff-modal-field + .handoff-modal-field { margin-top: 10px; }
98
+ .handoff-modal-field .wt-modal-label { margin-top: 0; }
@@ -30,7 +30,7 @@
30
30
  (function(){try{var k="clay-theme-vars",v=localStorage.getItem(k),r=document.documentElement;if(v){var o=JSON.parse(v),p;for(p in o)r.style.setProperty(p,o[p]);var vt=localStorage.getItem(k.replace("-vars","-variant"));if(vt==="light"){r.classList.add("light-theme");r.classList.remove("dark-theme")}else{r.classList.add("dark-theme");r.classList.remove("light-theme")}var m=document.querySelector('meta[name="theme-color"]');if(m&&o["--bg"])m.setAttribute("content",o["--bg"])}else{var sl=window.matchMedia&&window.matchMedia("(prefers-color-scheme: light)").matches;if(sl){r.classList.add("light-theme");r.classList.remove("dark-theme")}}}catch(e){}})();
31
31
  </script>
32
32
  <script>if(window.navigator.standalone||window.matchMedia("(display-mode:standalone)").matches){document.documentElement.classList.add("pwa-standalone")}</script>
33
- <link rel="stylesheet" href="style.css?v=20260824f">
33
+ <link rel="stylesheet" href="style.css?v=20260826a">
34
34
  <style>
35
35
  @media(max-width:768px){
36
36
  /* User messages: vertical stack, avatar on top, right-aligned */
@@ -388,12 +388,8 @@
388
388
  <div id="ralph-sticky" class="hidden"></div>
389
389
  <div id="debate-sticky" class="hidden"></div>
390
390
  <div class="status">
391
- <button id="header-add-worker-btn" type="button" aria-label="Add AI Worker" title="Add an AI Worker — this session becomes the Driver of a pair">
392
- <span class="header-worker-icon" aria-hidden="true">
393
- <i data-lucide="bot"></i>
394
- <span class="header-worker-add-badge"><i data-lucide="plus"></i></span>
395
- </span>
396
- <span class="header-worker-label">Add Worker</span>
391
+ <button id="header-session-actions-btn" type="button" aria-label="Session actions" title="Session actions" aria-haspopup="menu" aria-expanded="false">
392
+ <i data-lucide="ellipsis"></i>
397
393
  </button>
398
394
  <button type="button" id="header-tui-font-btn" class="header-tui-font-btn hidden" title="Terminal font" aria-label="Terminal font">
399
395
  <i data-lucide="type"></i>
@@ -2364,7 +2360,7 @@
2364
2360
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0/lib/addon-fit.min.js"></script>
2365
2361
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0/lib/addon-web-links.min.js"></script>
2366
2362
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-webgl@0/lib/addon-webgl.min.js"></script>
2367
- <script type="module" src="app.js?v=20260824g"></script>
2363
+ <script type="module" src="app.js?v=20260826a"></script>
2368
2364
  <div id="pwa-install-modal" class="pwa-modal hidden">
2369
2365
  <div class="pwa-modal-backdrop"></div>
2370
2366
  <div class="pwa-modal-card">
@@ -0,0 +1,69 @@
1
+ import { VENDOR_NAMES, VENDOR_ORDER, isExperimentalVendor } from './app-rendering.js';
2
+ import { effortLevelsFor, effortDisplayName } from './app-panels.js';
3
+
4
+ function optionValue(entry) {
5
+ if (typeof entry === "string") return entry;
6
+ return entry && (entry.value || entry.id) || "";
7
+ }
8
+
9
+ function optionLabel(entry) {
10
+ if (typeof entry === "string") return entry;
11
+ return entry && (entry.displayName || entry.name || entry.value || entry.id) || "";
12
+ }
13
+
14
+ export function buildAgentVendorSelect(installed, preferred) {
15
+ var select = document.createElement("select");
16
+ select.className = "wt-modal-input";
17
+ for (var i = 0; i < VENDOR_ORDER.length; i++) {
18
+ var vendor = VENDOR_ORDER[i];
19
+ if (installed.indexOf(vendor) === -1) continue;
20
+ var option = document.createElement("option");
21
+ option.value = vendor;
22
+ option.textContent = (isExperimentalVendor(vendor) ? "🧪 " : "") + (VENDOR_NAMES[vendor] || vendor);
23
+ if (isExperimentalVendor(vendor)) option.title = "Experimental integration; not yet validated through direct use testing";
24
+ select.appendChild(option);
25
+ }
26
+ if (preferred && installed.indexOf(preferred) !== -1) select.value = preferred;
27
+ return select;
28
+ }
29
+
30
+ export function fillAgentModels(select, vendor, options, preferred) {
31
+ var models = (options.modelsByVendor && options.modelsByVendor[vendor]) || [];
32
+ select.innerHTML = "";
33
+ var automatic = document.createElement("option");
34
+ automatic.value = "";
35
+ automatic.textContent = "Automatic";
36
+ select.appendChild(automatic);
37
+ for (var i = 0; i < models.length; i++) {
38
+ var option = document.createElement("option");
39
+ option.value = optionValue(models[i]);
40
+ option.textContent = optionLabel(models[i]);
41
+ select.appendChild(option);
42
+ }
43
+ select.value = preferred || "";
44
+ if (select.selectedIndex === -1) select.value = "";
45
+ }
46
+
47
+ export function buildAgentEffortSelect() {
48
+ var select = document.createElement("select");
49
+ select.className = "wt-modal-input";
50
+ return select;
51
+ }
52
+
53
+ export function fillAgentEffort(select, vendor, options, modelValue, preferred) {
54
+ var previous = preferred === undefined ? select.value : preferred;
55
+ var models = (options.modelsByVendor && options.modelsByVendor[vendor]) || [];
56
+ var levels = effortLevelsFor(vendor, models, modelValue);
57
+ select.innerHTML = "";
58
+ var automatic = document.createElement("option");
59
+ automatic.value = "";
60
+ automatic.textContent = "Default";
61
+ select.appendChild(automatic);
62
+ for (var i = 0; i < levels.length; i++) {
63
+ var option = document.createElement("option");
64
+ option.value = levels[i];
65
+ option.textContent = effortDisplayName(levels[i]);
66
+ select.appendChild(option);
67
+ }
68
+ select.value = levels.indexOf(previous) !== -1 ? previous : "";
69
+ }
@@ -12,7 +12,6 @@ import { saveToolState, resetToolState, restoreToolState } from './tools.js';
12
12
  import { getSessionUsage, setSessionUsage, getContextData, setContextData, updateContextPanel, updateUsagePanel } from './app-panels.js';
13
13
  import { onHistoryPrepended as onSessionSearchHistoryPrepended } from './session-search.js';
14
14
  import { getCachedSessions } from './sidebar-sessions.js';
15
- import { openPairDialog } from './split-pair-ui.js';
16
15
  import { showConfirm } from './app-misc.js';
17
16
 
18
17
  // --- Module-owned state ---
@@ -23,27 +22,6 @@ export function initHeader() {
23
22
  var headerRenameBtn = document.getElementById("header-rename-btn");
24
23
  var headerTitleEl = document.getElementById("header-title");
25
24
  var headerInfoBtn = document.getElementById("header-info-btn");
26
- var headerAddWorkerBtn = document.getElementById("header-add-worker-btn");
27
-
28
- // --- Add Worker: the current session becomes the Driver of a new pair ---
29
- if (headerAddWorkerBtn) {
30
- headerAddWorkerBtn.addEventListener("click", function () {
31
- var sessionId = store.get('activeSessionId');
32
- if (!sessionId || store.get('splitPanes') || store.get('dmMode')) return;
33
- openPairDialog({
34
- sessionId: sessionId,
35
- title: headerTitleEl ? headerTitleEl.textContent : "",
36
- vendor: store.get('currentVendor') || "claude",
37
- });
38
- });
39
- // A split shows the GROUP in the title bar; adding a worker there makes
40
- // no sense (the group already has two members), so hide the button.
41
- store.subscribe(function (state, prev) {
42
- if (state.splitPanes !== prev.splitPanes) {
43
- headerAddWorkerBtn.classList.toggle("hidden", !!state.splitPanes);
44
- }
45
- });
46
- }
47
25
  var headerFullAccessBtn = document.getElementById("header-full-access-btn");
48
26
 
49
27
  function updateFullAccessButton() {