clay-server 3.4.0-beta.12 → 3.4.0-beta.13
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/project-session-handoff.js +162 -0
- package/lib/project.js +14 -0
- package/lib/public/app.js +2 -0
- package/lib/public/css/menus.css +0 -56
- package/lib/public/css/pane.css +7 -1
- package/lib/public/css/session-actions.css +98 -0
- package/lib/public/index.html +4 -8
- package/lib/public/modules/agent-config-selects.js +69 -0
- package/lib/public/modules/app-header.js +0 -22
- package/lib/public/modules/app-messages.js +8 -0
- package/lib/public/modules/app-panels.js +1 -3
- package/lib/public/modules/model-picker.js +2 -15
- package/lib/public/modules/session-actions.js +294 -0
- package/lib/public/modules/split-pair-ui.js +16 -78
- package/lib/public/modules/split-view.js +3 -0
- package/lib/public/style.css +1 -0
- package/lib/session-handoff-context.js +110 -0
- package/lib/sessions.js +2 -0
- package/lib/ws-schema.js +5 -0
- package/lib/yoke/adapters/codex.js +3 -2
- package/lib/yoke/adapters/kiro.js +2 -2
- package/package.json +1 -1
|
@@ -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();
|
package/lib/public/css/menus.css
CHANGED
|
@@ -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. */
|
package/lib/public/css/pane.css
CHANGED
|
@@ -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; }
|
package/lib/public/index.html
CHANGED
|
@@ -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=
|
|
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-
|
|
392
|
-
<
|
|
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=
|
|
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() {
|
|
@@ -21,6 +21,7 @@ import { handlePaletteSessionSwitch, setPaletteVersion } from './command-palette
|
|
|
21
21
|
import { handleFindInSessionResults } from './session-search.js';
|
|
22
22
|
import { syncPaneTitles, maybeRestoreSplitGroup, openGroup } from './split-view.js';
|
|
23
23
|
import { showPairDialog, handlePairCreated, handleSplitDelegation, showWorkerDelegationNotice, hideWorkerDelegationNotice } from './split-pair-ui.js';
|
|
24
|
+
import { handleSessionActionMessage } from './session-actions.js';
|
|
24
25
|
import { renderWorkerProposal, updateWorkerProposal } from './worker-proposal.js';
|
|
25
26
|
import { handleInputSync, autoResize, builtinCommands, setScheduleBtnDisabled, sendShellResultToAgent } from './input.js';
|
|
26
27
|
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';
|
|
@@ -601,6 +602,13 @@ export function processMessage(msg) {
|
|
|
601
602
|
if (createdPairGroup) openGroup(createdPairGroup);
|
|
602
603
|
break;
|
|
603
604
|
|
|
605
|
+
case "session_handoff_result":
|
|
606
|
+
case "handoff_session_options":
|
|
607
|
+
case "handoff_context":
|
|
608
|
+
case "handoff_created":
|
|
609
|
+
handleSessionActionMessage(msg);
|
|
610
|
+
break;
|
|
611
|
+
|
|
604
612
|
case "worker_proposal":
|
|
605
613
|
renderWorkerProposal(msg);
|
|
606
614
|
break;
|
|
@@ -814,9 +814,7 @@ export function updateContextPanel() {
|
|
|
814
814
|
hCtxEl = document.createElement("div");
|
|
815
815
|
hCtxEl.className = "header-context";
|
|
816
816
|
hCtxEl.innerHTML = '<div class="header-context-bar"><div class="header-context-fill"></div></div><span class="header-context-label"></span>';
|
|
817
|
-
|
|
818
|
-
var contextAnchor = workerBtn && workerBtn.parentNode === statusArea ? workerBtn.nextSibling : statusArea.firstChild;
|
|
819
|
-
statusArea.insertBefore(hCtxEl, contextAnchor);
|
|
817
|
+
statusArea.insertBefore(hCtxEl, statusArea.firstChild);
|
|
820
818
|
hCtxEl.addEventListener("mouseenter", function() {
|
|
821
819
|
if (store.get('richContextUsage')) {
|
|
822
820
|
showCtxPopover();
|
|
@@ -238,8 +238,6 @@ export function renderModelPicker() {
|
|
|
238
238
|
return;
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
-
var caps = s.vendorCapabilities || {};
|
|
242
|
-
var locked = s.activeSessionMode === "gui" && !!s.sessionHasHistory && !caps.midSessionModelSwitch;
|
|
243
241
|
for (var i = 0; i < s.currentModels.length; i++) {
|
|
244
242
|
var item = s.currentModels[i];
|
|
245
243
|
var value = modelEntryValue(item);
|
|
@@ -247,29 +245,18 @@ export function renderModelPicker() {
|
|
|
247
245
|
var button = document.createElement("button");
|
|
248
246
|
button.className = "config-radio-item";
|
|
249
247
|
if (modelEntryMatches(item, s.currentModel)) button.classList.add("active");
|
|
250
|
-
if (locked) button.classList.add("locked");
|
|
251
248
|
button.dataset.model = value;
|
|
252
249
|
button.textContent = typeof item === "string" ? item : (item.displayName || value);
|
|
253
250
|
if (s.modelSelectionPending && s.modelSelectionPending.model === value) {
|
|
254
251
|
button.classList.add("pending");
|
|
255
252
|
button.textContent += " · Selecting…";
|
|
256
253
|
}
|
|
257
|
-
button.disabled =
|
|
258
|
-
|
|
259
|
-
button.title = "This vendor binds the model when the session starts. Start a new session to change it.";
|
|
260
|
-
} else {
|
|
261
|
-
button.addEventListener("click", function() { requestModelSelection(this.dataset.model); });
|
|
262
|
-
}
|
|
254
|
+
button.disabled = !!s.modelSelectionPending;
|
|
255
|
+
button.addEventListener("click", function() { requestModelSelection(this.dataset.model); });
|
|
263
256
|
configModelList.appendChild(button);
|
|
264
257
|
}
|
|
265
258
|
|
|
266
259
|
if (s.modelSelectionError) {
|
|
267
260
|
appendState("Model selection failed", s.modelSelectionError, false);
|
|
268
261
|
}
|
|
269
|
-
if (locked) {
|
|
270
|
-
var hint = document.createElement("div");
|
|
271
|
-
hint.className = "config-model-hint";
|
|
272
|
-
hint.textContent = "Locked after first message. Start a new session to change models.";
|
|
273
|
-
configModelList.appendChild(hint);
|
|
274
|
-
}
|
|
275
262
|
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { getWs } from './ws-ref.js';
|
|
3
|
+
import { refreshIcons, iconHtml } from './icons.js';
|
|
4
|
+
import { addSystemMessage, VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
|
|
5
|
+
import { openPairDialog } from './split-pair-ui.js';
|
|
6
|
+
import { buildAgentVendorSelect, fillAgentModels, buildAgentEffortSelect, fillAgentEffort } from './agent-config-selects.js';
|
|
7
|
+
import { showToast } from './utils.js';
|
|
8
|
+
|
|
9
|
+
var menu = null;
|
|
10
|
+
var button = null;
|
|
11
|
+
var outsideHandler = null;
|
|
12
|
+
var escapeHandler = null;
|
|
13
|
+
var handoffDialog = null;
|
|
14
|
+
var handoffEscapeHandler = null;
|
|
15
|
+
var handoffSubmitButton = null;
|
|
16
|
+
|
|
17
|
+
function closeMenu() {
|
|
18
|
+
if (menu) menu.remove();
|
|
19
|
+
menu = null;
|
|
20
|
+
if (button) button.setAttribute("aria-expanded", "false");
|
|
21
|
+
if (outsideHandler) document.removeEventListener("pointerdown", outsideHandler, true);
|
|
22
|
+
if (escapeHandler) document.removeEventListener("keydown", escapeHandler);
|
|
23
|
+
outsideHandler = null;
|
|
24
|
+
escapeHandler = null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function positionMenu() {
|
|
28
|
+
if (!menu || !button) return;
|
|
29
|
+
var rect = button.getBoundingClientRect();
|
|
30
|
+
var width = menu.offsetWidth;
|
|
31
|
+
menu.style.top = Math.round(rect.bottom + 6) + "px";
|
|
32
|
+
menu.style.left = Math.max(8, Math.round(rect.right - width)) + "px";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function actionRow(icon, label, description) {
|
|
36
|
+
var row = document.createElement("button");
|
|
37
|
+
row.type = "button";
|
|
38
|
+
row.className = "session-actions-row";
|
|
39
|
+
row.innerHTML = iconHtml(icon, "session-actions-row-icon") +
|
|
40
|
+
'<span class="session-actions-row-copy"><strong></strong><span></span></span>' +
|
|
41
|
+
iconHtml("chevron-right", "session-actions-row-chevron");
|
|
42
|
+
row.querySelector("strong").textContent = label;
|
|
43
|
+
row.querySelector(".session-actions-row-copy > span").textContent = description;
|
|
44
|
+
return row;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function menuShell(label) {
|
|
48
|
+
var el = document.createElement("div");
|
|
49
|
+
el.className = "session-actions-menu";
|
|
50
|
+
el.setAttribute("role", "menu");
|
|
51
|
+
el.setAttribute("aria-label", label);
|
|
52
|
+
document.body.appendChild(el);
|
|
53
|
+
menu = el;
|
|
54
|
+
button.setAttribute("aria-expanded", "true");
|
|
55
|
+
return el;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function installCloseHandlers() {
|
|
59
|
+
outsideHandler = function (event) {
|
|
60
|
+
if (menu && !menu.contains(event.target) && event.target !== button && !button.contains(event.target)) closeMenu();
|
|
61
|
+
};
|
|
62
|
+
escapeHandler = function (event) {
|
|
63
|
+
if (event.key === "Escape") {
|
|
64
|
+
event.preventDefault();
|
|
65
|
+
closeMenu();
|
|
66
|
+
if (button) button.focus();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
setTimeout(function () {
|
|
70
|
+
document.addEventListener("pointerdown", outsideHandler, true);
|
|
71
|
+
document.addEventListener("keydown", escapeHandler);
|
|
72
|
+
}, 0);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function closeHandoffDialog() {
|
|
76
|
+
if (handoffDialog) handoffDialog.remove();
|
|
77
|
+
if (handoffEscapeHandler) document.removeEventListener("keydown", handoffEscapeHandler);
|
|
78
|
+
handoffDialog = null;
|
|
79
|
+
handoffEscapeHandler = null;
|
|
80
|
+
handoffSubmitButton = null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function handoffField(labelText, control) {
|
|
84
|
+
var field = document.createElement("div");
|
|
85
|
+
field.className = "handoff-modal-field";
|
|
86
|
+
var label = document.createElement("label");
|
|
87
|
+
label.className = "wt-modal-label";
|
|
88
|
+
label.textContent = labelText;
|
|
89
|
+
field.appendChild(label);
|
|
90
|
+
field.appendChild(control);
|
|
91
|
+
return field;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function requestHandoffDialog() {
|
|
95
|
+
var ws = getWs();
|
|
96
|
+
if (!ws || ws.readyState !== 1) return;
|
|
97
|
+
closeMenu();
|
|
98
|
+
ws.send(JSON.stringify({ type: "handoff_session_options" }));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function showHandoffDialog(options) {
|
|
102
|
+
closeHandoffDialog();
|
|
103
|
+
var installed = options.installedVendors || [];
|
|
104
|
+
var vendorInfo = store.get('vendorInfo') || {};
|
|
105
|
+
if (store.get('isOsUsers')) {
|
|
106
|
+
installed = installed.filter(function (vendor) {
|
|
107
|
+
return !vendorInfo[vendor] || vendorInfo[vendor].osUserIsolation !== false;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
if (installed.length < 1) {
|
|
111
|
+
showToast("Install a coding agent before continuing this session.", "error");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
var currentVendor = store.get('currentVendor') || "claude";
|
|
116
|
+
var currentModel = store.get('currentModel') || "";
|
|
117
|
+
var currentEffort = store.get('currentEffort') || "";
|
|
118
|
+
var container = document.createElement("div");
|
|
119
|
+
var overlay = document.createElement("div");
|
|
120
|
+
overlay.className = "wt-modal-overlay";
|
|
121
|
+
container.appendChild(overlay);
|
|
122
|
+
var modal = document.createElement("div");
|
|
123
|
+
modal.className = "wt-modal handoff-modal";
|
|
124
|
+
modal.setAttribute("role", "dialog");
|
|
125
|
+
modal.setAttribute("aria-modal", "true");
|
|
126
|
+
modal.setAttribute("aria-labelledby", "handoff-modal-title");
|
|
127
|
+
|
|
128
|
+
var title = document.createElement("div");
|
|
129
|
+
title.id = "handoff-modal-title";
|
|
130
|
+
title.className = "wt-modal-title";
|
|
131
|
+
title.textContent = "Continue in a new agent";
|
|
132
|
+
modal.appendChild(title);
|
|
133
|
+
var desc = document.createElement("div");
|
|
134
|
+
desc.className = "handoff-modal-desc";
|
|
135
|
+
desc.textContent = "Start an independent session with a focused copy of this conversation's context.";
|
|
136
|
+
modal.appendChild(desc);
|
|
137
|
+
|
|
138
|
+
var source = document.createElement("div");
|
|
139
|
+
source.className = "handoff-modal-source";
|
|
140
|
+
var sourceAvatar = document.createElement("img");
|
|
141
|
+
sourceAvatar.src = VENDOR_AVATARS[currentVendor] || VENDOR_AVATARS.claude;
|
|
142
|
+
sourceAvatar.alt = "";
|
|
143
|
+
source.appendChild(sourceAvatar);
|
|
144
|
+
var sourceCopy = document.createElement("span");
|
|
145
|
+
var sourceTitle = document.createElement("strong");
|
|
146
|
+
sourceTitle.textContent = document.getElementById("header-title").textContent || "Current session";
|
|
147
|
+
var sourceMeta = document.createElement("span");
|
|
148
|
+
sourceMeta.textContent = "From " + (VENDOR_NAMES[currentVendor] || currentVendor);
|
|
149
|
+
sourceCopy.appendChild(sourceTitle);
|
|
150
|
+
sourceCopy.appendChild(sourceMeta);
|
|
151
|
+
source.appendChild(sourceCopy);
|
|
152
|
+
modal.appendChild(source);
|
|
153
|
+
|
|
154
|
+
var vendorSelect = buildAgentVendorSelect(installed, currentVendor);
|
|
155
|
+
var modelSelect = document.createElement("select");
|
|
156
|
+
modelSelect.className = "wt-modal-input";
|
|
157
|
+
var effortSelect = buildAgentEffortSelect();
|
|
158
|
+
var vendorField = handoffField("Agent", vendorSelect);
|
|
159
|
+
var modelField = handoffField("Model", modelSelect);
|
|
160
|
+
var effortField = handoffField("Reasoning effort", effortSelect);
|
|
161
|
+
modal.appendChild(vendorField);
|
|
162
|
+
modal.appendChild(modelField);
|
|
163
|
+
modal.appendChild(effortField);
|
|
164
|
+
|
|
165
|
+
function syncEffort(preferred) {
|
|
166
|
+
fillAgentEffort(effortSelect, vendorSelect.value, options, modelSelect.value, preferred);
|
|
167
|
+
var capabilities = (options.capabilitiesByVendor && options.capabilitiesByVendor[vendorSelect.value]) || {};
|
|
168
|
+
effortField.style.display = capabilities.effort === false ? "none" : "";
|
|
169
|
+
}
|
|
170
|
+
fillAgentModels(modelSelect, vendorSelect.value, options, vendorSelect.value === currentVendor ? currentModel : "");
|
|
171
|
+
syncEffort(vendorSelect.value === currentVendor ? currentEffort : "");
|
|
172
|
+
vendorSelect.addEventListener("change", function () {
|
|
173
|
+
fillAgentModels(modelSelect, vendorSelect.value, options, "");
|
|
174
|
+
syncEffort("");
|
|
175
|
+
});
|
|
176
|
+
modelSelect.addEventListener("change", function () { syncEffort(effortSelect.value); });
|
|
177
|
+
|
|
178
|
+
var actions = document.createElement("div");
|
|
179
|
+
actions.className = "wt-modal-actions";
|
|
180
|
+
var cancel = document.createElement("button");
|
|
181
|
+
cancel.type = "button";
|
|
182
|
+
cancel.className = "wt-modal-btn";
|
|
183
|
+
cancel.textContent = "Cancel";
|
|
184
|
+
var submit = document.createElement("button");
|
|
185
|
+
submit.type = "button";
|
|
186
|
+
submit.className = "wt-modal-btn primary";
|
|
187
|
+
submit.textContent = "Continue";
|
|
188
|
+
actions.appendChild(cancel);
|
|
189
|
+
actions.appendChild(submit);
|
|
190
|
+
modal.appendChild(actions);
|
|
191
|
+
container.appendChild(modal);
|
|
192
|
+
document.body.appendChild(container);
|
|
193
|
+
handoffDialog = container;
|
|
194
|
+
handoffSubmitButton = submit;
|
|
195
|
+
|
|
196
|
+
cancel.addEventListener("click", closeHandoffDialog);
|
|
197
|
+
overlay.addEventListener("click", closeHandoffDialog);
|
|
198
|
+
handoffEscapeHandler = function (event) {
|
|
199
|
+
if (event.key === "Escape") closeHandoffDialog();
|
|
200
|
+
};
|
|
201
|
+
document.addEventListener("keydown", handoffEscapeHandler);
|
|
202
|
+
submit.addEventListener("click", function () {
|
|
203
|
+
var ws = getWs();
|
|
204
|
+
if (!ws || ws.readyState !== 1) return;
|
|
205
|
+
ws.send(JSON.stringify({
|
|
206
|
+
type: "handoff_session",
|
|
207
|
+
targetVendor: vendorSelect.value,
|
|
208
|
+
model: modelSelect.value,
|
|
209
|
+
effort: effortSelect.value,
|
|
210
|
+
}));
|
|
211
|
+
submit.disabled = true;
|
|
212
|
+
submit.textContent = "Continuing…";
|
|
213
|
+
});
|
|
214
|
+
vendorSelect.focus();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function showMainMenu() {
|
|
218
|
+
closeMenu();
|
|
219
|
+
var el = menuShell("Session actions");
|
|
220
|
+
var addWorker = actionRow("bot", "Add AI Worker", "Open a second agent beside this session.");
|
|
221
|
+
addWorker.addEventListener("click", function () {
|
|
222
|
+
var sessionId = store.get('activeSessionId');
|
|
223
|
+
if (!sessionId) return;
|
|
224
|
+
closeMenu();
|
|
225
|
+
openPairDialog({
|
|
226
|
+
sessionId: sessionId,
|
|
227
|
+
title: document.getElementById("header-title").textContent,
|
|
228
|
+
vendor: store.get('currentVendor') || "claude",
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
var handoff = actionRow("forward", "Continue in another agent", "Carry this conversation into a new session.");
|
|
232
|
+
var unavailable = !store.get('sessionHasHistory') || store.get('sessionIsProcessing');
|
|
233
|
+
handoff.disabled = unavailable;
|
|
234
|
+
handoff.title = unavailable ? "Available after the current turn is complete" : "";
|
|
235
|
+
handoff.addEventListener("click", requestHandoffDialog);
|
|
236
|
+
el.appendChild(addWorker);
|
|
237
|
+
el.appendChild(handoff);
|
|
238
|
+
refreshIcons();
|
|
239
|
+
positionMenu();
|
|
240
|
+
installCloseHandlers();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function updateVisibility() {
|
|
244
|
+
if (!button) return;
|
|
245
|
+
var state = store.snap();
|
|
246
|
+
var hidden = !state.activeSessionId || state.dmMode || state.paneMode || !!state.splitPanes || state.activeSessionMode !== "gui";
|
|
247
|
+
button.classList.toggle("hidden", hidden);
|
|
248
|
+
if (hidden) closeMenu();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function initSessionActions() {
|
|
252
|
+
button = document.getElementById("header-session-actions-btn");
|
|
253
|
+
if (!button) return;
|
|
254
|
+
button.addEventListener("click", function () {
|
|
255
|
+
if (menu) closeMenu();
|
|
256
|
+
else showMainMenu();
|
|
257
|
+
});
|
|
258
|
+
store.subscribe(function (state, prev) {
|
|
259
|
+
if (state.activeSessionId !== prev.activeSessionId || state.dmMode !== prev.dmMode ||
|
|
260
|
+
state.paneMode !== prev.paneMode || state.splitPanes !== prev.splitPanes ||
|
|
261
|
+
state.activeSessionMode !== prev.activeSessionMode) updateVisibility();
|
|
262
|
+
});
|
|
263
|
+
window.addEventListener("resize", closeMenu);
|
|
264
|
+
updateVisibility();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function handleSessionActionMessage(msg) {
|
|
268
|
+
if (msg.type === "handoff_session_options") {
|
|
269
|
+
showHandoffDialog(msg);
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
if (msg.type === "session_handoff_result") {
|
|
273
|
+
if (msg.ok) closeHandoffDialog();
|
|
274
|
+
else {
|
|
275
|
+
showToast(msg.error || "Could not continue the session in another agent.", "error");
|
|
276
|
+
if (handoffSubmitButton) {
|
|
277
|
+
handoffSubmitButton.disabled = false;
|
|
278
|
+
handoffSubmitButton.textContent = "Continue";
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
if (msg.type === "handoff_context") {
|
|
284
|
+
var sourceName = VENDOR_NAMES[msg.sourceVendor] || msg.sourceVendor || "another agent";
|
|
285
|
+
addSystemMessage("Continued from “" + (msg.sourceTitle || "Untitled session") + "” in " + sourceName + ".", false);
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
if (msg.type === "handoff_created") {
|
|
289
|
+
var targetName = VENDOR_NAMES[msg.targetVendor] || msg.targetVendor || "another agent";
|
|
290
|
+
addSystemMessage("Continued in a new " + targetName + " session.", false);
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { store } from './store.js';
|
|
2
2
|
import { getWs } from './ws-ref.js';
|
|
3
|
-
import { VENDOR_AVATARS
|
|
4
|
-
import {
|
|
3
|
+
import { VENDOR_AVATARS } from './app-rendering.js';
|
|
4
|
+
import { buildAgentVendorSelect, fillAgentModels, buildAgentEffortSelect, fillAgentEffort } from './agent-config-selects.js';
|
|
5
5
|
import { showToast } from './utils.js';
|
|
6
6
|
|
|
7
7
|
var pairDialog = null;
|
|
@@ -9,78 +9,14 @@ var pairDialog = null;
|
|
|
9
9
|
// that session becomes the Driver and the dialog only configures the Worker.
|
|
10
10
|
var pendingDriver = null;
|
|
11
11
|
|
|
12
|
-
function optionValue(entry) {
|
|
13
|
-
if (typeof entry === "string") return entry;
|
|
14
|
-
return entry && (entry.value || entry.id) || "";
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function optionLabel(entry) {
|
|
18
|
-
if (typeof entry === "string") return entry;
|
|
19
|
-
return entry && (entry.displayName || entry.name || entry.value || entry.id) || "";
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function fillModels(select, vendor, options) {
|
|
23
|
-
var models = (options.modelsByVendor && options.modelsByVendor[vendor]) || [];
|
|
24
|
-
select.innerHTML = "";
|
|
25
|
-
var automatic = document.createElement("option");
|
|
26
|
-
automatic.value = "";
|
|
27
|
-
automatic.textContent = "Automatic";
|
|
28
|
-
select.appendChild(automatic);
|
|
29
|
-
for (var i = 0; i < models.length; i++) {
|
|
30
|
-
var option = document.createElement("option");
|
|
31
|
-
option.value = optionValue(models[i]);
|
|
32
|
-
option.textContent = optionLabel(models[i]);
|
|
33
|
-
select.appendChild(option);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
12
|
function closePairDialog() {
|
|
38
13
|
if (pairDialog) pairDialog.remove();
|
|
39
14
|
pairDialog = null;
|
|
40
15
|
}
|
|
41
16
|
|
|
42
|
-
function buildVendorSelect(installed, preferred) {
|
|
43
|
-
var select = document.createElement("select");
|
|
44
|
-
select.className = "wt-modal-input";
|
|
45
|
-
for (var i = 0; i < VENDOR_ORDER.length; i++) {
|
|
46
|
-
if (installed.indexOf(VENDOR_ORDER[i]) === -1) continue;
|
|
47
|
-
var option = document.createElement("option");
|
|
48
|
-
option.value = VENDOR_ORDER[i];
|
|
49
|
-
option.textContent = (isExperimentalVendor(VENDOR_ORDER[i]) ? "🧪 " : "") + (VENDOR_NAMES[VENDOR_ORDER[i]] || VENDOR_ORDER[i]);
|
|
50
|
-
if (isExperimentalVendor(VENDOR_ORDER[i])) option.title = "Experimental integration; not yet validated through direct use testing";
|
|
51
|
-
select.appendChild(option);
|
|
52
|
-
}
|
|
53
|
-
if (preferred && installed.indexOf(preferred) !== -1) select.value = preferred;
|
|
54
|
-
return select;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
17
|
// Effort levels are vendor- and model-specific (codex: minimal..xhigh,
|
|
58
18
|
// claude/kiro: low..max, plus per-model supportedEffortLevels overrides), so
|
|
59
19
|
// options are rebuilt whenever the role's vendor or model changes.
|
|
60
|
-
function fillEffortOptions(select, vendor, options, modelValue) {
|
|
61
|
-
var previous = select.value;
|
|
62
|
-
var models = (options.modelsByVendor && options.modelsByVendor[vendor]) || [];
|
|
63
|
-
var levels = effortLevelsFor(vendor, models, modelValue);
|
|
64
|
-
select.innerHTML = "";
|
|
65
|
-
var automatic = document.createElement("option");
|
|
66
|
-
automatic.value = "";
|
|
67
|
-
automatic.textContent = "Default";
|
|
68
|
-
select.appendChild(automatic);
|
|
69
|
-
for (var i = 0; i < levels.length; i++) {
|
|
70
|
-
var option = document.createElement("option");
|
|
71
|
-
option.value = levels[i];
|
|
72
|
-
option.textContent = effortDisplayName(levels[i]);
|
|
73
|
-
select.appendChild(option);
|
|
74
|
-
}
|
|
75
|
-
select.value = levels.indexOf(previous) !== -1 ? previous : "";
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function buildEffortSelect() {
|
|
79
|
-
var select = document.createElement("select");
|
|
80
|
-
select.className = "wt-modal-input";
|
|
81
|
-
return select;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
20
|
function fieldLabel(text, control) {
|
|
85
21
|
var label = document.createElement("label");
|
|
86
22
|
label.className = "wt-modal-label";
|
|
@@ -155,25 +91,25 @@ export function showPairDialog(options) {
|
|
|
155
91
|
: "One Driver plans and directs a visible Worker session.";
|
|
156
92
|
modal.appendChild(desc);
|
|
157
93
|
|
|
158
|
-
var driverVendor =
|
|
94
|
+
var driverVendor = buildAgentVendorSelect(installed, installed.indexOf("claude") !== -1 ? "claude" : installed[0]);
|
|
159
95
|
var workerDefault = options.lastVendor && options.lastVendor !== driverVendor.value ? options.lastVendor : (installed.indexOf("codex") !== -1 ? "codex" : installed[0]);
|
|
160
|
-
var workerVendor =
|
|
96
|
+
var workerVendor = buildAgentVendorSelect(installed, workerDefault);
|
|
161
97
|
var driverModel = document.createElement("select");
|
|
162
98
|
driverModel.className = "wt-modal-input";
|
|
163
99
|
var workerModel = document.createElement("select");
|
|
164
100
|
workerModel.className = "wt-modal-input";
|
|
165
|
-
var driverEffort =
|
|
166
|
-
var workerEffort =
|
|
167
|
-
function refreshDriverEffort() {
|
|
168
|
-
function refreshWorkerEffort() {
|
|
169
|
-
|
|
170
|
-
|
|
101
|
+
var driverEffort = buildAgentEffortSelect();
|
|
102
|
+
var workerEffort = buildAgentEffortSelect();
|
|
103
|
+
function refreshDriverEffort() { fillAgentEffort(driverEffort, driverVendor.value, options, driverModel.value); }
|
|
104
|
+
function refreshWorkerEffort() { fillAgentEffort(workerEffort, workerVendor.value, options, workerModel.value); }
|
|
105
|
+
fillAgentModels(driverModel, driverVendor.value, options);
|
|
106
|
+
fillAgentModels(workerModel, workerVendor.value, options);
|
|
171
107
|
refreshDriverEffort();
|
|
172
108
|
refreshWorkerEffort();
|
|
173
109
|
workerEffort.value = "medium";
|
|
174
110
|
if (workerEffort.value !== "medium") workerEffort.value = "";
|
|
175
|
-
driverVendor.addEventListener("change", function () {
|
|
176
|
-
workerVendor.addEventListener("change", function () {
|
|
111
|
+
driverVendor.addEventListener("change", function () { fillAgentModels(driverModel, driverVendor.value, options); refreshDriverEffort(); });
|
|
112
|
+
workerVendor.addEventListener("change", function () { fillAgentModels(workerModel, workerVendor.value, options); refreshWorkerEffort(); });
|
|
177
113
|
driverModel.addEventListener("change", refreshDriverEffort);
|
|
178
114
|
workerModel.addEventListener("change", refreshWorkerEffort);
|
|
179
115
|
|
|
@@ -319,7 +255,8 @@ export function syncPairChrome(host, split) {
|
|
|
319
255
|
setBtn.title = "Make this session the Driver; the other pane becomes its Worker";
|
|
320
256
|
setBtn.addEventListener("click", function () { sendSetPair(group.id, paneSessionId); });
|
|
321
257
|
var titleEl = header.querySelector(".split-pane-title");
|
|
322
|
-
|
|
258
|
+
var accessEl = header.querySelector(".split-pane-full-access");
|
|
259
|
+
header.insertBefore(setBtn, accessEl ? accessEl.nextSibling : (titleEl ? titleEl.nextSibling : null));
|
|
323
260
|
})(split.panes[ai].sessionId, headers[ai]);
|
|
324
261
|
}
|
|
325
262
|
return;
|
|
@@ -339,7 +276,8 @@ export function syncPairChrome(host, split) {
|
|
|
339
276
|
sendSetPair(group.id, isDriver ? null : sessionId);
|
|
340
277
|
});
|
|
341
278
|
var title = header.querySelector(".split-pane-title");
|
|
342
|
-
|
|
279
|
+
var access = header.querySelector(".split-pane-full-access");
|
|
280
|
+
header.insertBefore(badge, access ? access.nextSibling : (title ? title.nextSibling : null));
|
|
343
281
|
})(split.panes[pi].sessionId, headers[pi]);
|
|
344
282
|
}
|
|
345
283
|
var active = (store.get('splitDelegations') || {})[group.id];
|
|
@@ -231,6 +231,9 @@ function createPane(pane, index) {
|
|
|
231
231
|
}, "Skip Permissions", true);
|
|
232
232
|
});
|
|
233
233
|
header.appendChild(fullAccess);
|
|
234
|
+
// Permission state belongs to the session identity on the left. Keep it
|
|
235
|
+
// immediately after the title instead of grouping it with usage and close.
|
|
236
|
+
header.insertBefore(fullAccess, ctxChip);
|
|
234
237
|
|
|
235
238
|
var close = document.createElement("button");
|
|
236
239
|
close.type = "button";
|
package/lib/public/style.css
CHANGED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
var spawnSync = require("child_process").spawnSync;
|
|
2
|
+
var yoke = require("./yoke");
|
|
3
|
+
|
|
4
|
+
var MAX_CONTEXT_CHARS = 36000;
|
|
5
|
+
var MAX_TURNS = 12;
|
|
6
|
+
var MAX_USER_CHARS = 5000;
|
|
7
|
+
var MAX_ASSISTANT_CHARS = 9000;
|
|
8
|
+
var MAX_GIT_CHARS = 5000;
|
|
9
|
+
var MAX_TRANSCRIPT_CHARS = 24000;
|
|
10
|
+
|
|
11
|
+
function trimText(value, limit) {
|
|
12
|
+
var text = typeof value === "string" ? value.trim() : "";
|
|
13
|
+
if (text.length <= limit) return text;
|
|
14
|
+
return text.slice(0, limit) + "\n[truncated]";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function recentTurns(history) {
|
|
18
|
+
var turns = [];
|
|
19
|
+
var current = null;
|
|
20
|
+
history = Array.isArray(history) ? history : [];
|
|
21
|
+
for (var i = 0; i < history.length; i++) {
|
|
22
|
+
var entry = history[i];
|
|
23
|
+
if (!entry) continue;
|
|
24
|
+
if (entry.type === "user_message" || (entry.type === "handoff_context" && entry.request)) {
|
|
25
|
+
current = { user: trimText(entry.text || entry.request, MAX_USER_CHARS), assistant: "" };
|
|
26
|
+
turns.push(current);
|
|
27
|
+
} else if (entry.type === "delta" && entry.text) {
|
|
28
|
+
if (!current) {
|
|
29
|
+
current = { user: "", assistant: "" };
|
|
30
|
+
turns.push(current);
|
|
31
|
+
}
|
|
32
|
+
current.assistant += entry.text;
|
|
33
|
+
if (current.assistant.length > MAX_ASSISTANT_CHARS) {
|
|
34
|
+
current.assistant = current.assistant.slice(-MAX_ASSISTANT_CHARS);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return turns.slice(-MAX_TURNS);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function latestUserRequest(history) {
|
|
42
|
+
var turns = recentTurns(history);
|
|
43
|
+
for (var i = turns.length - 1; i >= 0; i--) {
|
|
44
|
+
if (turns[i].user) return turns[i].user;
|
|
45
|
+
}
|
|
46
|
+
return "";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function gitCommand(cwd, args) {
|
|
50
|
+
var result = spawnSync("git", args, {
|
|
51
|
+
cwd: cwd,
|
|
52
|
+
encoding: "utf8",
|
|
53
|
+
timeout: 5000,
|
|
54
|
+
maxBuffer: 1024 * 1024,
|
|
55
|
+
});
|
|
56
|
+
if (result.error || result.status !== 0) return "";
|
|
57
|
+
return trimText(result.stdout, MAX_GIT_CHARS);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function repositoryState(cwd) {
|
|
61
|
+
var status = gitCommand(cwd, ["status", "--short", "--branch"]);
|
|
62
|
+
var lines = [];
|
|
63
|
+
lines.push("Working tree:\n" + (status || "clean"));
|
|
64
|
+
return lines.join("\n\n");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function transcriptText(turns) {
|
|
68
|
+
var sections = [];
|
|
69
|
+
for (var i = 0; i < turns.length; i++) {
|
|
70
|
+
var turn = turns[i];
|
|
71
|
+
var parts = ["Turn " + (i + 1)];
|
|
72
|
+
if (turn.user) parts.push("USER:\n" + turn.user);
|
|
73
|
+
if (turn.assistant) parts.push("ASSISTANT:\n" + trimText(turn.assistant, MAX_ASSISTANT_CHARS));
|
|
74
|
+
sections.push(parts.join("\n\n"));
|
|
75
|
+
}
|
|
76
|
+
while (sections.length > 1 && sections.join("\n\n---\n\n").length > MAX_TRANSCRIPT_CHARS) {
|
|
77
|
+
sections.shift();
|
|
78
|
+
}
|
|
79
|
+
return sections.join("\n\n---\n\n");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function buildHandoffContext(options) {
|
|
83
|
+
var source = options.source;
|
|
84
|
+
var targetVendor = options.targetVendor;
|
|
85
|
+
var sourceVendor = source.vendor || "claude";
|
|
86
|
+
var sourceName = (yoke.getVendorInfo(sourceVendor) || {}).displayName || sourceVendor;
|
|
87
|
+
var targetName = (yoke.getVendorInfo(targetVendor) || {}).displayName || targetVendor;
|
|
88
|
+
var turns = recentTurns(source.history);
|
|
89
|
+
var latestUser = latestUserRequest(source.history);
|
|
90
|
+
var parts = [
|
|
91
|
+
"[Clay session handoff]",
|
|
92
|
+
"You are continuing work from another Clay coding-agent session. This is a snapshot, not a native conversation resume. Verify the current filesystem state before acting.",
|
|
93
|
+
"Source agent: " + sourceName,
|
|
94
|
+
"Target agent: " + targetName,
|
|
95
|
+
"Source session: " + (source.title || "Untitled session") + " (#" + source.localId + ")",
|
|
96
|
+
];
|
|
97
|
+
if (latestUser) parts.push("Current user request, verbatim:\n" + latestUser);
|
|
98
|
+
parts.push("Repository state at handoff:\n" + repositoryState(options.cwd));
|
|
99
|
+
parts.push("Recent conversation:\n" + transcriptText(turns));
|
|
100
|
+
parts.push("Continue from the unresolved work above. Preserve the user's decisions and constraints, inspect the actual files before making assumptions, and proceed without asking the user to repeat context.");
|
|
101
|
+
return trimText(parts.join("\n\n"), MAX_CONTEXT_CHARS);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
MAX_CONTEXT_CHARS: MAX_CONTEXT_CHARS,
|
|
106
|
+
buildHandoffContext: buildHandoffContext,
|
|
107
|
+
latestUserRequest: latestUserRequest,
|
|
108
|
+
recentTurns: recentTurns,
|
|
109
|
+
repositoryState: repositoryState,
|
|
110
|
+
};
|
package/lib/sessions.js
CHANGED
|
@@ -169,6 +169,7 @@ function createSessionManager(opts) {
|
|
|
169
169
|
if (session.lastRewindUuid) metaObj.lastRewindUuid = session.lastRewindUuid;
|
|
170
170
|
if (session.loop) metaObj.loop = session.loop;
|
|
171
171
|
if (session.spawn) metaObj.spawn = session.spawn;
|
|
172
|
+
if (session.handoff) metaObj.handoff = session.handoff;
|
|
172
173
|
if (session.debateState) metaObj.debateState = session.debateState;
|
|
173
174
|
if (session.debateSetupMode) metaObj.debateSetupMode = true;
|
|
174
175
|
var meta = JSON.stringify(metaObj);
|
|
@@ -281,6 +282,7 @@ function createSessionManager(opts) {
|
|
|
281
282
|
session.effort = m.effort || null;
|
|
282
283
|
if (m.loop) session.loop = m.loop;
|
|
283
284
|
if (m.spawn) session.spawn = m.spawn;
|
|
285
|
+
if (m.handoff) session.handoff = m.handoff;
|
|
284
286
|
if (m.debateState) session.debateState = m.debateState;
|
|
285
287
|
if (m.debateSetupMode) session.debateSetupMode = true;
|
|
286
288
|
if (m.ownerId) session.ownerId = m.ownerId;
|
package/lib/ws-schema.js
CHANGED
|
@@ -30,6 +30,8 @@ var schema = {
|
|
|
30
30
|
"search_session_content": { direction: "c2s", handler: "lib/project-sessions.js", description: "Full-text search within a session" },
|
|
31
31
|
"load_more_history": { direction: "c2s", handler: "lib/project-sessions.js", description: "Request older history entries for the current session" },
|
|
32
32
|
"fork_session": { direction: "c2s", handler: "lib/project-sessions.js", description: "Fork a session from a given message UUID" },
|
|
33
|
+
"handoff_session": { direction: "c2s", handler: "lib/project-session-handoff.js", description: "Continue the active session in a newly created agent session" },
|
|
34
|
+
"handoff_session_options": { direction: "c2s", handler: "lib/project-session-handoff.js", description: "Request vendors, models, and effort capabilities for the handoff dialog (response reuses the same type)" },
|
|
33
35
|
"input_sync": { direction: "c2s", handler: "lib/project-sessions.js", description: "Sync the current input field text to other clients" },
|
|
34
36
|
"tui_transcript_request": { direction: "c2s", handler: "lib/project-sessions.js", description: "Ask for the assistant text index of a Claude TUI session (for hover-to-grab)" },
|
|
35
37
|
"split_group_create": { direction: "c2s", handler: "lib/session-split-groups.js", description: "Create a persistent two-session split group" },
|
|
@@ -49,6 +51,9 @@ var schema = {
|
|
|
49
51
|
"search_results": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Session title search results" },
|
|
50
52
|
"search_content_results": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Full-text content search results" },
|
|
51
53
|
"fork_complete": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Fork succeeded, includes new session ID" },
|
|
54
|
+
"session_handoff_result": { direction: "s2c", handler: "lib/public/modules/session-actions.js", description: "Session handoff result and new target session ID" },
|
|
55
|
+
"handoff_context": { direction: "s2c", handler: "lib/public/modules/session-actions.js", description: "Timeline marker identifying the source of a handoff session" },
|
|
56
|
+
"handoff_created": { direction: "s2c", handler: "lib/public/modules/session-actions.js", description: "Timeline marker linking a source session to its handoff target" },
|
|
52
57
|
"input_sync_broadcast": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Broadcast input text from another client" },
|
|
53
58
|
"tui_transcript_state": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Assistant text index for a Claude TUI session (full replace; sent on request and after each new assistant message)" },
|
|
54
59
|
"split_groups": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Full persistent split-group list for the current user" },
|
|
@@ -1072,6 +1072,7 @@ function createCodexQueryHandle(appServer, queryOpts) {
|
|
|
1072
1072
|
await appServer.send("turn/start", {
|
|
1073
1073
|
threadId: state.threadId,
|
|
1074
1074
|
input: input,
|
|
1075
|
+
model: state.model,
|
|
1075
1076
|
}, 60000);
|
|
1076
1077
|
|
|
1077
1078
|
// Wait for turn to complete
|
|
@@ -1144,7 +1145,7 @@ function createCodexQueryHandle(appServer, queryOpts) {
|
|
|
1144
1145
|
},
|
|
1145
1146
|
|
|
1146
1147
|
setModel: function(model) {
|
|
1147
|
-
|
|
1148
|
+
state.model = model || "gpt-5.6-terra";
|
|
1148
1149
|
return Promise.resolve();
|
|
1149
1150
|
},
|
|
1150
1151
|
|
|
@@ -1326,7 +1327,7 @@ function createCodexAdapter(opts) {
|
|
|
1326
1327
|
fastModeState: null,
|
|
1327
1328
|
capabilities: {
|
|
1328
1329
|
effort: true,
|
|
1329
|
-
midSessionModelSwitch:
|
|
1330
|
+
midSessionModelSwitch: true,
|
|
1330
1331
|
fork: true,
|
|
1331
1332
|
rollback: true,
|
|
1332
1333
|
sessionListing: false,
|
|
@@ -762,7 +762,7 @@ function createKiroQueryHandle(acp, queryOpts) {
|
|
|
762
762
|
|
|
763
763
|
setModel: function(model) {
|
|
764
764
|
state.model = model;
|
|
765
|
-
return setSessionModel(model)
|
|
765
|
+
return setSessionModel(model);
|
|
766
766
|
},
|
|
767
767
|
|
|
768
768
|
setEffort: function() { return Promise.resolve(); },
|
|
@@ -867,7 +867,7 @@ function createKiroAdapter(opts) {
|
|
|
867
867
|
fastModeState: null,
|
|
868
868
|
capabilities: {
|
|
869
869
|
effort: false,
|
|
870
|
-
midSessionModelSwitch:
|
|
870
|
+
midSessionModelSwitch: true,
|
|
871
871
|
fork: false,
|
|
872
872
|
rollback: false,
|
|
873
873
|
sessionListing: false,
|
package/package.json
CHANGED