clay-server 5.2.0-beta.8 → 5.2.0-beta.9
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/pane-pending-interactions.js +20 -0
- package/lib/pane-session-activation.js +114 -0
- package/lib/pane-session-transport.js +26 -0
- package/lib/project-sessions.js +15 -5
- package/lib/project.js +11 -3
- package/lib/public/app.js +14 -0
- package/lib/public/css/contextual-clay.css +1 -1
- package/lib/public/css/input.css +6 -0
- package/lib/public/css/sticky-notes.css +37 -11
- package/lib/public/modules/app-connection.js +3 -0
- package/lib/public/modules/app-message-router.js +13 -0
- package/lib/public/modules/app-messages.js +25 -5
- package/lib/public/modules/app-misc.js +3 -1
- package/lib/public/modules/app-projects.js +5 -0
- package/lib/public/modules/clay-owner-navigation.js +0 -3
- package/lib/public/modules/clay-studio-render.js +3 -1
- package/lib/public/modules/command-palette.js +52 -14
- package/lib/public/modules/contextual-clay-workspace.js +14 -2
- package/lib/public/modules/contextual-clay.js +16 -3
- package/lib/public/modules/contextual-mate-context.js +6 -4
- package/lib/public/modules/elicitation-form.js +5 -0
- package/lib/public/modules/history-replay-batch.js +8 -0
- package/lib/public/modules/input.js +55 -11
- package/lib/public/modules/message-delivery.js +10 -2
- package/lib/public/modules/model-picker.js +2 -0
- package/lib/public/modules/pane-activation.js +214 -0
- package/lib/public/modules/pane-binding.js +82 -0
- package/lib/public/modules/pane-bridge.js +5 -2
- package/lib/public/modules/pane-browser-bridge.js +5 -3
- package/lib/public/modules/pane-file-bridge.js +4 -0
- package/lib/public/modules/pane-issue-bridge.js +4 -1
- package/lib/public/modules/rewind.js +5 -0
- package/lib/public/modules/session-actions.js +5 -2
- package/lib/public/modules/session-context-menu.js +5 -0
- package/lib/public/modules/session-rich-drafts.js +21 -0
- package/lib/public/modules/session-switch-defer.js +67 -0
- package/lib/public/modules/split-group-restore.js +68 -0
- package/lib/public/modules/split-pair-ui.js +1 -0
- package/lib/public/modules/split-pane-reconciler.js +15 -0
- package/lib/public/modules/split-pane-renderer.js +87 -6
- package/lib/public/modules/split-session-boundary.js +1 -1
- package/lib/public/modules/split-view.js +51 -52
- package/lib/public/modules/sticky-notes-checklist.js +51 -6
- package/lib/public/modules/stt-controller.js +4 -1
- package/lib/public/modules/stt-recording.js +1 -1
- package/lib/public/modules/tools.js +18 -2
- package/lib/sessions.js +15 -8
- package/package.json +4 -3
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { forwardContextualSessionCitation, validContextualCitation, contextualCitationLink } from './contextual-session-citations.js';
|
|
2
|
-
import { dismissContextualClay } from './contextual-clay.js';
|
|
3
2
|
import { mateAvatarUrl } from './avatar.js';
|
|
4
3
|
import { escapeHtml } from './utils.js';
|
|
5
4
|
import { refreshIcons } from './icons.js';
|
|
@@ -146,7 +145,6 @@ export function handleClaySessionTarget(msg) {
|
|
|
146
145
|
navigateToLog(msg.target);
|
|
147
146
|
return true;
|
|
148
147
|
}
|
|
149
|
-
if (citation) { dismissContextualClay(false); if ((store.get('contextualClay') || {}).mode !== 'closed') return true; }
|
|
150
148
|
navigateToSession({ projectSlug: msg.target.projectSlug, sessionId: msg.target.sessionId }, null, msg.target.entryIndex);
|
|
151
149
|
return true;
|
|
152
150
|
}
|
|
@@ -174,24 +172,36 @@ export function closeCommandPalette() {
|
|
|
174
172
|
|
|
175
173
|
export function setPaletteVersion(version) { cachedVersion = version; }
|
|
176
174
|
|
|
177
|
-
export function handlePaletteSessionSwitch() {
|
|
175
|
+
export function handlePaletteSessionSwitch(authoritative) {
|
|
178
176
|
if (pendingLogNav && ctx.currentSlug && ctx.currentSlug() === pendingLogNav.projectSlug) {
|
|
179
177
|
var logNav = pendingLogNav;
|
|
180
178
|
pendingLogNav = null;
|
|
181
179
|
openProjectLog(logNav.ref);
|
|
182
180
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
pendingNav
|
|
186
|
-
|
|
187
|
-
|
|
181
|
+
// session_list precedes the connection's authoritative session_switched.
|
|
182
|
+
// Wait for that identity instead of requesting a duplicate initial replay.
|
|
183
|
+
if (authoritative === true && pendingNav && store.get('activeSessionId')) {
|
|
184
|
+
var nav = pendingNav;
|
|
185
|
+
pendingNav = null;
|
|
186
|
+
if (ctx.currentSlug && ctx.currentSlug() === nav.slug) {
|
|
187
|
+
navigateToSession({ projectSlug: nav.slug, sessionId: nav.sessionId }, nav.query, nav.entryIndex);
|
|
188
|
+
}
|
|
188
189
|
}
|
|
190
|
+
var pending = store.get('pendingSessionEntryNavigation');
|
|
191
|
+
if (pending && isActiveEntryNavigation(pending)) pending.awaitingTarget = false;
|
|
192
|
+
var highlight = store.get('sessionEntryHighlight');
|
|
193
|
+
if (highlight && !isCurrentEntryHighlight(highlight)) store.set({ sessionEntryHighlight: null });
|
|
189
194
|
}
|
|
190
195
|
|
|
191
196
|
export function handlePaletteHistoryDone() {
|
|
192
197
|
var pending = store.get('pendingSessionEntryNavigation');
|
|
193
|
-
if (!pending)
|
|
198
|
+
if (!pending) {
|
|
199
|
+
var highlight = store.get('sessionEntryHighlight');
|
|
200
|
+
if (highlight && isCurrentEntryHighlight(highlight)) jumpToSessionEntry(highlight.entryIndex, highlight);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
194
203
|
if (!isActiveEntryNavigation(pending)) {
|
|
204
|
+
if (pending.awaitingTarget) return;
|
|
195
205
|
store.set({ pendingSessionEntryNavigation: null });
|
|
196
206
|
return;
|
|
197
207
|
}
|
|
@@ -201,18 +211,26 @@ export function handlePaletteHistoryDone() {
|
|
|
201
211
|
if (ws && ws.readyState === 1) ws.send(JSON.stringify({ type: "load_more_history", before: historyFrom, target: pending.entryIndex }));
|
|
202
212
|
return;
|
|
203
213
|
}
|
|
204
|
-
jumpToSessionEntry(pending.entryIndex);
|
|
214
|
+
jumpToSessionEntry(pending.entryIndex, pending);
|
|
205
215
|
}
|
|
206
216
|
|
|
207
217
|
export function handlePaletteHistoryPrepended() {
|
|
208
218
|
var pending = store.get('pendingSessionEntryNavigation');
|
|
209
219
|
if (!pending) return;
|
|
210
220
|
if (!isActiveEntryNavigation(pending)) {
|
|
221
|
+
if (pending.awaitingTarget) return;
|
|
211
222
|
store.set({ pendingSessionEntryNavigation: null });
|
|
212
223
|
return;
|
|
213
224
|
}
|
|
214
225
|
requestAnimationFrame(function () {
|
|
215
|
-
if (store.get('pendingSessionEntryNavigation')
|
|
226
|
+
if (store.get('pendingSessionEntryNavigation') !== pending) return;
|
|
227
|
+
// This frame was scheduled for an already active target. A later
|
|
228
|
+
// navigation cancels it even if its initial awaiting flag was retained.
|
|
229
|
+
if (!isActiveEntryNavigation(pending)) {
|
|
230
|
+
store.set({ pendingSessionEntryNavigation: null });
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
handlePaletteHistoryDone();
|
|
216
234
|
});
|
|
217
235
|
}
|
|
218
236
|
|
|
@@ -220,6 +238,11 @@ function isActiveEntryNavigation(pending) {
|
|
|
220
238
|
return String(store.get('currentSlug') || (ctx && ctx.currentSlug ? ctx.currentSlug() : "")) === String(pending.projectSlug) && String(store.get('activeSessionId')) === String(pending.sessionId);
|
|
221
239
|
}
|
|
222
240
|
|
|
241
|
+
function isCurrentEntryHighlight(highlight) {
|
|
242
|
+
return highlight.expiresAt > Date.now() && isActiveEntryNavigation(highlight)
|
|
243
|
+
&& highlight.sessionOriginId === (store.get('sessionOriginId') || null);
|
|
244
|
+
}
|
|
245
|
+
|
|
223
246
|
function jumpToSessionEntry(entryIndex, pending) {
|
|
224
247
|
if (pending && !isActiveEntryNavigation(pending)) return;
|
|
225
248
|
var messages = document.querySelectorAll("#messages [data-history-index], #messages [data-history-indexes]");
|
|
@@ -228,17 +251,31 @@ function jumpToSessionEntry(entryIndex, pending) {
|
|
|
228
251
|
var indexes = (messages[i].dataset.historyIndexes || messages[i].dataset.historyIndex || "").split(",");
|
|
229
252
|
if (indexes.indexOf(String(entryIndex)) !== -1) { target = messages[i]; break; }
|
|
230
253
|
}
|
|
231
|
-
store.set({ pendingSessionEntryNavigation: null });
|
|
232
254
|
if (!target) return;
|
|
255
|
+
store.set({ pendingSessionEntryNavigation: null });
|
|
233
256
|
disarmStickyBottom();
|
|
234
257
|
var oldTargets = document.querySelectorAll("#messages .session-entry-target");
|
|
235
258
|
for (var oldIndex = 0; oldIndex < oldTargets.length; oldIndex++) oldTargets[oldIndex].classList.remove("session-entry-target");
|
|
236
259
|
var reducedMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
260
|
+
// A repeated authoritative replay replaces the DOM after the initial jump.
|
|
261
|
+
// Retain only this exact origin for the original feedback lifetime.
|
|
262
|
+
var highlight = pending && pending.expiresAt ? pending : {
|
|
263
|
+
projectSlug: store.get('currentSlug'), sessionId: store.get('activeSessionId'),
|
|
264
|
+
sessionOriginId: store.get('sessionOriginId') || null, entryIndex: entryIndex,
|
|
265
|
+
expiresAt: Date.now() + (reducedMotion ? 700 : 1800),
|
|
266
|
+
};
|
|
267
|
+
store.set({ sessionEntryHighlight: highlight });
|
|
237
268
|
target.scrollIntoView({ behavior: reducedMotion ? "auto" : "smooth", block: "center" });
|
|
238
269
|
target.classList.remove("session-entry-target");
|
|
239
270
|
void target.offsetWidth;
|
|
240
271
|
target.classList.add("session-entry-target");
|
|
241
|
-
|
|
272
|
+
target.sessionEntryHighlight = highlight;
|
|
273
|
+
setTimeout(function () {
|
|
274
|
+
if (target.sessionEntryHighlight !== highlight) return;
|
|
275
|
+
target.classList.remove("session-entry-target");
|
|
276
|
+
delete target.sessionEntryHighlight;
|
|
277
|
+
if (store.get('sessionEntryHighlight') === highlight) store.set({ sessionEntryHighlight: null });
|
|
278
|
+
}, Math.max(0, highlight.expiresAt - Date.now()));
|
|
242
279
|
}
|
|
243
280
|
|
|
244
281
|
function navigateToLog(target) {
|
|
@@ -433,8 +470,9 @@ function activateItem(entry) {
|
|
|
433
470
|
export function navigateToSession(item, query, entryIndex) {
|
|
434
471
|
closeCommandPalette();
|
|
435
472
|
pendingNav = null;
|
|
473
|
+
store.set({ sessionEntryHighlight: null });
|
|
436
474
|
var hasEntry = entryIndex !== null && entryIndex !== undefined && Number.isSafeInteger(Number(entryIndex)) && Number(entryIndex) >= 0;
|
|
437
|
-
store.set({ pendingSessionEntryNavigation: hasEntry ? { projectSlug: item.projectSlug, sessionId: item.sessionId, entryIndex: Number(entryIndex) } : null });
|
|
475
|
+
store.set({ pendingSessionEntryNavigation: hasEntry ? { projectSlug: item.projectSlug, sessionId: item.sessionId, entryIndex: Number(entryIndex), awaitingTarget: true } : null });
|
|
438
476
|
if (ctx.currentSlug && ctx.currentSlug() === item.projectSlug) {
|
|
439
477
|
var alreadyActive = String(store.get('activeSessionId')) === String(item.sessionId);
|
|
440
478
|
if (alreadyActive) {
|
|
@@ -89,7 +89,9 @@ export function finishContextualWorkspace() {
|
|
|
89
89
|
if (handoff.stage === 'workspace' && store.get('currentSlug') === handoff.slug && store.get('activeSessionId') === handoff.sessionId) draft = takeComposerDraft();
|
|
90
90
|
connection.restore(draft);
|
|
91
91
|
connection.suspend(false);
|
|
92
|
-
|
|
92
|
+
var presentation = store.get('contextualClay') || {};
|
|
93
|
+
var keepOverlay = presentation.mode === 'popover' && connection !== bridge();
|
|
94
|
+
store.set({ contextualWorkspace: Object.assign({}, handoff, { draft: null, stage: 'returning' }), contextualClay: keepOverlay ? presentation : Object.assign({}, presentation, { mode: 'closed' }) });
|
|
93
95
|
lockNative(true);
|
|
94
96
|
return true;
|
|
95
97
|
} catch (error) { showToast(error.message, 'error'); return false; }
|
|
@@ -136,7 +138,17 @@ export function renderContextualWorkspaceBack() {
|
|
|
136
138
|
|
|
137
139
|
export function prepareContextualWorkspaceDeparture(slug) {
|
|
138
140
|
var origin = store.get('contextualNativeOrigin');
|
|
139
|
-
if (origin && slug !== origin.slug
|
|
141
|
+
if (origin && slug !== origin.slug) {
|
|
142
|
+
if ((store.get('contextualClay') || {}).mode === 'popover') {
|
|
143
|
+
var connection = bridge(origin.mateId, origin.sessionId);
|
|
144
|
+
try {
|
|
145
|
+
if (origin.stage !== 'pane' || !connection || !connection.identity().ready) throw new Error('Wait for this Mate conversation to reconnect.');
|
|
146
|
+
// The pane keeps its composer when its former native parent changes.
|
|
147
|
+
connection.checkDraft();
|
|
148
|
+
store.set({ contextualNativeOrigin: null }); lockNative(false);
|
|
149
|
+
} catch (error) { showToast(error.message, 'error'); return false; }
|
|
150
|
+
} else if (!finishContextualNativeOrigin()) return false;
|
|
151
|
+
}
|
|
140
152
|
var handoff = store.get('contextualWorkspace');
|
|
141
153
|
if (handoff && handoff.stage.indexOf('workspace') === 0 && slug !== handoff.slug) return finishContextualWorkspace();
|
|
142
154
|
}
|
|
@@ -34,6 +34,8 @@ function escapePopover(event) {
|
|
|
34
34
|
if (event.defaultPrevented || contextualClayMode() !== 'popover') return;
|
|
35
35
|
var context = host().querySelector('details[open]');
|
|
36
36
|
if (context) { context.open = false; context.querySelector('summary').focus(); return; }
|
|
37
|
+
var focused = document.activeElement;
|
|
38
|
+
if (!host().contains(focused) && focused !== trigger()) return;
|
|
37
39
|
event.preventDefault(); dismissContextualClay(true);
|
|
38
40
|
});
|
|
39
41
|
}
|
|
@@ -82,7 +84,16 @@ function render() {
|
|
|
82
84
|
}
|
|
83
85
|
var projectContext = (store.get('projectsHubList') || []).find(function (item) { return item.slug === store.get('currentSlug'); });
|
|
84
86
|
var contextSession = getCachedSessions().find(function (item) { return item.id === store.get('activeSessionId'); });
|
|
85
|
-
|
|
87
|
+
var screen = store.get('homeShellVisible') || store.get('projectsHubVisible') ? 'Home' : store.get('dmMode') ? 'Direct message' : 'Project';
|
|
88
|
+
var projectView = screen === 'Project';
|
|
89
|
+
var parentReady = store.get('activeProjectSlug') === store.get('currentSlug') && store.get('sessionActivatedProjectSlug') === store.get('currentSlug') && !store.get('replayingHistory');
|
|
90
|
+
updateMateContext(surface, mate, contextualFrame(), {
|
|
91
|
+
projectTitle: projectView ? projectContext && (projectContext.title || projectContext.project || projectContext.slug) : store.get('projectsHubVisible') ? 'Projects' : screen,
|
|
92
|
+
conversationTitle: projectView && parentReady && contextSession ? contextSession.title : null,
|
|
93
|
+
projectSlug: projectView && parentReady ? store.get('currentSlug') : null,
|
|
94
|
+
sessionId: projectView && parentReady ? store.get('activeSessionId') : null,
|
|
95
|
+
screen: screen, ownerId: store.get('myUserId') || null
|
|
96
|
+
});
|
|
86
97
|
if (trigger()) trigger().setAttribute('aria-expanded', String(mode === 'popover'));
|
|
87
98
|
var selectedFrame = contextualFrame();
|
|
88
99
|
var connection = selectedFrame && selectedFrame.conversation;
|
|
@@ -193,7 +204,7 @@ export function initContextualClay() {
|
|
|
193
204
|
initClayControlActivity();
|
|
194
205
|
initContextualWorkspace();
|
|
195
206
|
document.addEventListener('pointerdown', function (event) {
|
|
196
|
-
if (
|
|
207
|
+
if (host() && !host().contains(event.target)) host().querySelectorAll('details[open]').forEach(function (menu) { menu.open = false; });
|
|
197
208
|
});
|
|
198
209
|
document.addEventListener('keydown', function (event) {
|
|
199
210
|
if (event.key === 'Escape' && !event.defaultPrevented && contextualClayMode() === 'popover') {
|
|
@@ -231,7 +242,9 @@ export function initContextualClay() {
|
|
|
231
242
|
var pending = value.contextualMatePending;
|
|
232
243
|
store.set({ contextualMatePending: null }); activateContextualMate(pending); return;
|
|
233
244
|
}
|
|
234
|
-
|
|
245
|
+
// Ordinary navigation refreshes context without replacing the conversation.
|
|
246
|
+
// Direct navigation into a native Mate workspace ends the floating surface.
|
|
247
|
+
if (current.mode === 'popover' && value.currentSlug !== previous.currentSlug && (value.currentSlug || '').indexOf('mate-') === 0) dismissContextualClay(false);
|
|
235
248
|
render();
|
|
236
249
|
});
|
|
237
250
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { store } from './store.js';
|
|
2
2
|
import { getWs } from './ws-ref.js';
|
|
3
|
+
import { contextualFrame } from './contextual-mate-identity.js';
|
|
3
4
|
|
|
4
5
|
function identity(frame) {
|
|
5
6
|
try { return frame && frame.conversation && frame.conversation.identity(); } catch (error) { return null; }
|
|
6
7
|
}
|
|
7
8
|
function contextKey(frame, canonical) {
|
|
8
9
|
var current = identity(frame);
|
|
9
|
-
return JSON.stringify([store.get('currentSlug'), store.get('activeSessionId'), store.get('homeShellVisible'), store.get('dmMode'), store.get('rightWorkbenchOwner'), store.get('homeSubSurface'), store.get('rightWorkbenchRevision'), store.get('myUserId'), store.get('connected'), current && current.mateId, current && current.sessionId, current && (canonical ? current.connected : current.ready), canonical && current && current.sessionOriginId]);
|
|
10
|
+
return JSON.stringify([store.get('currentSlug'), store.get('activeSessionId'), store.get('sessionOriginId'), store.get('activeProjectSlug'), store.get('sessionActivatedProjectSlug'), store.get('replayingHistory'), (store.get('contextualClay') || {}).mode, store.get('homeShellVisible'), store.get('projectsHubVisible'), store.get('dmMode'), store.get('rightWorkbenchOwner'), store.get('homeSubSurface'), store.get('rightWorkbenchRevision'), store.get('myUserId'), store.get('connected'), current && current.mateId, current && current.sessionId, current && (canonical ? current.connected : current.ready), canonical && current && current.sessionOriginId]);
|
|
10
11
|
}
|
|
11
12
|
export function updateMateContext(surface, mate, frame, snapshot) {
|
|
12
13
|
var current = identity(frame), ws = getWs(), key = contextKey(frame, mate && mate.builtinKey === 'clay'), prepared = store.get('contextualPrepared');
|
|
@@ -16,11 +17,12 @@ export function updateMateContext(surface, mate, frame, snapshot) {
|
|
|
16
17
|
capabilities.setAttribute('aria-label', 'Clay capabilities');
|
|
17
18
|
capabilities.textContent = "I can control Clay Studio for you—manage projects, coordinate work, set up schedules, and navigate your workspace. Just tell me what you’d like done.";
|
|
18
19
|
var privateWorkspace = (store.get('currentSlug') || '').indexOf('mate-') === 0;
|
|
19
|
-
var
|
|
20
|
+
var parentReady = store.get('activeProjectSlug') === store.get('currentSlug') && store.get('sessionActivatedProjectSlug') === store.get('currentSlug') && !store.get('replayingHistory');
|
|
21
|
+
var available = (store.get('contextualClay') || {}).mode === 'popover' && parentReady && !canonical && !privateWorkspace && current && current.ready && store.get('connected') && !store.get('homeShellVisible') && !store.get('projectsHubVisible') && !store.get('dmMode') && !store.get('rightWorkbenchOwner') && ws && ws.readyState === 1;
|
|
20
22
|
var changed = !prepared || prepared.key !== key || prepared.socket !== ws;
|
|
21
23
|
if (changed) {
|
|
22
24
|
var requestId = 'context-' + crypto.randomUUID();
|
|
23
|
-
prepared = { key: key, requestId: requestId, socket: ws, mateId: current && current.mateId, status: available ? 'preparing' : 'unavailable' };
|
|
25
|
+
prepared = { key: key, requestId: requestId, socket: ws, mateId: current && current.mateId, canonical: !!canonical, status: available ? 'preparing' : 'unavailable' };
|
|
24
26
|
}
|
|
25
27
|
// Stamp the reference before publishing a new preparation: store subscribers
|
|
26
28
|
// may render synchronously, and the composer must never see the prior view ID.
|
|
@@ -65,7 +67,7 @@ export function updateMateContext(surface, mate, frame, snapshot) {
|
|
|
65
67
|
export function handleMateContextMessage(msg) {
|
|
66
68
|
if (msg.type === 'mate_context_prepared') {
|
|
67
69
|
var pending = store.get('contextualPrepared');
|
|
68
|
-
if (pending && pending.requestId === msg.requestId && pending.socket === getWs()) {
|
|
70
|
+
if (pending && pending.requestId === msg.requestId && pending.socket === getWs() && pending.key === contextKey(contextualFrame(), pending.canonical)) {
|
|
69
71
|
store.set({ contextualPrepared: Object.assign({}, pending, msg, { status: msg.contextRef ? 'ready' : 'unavailable' }) });
|
|
70
72
|
if (msg.contextRef) setTimeout(function () {
|
|
71
73
|
var current = store.get('contextualPrepared');
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
import { escapeHtml } from './utils.js';
|
|
5
5
|
import { iconHtml } from './icons.js';
|
|
6
6
|
import { getWs } from './ws-ref.js';
|
|
7
|
+
import { store } from './store.js';
|
|
8
|
+
import { currentPaneReview } from './pane-binding.js';
|
|
7
9
|
import { fieldInfo, patternIsBounded, validateForm } from './elicitation-schema.js';
|
|
8
10
|
|
|
9
11
|
var UNSET = "__clay_unset__";
|
|
@@ -154,12 +156,14 @@ function showFormError(container, message) {
|
|
|
154
156
|
}
|
|
155
157
|
|
|
156
158
|
function send(container, action, content, onSent) {
|
|
159
|
+
if (!currentPaneReview(container)) return false;
|
|
157
160
|
var ws = getWs();
|
|
158
161
|
if (!ws || ws.readyState !== 1) {
|
|
159
162
|
showFormError(container, "Not connected. Try again when the connection is restored.");
|
|
160
163
|
return false;
|
|
161
164
|
}
|
|
162
165
|
var msg = { type: "elicitation_response", requestId: container.dataset.requestId, action: action };
|
|
166
|
+
if (container.paneBinding) msg.paneBinding = container.paneBinding;
|
|
163
167
|
if (action === "accept") msg.content = content;
|
|
164
168
|
ws.send(JSON.stringify(msg));
|
|
165
169
|
setBusy(container, true);
|
|
@@ -177,6 +181,7 @@ function actionButton(label, className, handler) {
|
|
|
177
181
|
|
|
178
182
|
export function buildElicitationCard(msg, onSent) {
|
|
179
183
|
var container = el("div", "permission-container elicitation-container");
|
|
184
|
+
container.paneBinding = store.get('panePendingBinding') || store.get('paneCommittedBinding') || null;
|
|
180
185
|
container.dataset.requestId = msg.requestId;
|
|
181
186
|
var header = el("div", "permission-header");
|
|
182
187
|
header.innerHTML = '<span class="permission-icon">' + iconHtml("key") + '</span>' +
|
|
@@ -27,6 +27,14 @@ function canCombine(previous, message) {
|
|
|
27
27
|
// can differ; the first token still supplies the message's display time.
|
|
28
28
|
var keys = Object.keys(previous).concat(Object.keys(message));
|
|
29
29
|
return keys.every(function (key) {
|
|
30
|
+
// Socket JSON gives each event its own activation object. Compare every
|
|
31
|
+
// field, including future metadata, without weakening replay boundaries.
|
|
32
|
+
if (key === 'paneActivation' && previous[key] && message[key]) {
|
|
33
|
+
var before = previous[key], after = message[key];
|
|
34
|
+
return Object.keys(before).concat(Object.keys(after)).every(function (field) {
|
|
35
|
+
return before[field] === after[field];
|
|
36
|
+
});
|
|
37
|
+
}
|
|
30
38
|
return key === 'text' || key === '_ts' || key === '_historyIndex' || key === '_historyIndexes' || previous[key] === message[key];
|
|
31
39
|
});
|
|
32
40
|
}
|
|
@@ -8,6 +8,7 @@ import { setRewindMode, isRewindMode } from './rewind.js';
|
|
|
8
8
|
import { renderPicker as renderContextPicker } from './context-sources.js';
|
|
9
9
|
import { checkForMention, showMentionMenu, hideMentionMenu, isMentionMenuVisible, mentionMenuKeydown, setMentionAtIdx, parseMentionFromInput, clearMentionState, stickyReapplyMention, sendMention, sendUserMention, renderMentionUser, renderUserMention, removeMentionChip } from './mention.js';
|
|
10
10
|
import { store } from './store.js';
|
|
11
|
+
import { sessionCallback, paneMessageBinding, samePaneBinding } from './pane-binding.js';
|
|
11
12
|
import { sendAcknowledgedMessage } from './message-delivery.js';
|
|
12
13
|
import { mateAvatarUrl } from './avatar.js';
|
|
13
14
|
import { tuiIsActive, tuiSubmitText } from './session-tui-view.js';
|
|
@@ -122,6 +123,9 @@ function showPreThinkingForTurn() {
|
|
|
122
123
|
|
|
123
124
|
// --- Send ---
|
|
124
125
|
export function sendMessage() {
|
|
126
|
+
if (store.get('deferredSessionSwitch') || store.get('panePendingBinding') || store.get('paneSuspended')) return;
|
|
127
|
+
if (store.get('paneCommittedBinding') && !store.get('paneReady')) return;
|
|
128
|
+
var actionBinding = paneMessageBinding();
|
|
125
129
|
if (store.get('contextualWriterSuspended')) return;
|
|
126
130
|
if (ctx && requestRecordedSend(ctx.inputEl, sendMessage)) return;
|
|
127
131
|
stopSTT();
|
|
@@ -346,6 +350,7 @@ export function sendMessage() {
|
|
|
346
350
|
store.set({ pendingScheduleAdmissions: admissions, pendingScheduleAdmission: admissions[scheduleRequestId] });
|
|
347
351
|
ctx.ws.send(JSON.stringify({
|
|
348
352
|
type: "schedule_message", requestId: scheduleRequestId,
|
|
353
|
+
paneBinding: actionBinding || undefined,
|
|
349
354
|
projectSlug: scheduleContext.projectSlug, sessionId: scheduleContext.sessionId, accountId: scheduleContext.accountId,
|
|
350
355
|
text: text || "", images: images, pastes: pastes, resetsAt: resetsAt,
|
|
351
356
|
}));
|
|
@@ -354,6 +359,7 @@ export function sendMessage() {
|
|
|
354
359
|
|
|
355
360
|
var screenReference = contextualScreenReference();
|
|
356
361
|
var payload = { type: "message", text: text || "" };
|
|
362
|
+
if (actionBinding) payload.paneBinding = actionBinding;
|
|
357
363
|
if (screenReference) payload.contextualSnapshot = screenReference;
|
|
358
364
|
if (images.length > 0) {
|
|
359
365
|
payload.images = images;
|
|
@@ -440,6 +446,7 @@ export function sendTextMessage(text) {
|
|
|
440
446
|
export function sendShellResultToAgent(msg) {
|
|
441
447
|
if (!ctx || !ctx.connected || !msg || msg.error || !msg.command) return false;
|
|
442
448
|
if (String(msg.sessionId) !== String(store.get("activeSessionId"))) return false;
|
|
449
|
+
if (store.get('paneCommittedBinding') && !samePaneBinding(msg.paneActivation, store.get('paneCommittedBinding'))) return false;
|
|
443
450
|
var payload = {
|
|
444
451
|
type: "message",
|
|
445
452
|
text: "$ " + msg.command,
|
|
@@ -447,7 +454,8 @@ export function sendShellResultToAgent(msg) {
|
|
|
447
454
|
};
|
|
448
455
|
var selectedVendor = store.get("currentVendor") || null;
|
|
449
456
|
if (selectedVendor) payload.vendor = selectedVendor;
|
|
450
|
-
|
|
457
|
+
if (msg.paneActivation) payload.paneBinding = msg.paneActivation;
|
|
458
|
+
if (ctx.ws.send(JSON.stringify(payload)) === false) return false;
|
|
451
459
|
commitVendorForTurn();
|
|
452
460
|
showPreThinkingForTurn();
|
|
453
461
|
return true;
|
|
@@ -543,9 +551,17 @@ function removePendingImage(idx) {
|
|
|
543
551
|
}
|
|
544
552
|
|
|
545
553
|
// Move composer ownership between two authenticated views of the same session.
|
|
554
|
+
export function composerDepartureReason() {
|
|
555
|
+
if (uploadingCount) return 'Wait for attachments to finish uploading before changing views.';
|
|
556
|
+
if (store.get('speechActive') || ctx && hasRecordedSpeechFor(ctx.inputEl)) return 'Finish or discard the voice recording before changing views.';
|
|
557
|
+
var retry = store.get('speechRetry');
|
|
558
|
+
if (retry && ctx && retry.input === ctx.inputEl) return 'Retry or discard the voice recording before changing views.';
|
|
559
|
+
return '';
|
|
560
|
+
}
|
|
546
561
|
export function takeComposerDraft() {
|
|
547
|
-
|
|
548
|
-
if (
|
|
562
|
+
var reason = composerDepartureReason();
|
|
563
|
+
if (reason) throw new Error(reason);
|
|
564
|
+
if (document.dispatchEvent) document.dispatchEvent(new CustomEvent('clay-composer-handoff'));
|
|
549
565
|
var input = ctx.inputEl;
|
|
550
566
|
var draft = { text: input.value, start: input.selectionStart, end: input.selectionEnd, direction: input.selectionDirection, images: pendingImages, pastes: pendingPastes, files: pendingFiles, delay: scheduleDelayMs };
|
|
551
567
|
input.value = '';
|
|
@@ -585,14 +601,14 @@ function pastePreview(text) {
|
|
|
585
601
|
}
|
|
586
602
|
|
|
587
603
|
function editPendingPaste(idx) {
|
|
588
|
-
showPasteModal(pendingPastes[idx].text, function (text) {
|
|
604
|
+
showPasteModal(pendingPastes[idx].text, sessionCallback(function (text) {
|
|
589
605
|
if (!text) {
|
|
590
606
|
removePendingPaste(idx);
|
|
591
607
|
return;
|
|
592
608
|
}
|
|
593
609
|
pendingPastes[idx] = { text: text, preview: pastePreview(text) };
|
|
594
610
|
renderInputPreviews();
|
|
595
|
-
});
|
|
611
|
+
}));
|
|
596
612
|
}
|
|
597
613
|
|
|
598
614
|
function removePendingFile(idx) {
|
|
@@ -726,6 +742,7 @@ function uploadFile(file) {
|
|
|
726
742
|
return;
|
|
727
743
|
}
|
|
728
744
|
uploadingCount++;
|
|
745
|
+
store.set({ composerUploadingCount: uploadingCount });
|
|
729
746
|
renderInputPreviews();
|
|
730
747
|
var reader = new FileReader();
|
|
731
748
|
reader.onload = function (ev) {
|
|
@@ -738,12 +755,14 @@ function uploadFile(file) {
|
|
|
738
755
|
xhr.setRequestHeader("Content-Type", "application/json");
|
|
739
756
|
xhr.onload = function () {
|
|
740
757
|
uploadingCount--;
|
|
758
|
+
store.set({ composerUploadingCount: uploadingCount });
|
|
741
759
|
if (xhr.status === 200) {
|
|
742
760
|
try {
|
|
743
761
|
var resp = JSON.parse(xhr.responseText);
|
|
744
762
|
pendingFiles.push({ name: resp.name || uploadName, path: resp.path });
|
|
745
|
-
} catch (e) {}
|
|
763
|
+
} catch (e) { showToast('Upload response could not be read: ' + uploadName + '. Your draft is preserved.', 'error'); }
|
|
746
764
|
} else {
|
|
765
|
+
showToast('Upload failed: ' + uploadName + '. Your draft is preserved.', 'error');
|
|
747
766
|
if (ctx.addSystemMessage) ctx.addSystemMessage("Upload failed: " + uploadName, true);
|
|
748
767
|
}
|
|
749
768
|
renderInputPreviews();
|
|
@@ -753,6 +772,8 @@ function uploadFile(file) {
|
|
|
753
772
|
};
|
|
754
773
|
xhr.onerror = function () {
|
|
755
774
|
uploadingCount--;
|
|
775
|
+
store.set({ composerUploadingCount: uploadingCount });
|
|
776
|
+
showToast('Upload failed: ' + uploadName + '. Your draft is preserved.', 'error');
|
|
756
777
|
if (ctx.addSystemMessage) ctx.addSystemMessage("Upload failed: " + uploadName, true);
|
|
757
778
|
renderInputPreviews();
|
|
758
779
|
if (ctx.processing && ctx.setSendBtnMode) {
|
|
@@ -761,6 +782,7 @@ function uploadFile(file) {
|
|
|
761
782
|
};
|
|
762
783
|
xhr.send(JSON.stringify({ name: uploadName, data: b64 }));
|
|
763
784
|
};
|
|
785
|
+
reader.onerror = function () { uploadingCount--; store.set({ composerUploadingCount: uploadingCount }); renderInputPreviews(); showToast('Attachment could not be read. Your draft is preserved.', 'error'); };
|
|
764
786
|
reader.readAsDataURL(file);
|
|
765
787
|
}
|
|
766
788
|
|
|
@@ -772,7 +794,14 @@ function readImageBlob(blob) {
|
|
|
772
794
|
uploadFile(blob);
|
|
773
795
|
return;
|
|
774
796
|
}
|
|
797
|
+
uploadingCount++; store.set({ composerUploadingCount: uploadingCount });
|
|
798
|
+
var completed = false;
|
|
799
|
+
function finish() {
|
|
800
|
+
if (completed) return; completed = true; uploadingCount--;
|
|
801
|
+
store.set({ composerUploadingCount: uploadingCount }); renderInputPreviews();
|
|
802
|
+
}
|
|
775
803
|
var reader = new FileReader();
|
|
804
|
+
reader.onerror = function () { finish(); showToast('Image could not be read. Your draft is preserved.', 'error'); };
|
|
776
805
|
reader.onload = function (ev) {
|
|
777
806
|
var dataUrl = ev.target.result;
|
|
778
807
|
// Check base64 payload size (~3/4 of base64 length)
|
|
@@ -781,7 +810,7 @@ function readImageBlob(blob) {
|
|
|
781
810
|
var estimatedBytes = b64.length * 0.75;
|
|
782
811
|
|
|
783
812
|
if (estimatedBytes <= MAX_IMAGE_BYTES) {
|
|
784
|
-
addPendingImage(dataUrl);
|
|
813
|
+
addPendingImage(dataUrl); finish();
|
|
785
814
|
return;
|
|
786
815
|
}
|
|
787
816
|
|
|
@@ -799,8 +828,9 @@ function readImageBlob(blob) {
|
|
|
799
828
|
var cx = canvas.getContext("2d");
|
|
800
829
|
cx.drawImage(img, 0, 0, nw, nh);
|
|
801
830
|
var resized = canvas.toDataURL("image/jpeg", RESIZE_QUALITY);
|
|
802
|
-
addPendingImage(resized);
|
|
831
|
+
addPendingImage(resized); finish();
|
|
803
832
|
};
|
|
833
|
+
img.onerror = function () { finish(); showToast('Image could not be loaded. Your draft is preserved.', 'error'); };
|
|
804
834
|
img.src = dataUrl;
|
|
805
835
|
};
|
|
806
836
|
reader.readAsDataURL(blob);
|
|
@@ -869,7 +899,9 @@ function sendInputSync() {
|
|
|
869
899
|
if (dk) ctx.ws.send(JSON.stringify({ type: "dm_typing", dmKey: dk, typing: hasText }));
|
|
870
900
|
return;
|
|
871
901
|
}
|
|
872
|
-
|
|
902
|
+
var speech = store.get('speechActive');
|
|
903
|
+
ctx.ws.send(JSON.stringify({ type: "input_sync", text: ctx.inputEl.value,
|
|
904
|
+
paneBinding: speech && speech.writing ? speech.paneBinding || undefined : undefined }));
|
|
873
905
|
}
|
|
874
906
|
|
|
875
907
|
export function handleInputSync(text) {
|
|
@@ -893,7 +925,7 @@ function createFileInput(accept, capture, multiple) {
|
|
|
893
925
|
input.style.display = "none";
|
|
894
926
|
document.body.appendChild(input);
|
|
895
927
|
|
|
896
|
-
|
|
928
|
+
var selectFiles = sessionCallback(function () {
|
|
897
929
|
if (input.files) {
|
|
898
930
|
for (var i = 0; i < input.files.length; i++) {
|
|
899
931
|
if (input.files[i].type.indexOf("image/") === 0) {
|
|
@@ -903,6 +935,9 @@ function createFileInput(accept, capture, multiple) {
|
|
|
903
935
|
}
|
|
904
936
|
}
|
|
905
937
|
}
|
|
938
|
+
});
|
|
939
|
+
input.addEventListener("change", function () {
|
|
940
|
+
selectFiles();
|
|
906
941
|
document.body.removeChild(input);
|
|
907
942
|
});
|
|
908
943
|
|
|
@@ -991,6 +1026,14 @@ export function initInput(_ctx) {
|
|
|
991
1026
|
}
|
|
992
1027
|
}
|
|
993
1028
|
|
|
1029
|
+
document.addEventListener('clay-composer-handoff', function () {
|
|
1030
|
+
if (scheduleInlineInput) {
|
|
1031
|
+
var minutes = parseInt(scheduleInlineInput.value, 10);
|
|
1032
|
+
if (minutes >= 1 && minutes <= 1440) setScheduleDelayMs(minutes * 60000);
|
|
1033
|
+
}
|
|
1034
|
+
collapseScheduleBtn();
|
|
1035
|
+
});
|
|
1036
|
+
|
|
994
1037
|
function setScheduleDelay(ms) {
|
|
995
1038
|
scheduleDelayMs = ms;
|
|
996
1039
|
if (!scheduleBtn) return;
|
|
@@ -1020,7 +1063,7 @@ export function initInput(_ctx) {
|
|
|
1020
1063
|
scheduleInlineInput.className = "schedule-inline-input";
|
|
1021
1064
|
scheduleBtn.appendChild(scheduleInlineInput);
|
|
1022
1065
|
|
|
1023
|
-
setTimeout(function () { scheduleInlineInput.focus(); }, 0);
|
|
1066
|
+
setTimeout(sessionCallback(function () { if (scheduleInlineInput) scheduleInlineInput.focus(); }), 0);
|
|
1024
1067
|
|
|
1025
1068
|
scheduleInlineInput.addEventListener("click", function (e) { e.stopPropagation(); });
|
|
1026
1069
|
scheduleInlineInput.addEventListener("keydown", function (e) {
|
|
@@ -1039,6 +1082,7 @@ export function initInput(_ctx) {
|
|
|
1039
1082
|
|
|
1040
1083
|
// Close on outside click
|
|
1041
1084
|
setTimeout(function () {
|
|
1085
|
+
if (!scheduleInlineInput) return;
|
|
1042
1086
|
scheduleOutsideHandler = function (ev) {
|
|
1043
1087
|
if (!scheduleBtn.contains(ev.target)) {
|
|
1044
1088
|
if (scheduleInlineInput) {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { store } from './store.js';
|
|
4
4
|
import { getWs } from './ws-ref.js';
|
|
5
|
+
import { paneMessageBinding } from './pane-binding.js';
|
|
5
6
|
|
|
6
7
|
var ACK_TIMEOUT_MS = 5000;
|
|
7
8
|
var ACK_RETRY_LIMIT = 3;
|
|
@@ -152,7 +153,7 @@ function transmit(entry, automatic) {
|
|
|
152
153
|
}
|
|
153
154
|
socketRefs[entry.payload.clientMessageId] = socket;
|
|
154
155
|
try {
|
|
155
|
-
socket.send(JSON.stringify(entry.payload));
|
|
156
|
+
if (socket.send(JSON.stringify(entry.payload)) === false) { markUnconfirmed(entry); return false; }
|
|
156
157
|
armAckTimer(entry.payload.clientMessageId, socket);
|
|
157
158
|
return true;
|
|
158
159
|
} catch (e) {
|
|
@@ -163,6 +164,8 @@ function transmit(entry, automatic) {
|
|
|
163
164
|
|
|
164
165
|
export function sendAcknowledgedMessage(payload) {
|
|
165
166
|
var nextPayload = Object.assign({}, payload, { clientMessageId: createClientMessageId() });
|
|
167
|
+
var binding = paneMessageBinding();
|
|
168
|
+
if (binding && !nextPayload.paneBinding) nextPayload.paneBinding = binding;
|
|
166
169
|
var entry = {
|
|
167
170
|
projectSlug: store.get('currentSlug'),
|
|
168
171
|
sessionId: store.get('activeSessionId'),
|
|
@@ -193,7 +196,12 @@ export function sendAcknowledgedMessage(payload) {
|
|
|
193
196
|
export function retryPendingMessage(clientMessageId) {
|
|
194
197
|
var entry = pendingEntry(clientMessageId);
|
|
195
198
|
if (!matchesActivatedContext(entry) || hasKnownCliMismatch(entry)) return false;
|
|
196
|
-
|
|
199
|
+
// An explicit Retry in the same durable conversation may use its new live
|
|
200
|
+
// binding. Automatic retry retains the original action binding and refuses.
|
|
201
|
+
var payload = Object.assign({}, entry.payload);
|
|
202
|
+
var binding = paneMessageBinding();
|
|
203
|
+
if (binding) payload.paneBinding = binding;
|
|
204
|
+
var retryEntry = Object.assign({}, entry, { payload: payload, attempts: 0, status: "pending" });
|
|
197
205
|
replacePending(pendingMessages().map(function (candidate) {
|
|
198
206
|
return candidate.payload.clientMessageId === clientMessageId ? retryEntry : candidate;
|
|
199
207
|
}));
|
|
@@ -50,6 +50,8 @@ export function setupModelPicker() {
|
|
|
50
50
|
|
|
51
51
|
export function resetModelPickerState() {
|
|
52
52
|
clearRequestTimer();
|
|
53
|
+
if (configPopover) configPopover.classList.add('hidden');
|
|
54
|
+
if (configChip) configChip.classList.remove('active');
|
|
53
55
|
store.set({
|
|
54
56
|
modelListVendor: "",
|
|
55
57
|
modelListStatus: "idle",
|