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
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { getWs } from './ws-ref.js';
|
|
3
|
+
import { getCachedSessions } from './sidebar-sessions.js';
|
|
4
|
+
import { takeComposerDraft, restoreComposerDraft } from './input.js';
|
|
5
|
+
import { resetHistoryReplayBatch } from './history-replay-batch.js';
|
|
6
|
+
import { updateContextPanel } from './app-panels.js';
|
|
7
|
+
import { paneTarget, samePaneBinding, samePaneTarget } from './pane-binding.js';
|
|
8
|
+
|
|
9
|
+
function enabled() {
|
|
10
|
+
return store.get('paneMode') && !store.get('paneActivationUnsupported') && !store.get('activeProjectMateId') && !/^\/p\/mate-/.test(location.pathname) && !new URLSearchParams(location.search).has('contextual') && window.parent !== window;
|
|
11
|
+
}
|
|
12
|
+
function block(value) {
|
|
13
|
+
var app = document.getElementById('app');
|
|
14
|
+
if (app) app.inert = value;
|
|
15
|
+
document.documentElement.dataset.paneReady = value ? 'false' : 'true';
|
|
16
|
+
}
|
|
17
|
+
function draftKey() { return String(store.get('myUserId')) + ':' + store.get('sessionOriginId'); }
|
|
18
|
+
function clearDeadline() {
|
|
19
|
+
if (store.get('paneActivationTimer')) clearTimeout(store.get('paneActivationTimer'));
|
|
20
|
+
store.set({ paneActivationTimer: null });
|
|
21
|
+
}
|
|
22
|
+
function failed(requestId) {
|
|
23
|
+
var pending = store.get('panePendingBinding');
|
|
24
|
+
if (!pending || requestId !== pending.requestId) return;
|
|
25
|
+
clearDeadline();
|
|
26
|
+
store.set({ paneReady: false, panePendingBinding: null });
|
|
27
|
+
block(true);
|
|
28
|
+
var frame = window.frameElement;
|
|
29
|
+
if (frame) frame.dispatchEvent(new window.parent.CustomEvent('clay-pane-activation-failed'));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Claim ordinary initial pins before the legacy session-list handler runs.
|
|
33
|
+
// Authentication, list and group metadata can arrive in either order; the
|
|
34
|
+
// subscription retries when those authoritative prerequisites become ready.
|
|
35
|
+
export function primePaneSession() {
|
|
36
|
+
if (!enabled() || !window.frameElement || !window.frameElement.clayPane) return false;
|
|
37
|
+
var id = store.get('paneSessionId');
|
|
38
|
+
var session = (getCachedSessions() || []).find(function (item) { return item.id === id; });
|
|
39
|
+
if (!session || session.isMate || (session.runtimeMode || session.mode || 'gui') !== 'gui' || session.tuiSuspended) return false;
|
|
40
|
+
activatePaneSession(id);
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function activatePaneSession(id, force, expectedTarget) {
|
|
45
|
+
var state = store.snap(), ws = getWs();
|
|
46
|
+
if (state.authIdentityLoaded === false) return false;
|
|
47
|
+
if (state.isMultiUserMode && !state.myUserId) return false;
|
|
48
|
+
if (!enabled() || !state.connected || !ws || ws.readyState !== 1 || state.splitGroupsProjectSlug !== state.currentSlug) return false;
|
|
49
|
+
var target = paneTarget(state, getCachedSessions(), id);
|
|
50
|
+
if (!target) return false;
|
|
51
|
+
if (expectedTarget && !samePaneTarget(target, expectedTarget)) return false;
|
|
52
|
+
var sourceFrame = window.frameElement;
|
|
53
|
+
if (!expectedTarget && sourceFrame && Number(sourceFrame.dataset && sourceFrame.dataset.sessionId) === id && sourceFrame.dataset.sessionOriginId && sourceFrame.dataset.sessionOriginId !== target.sessionOriginId) return false;
|
|
54
|
+
var old = state.paneCommittedBinding, pending = state.panePendingBinding;
|
|
55
|
+
if (!force && !state.paneSuspended && samePaneTarget(pending || old, target)) return true;
|
|
56
|
+
if (!pending && state.sessionOriginId) {
|
|
57
|
+
var draft;
|
|
58
|
+
try { draft = takeComposerDraft(); } catch (error) { return false; }
|
|
59
|
+
var drafts = Object.assign({}, state.paneRichDrafts || {});
|
|
60
|
+
drafts[draftKey()] = draft;
|
|
61
|
+
store.set({ paneRichDrafts: drafts });
|
|
62
|
+
}
|
|
63
|
+
clearDeadline();
|
|
64
|
+
resetHistoryReplayBatch();
|
|
65
|
+
var generation = (store.get('paneActivationGeneration') || 0) + 1;
|
|
66
|
+
target = Object.assign({}, target, { generation: generation, requestId: 'pane-' + generation + '-' + Math.random().toString(36).slice(2, 12) });
|
|
67
|
+
var path = new URL(store.get('wsPath'), location.origin);
|
|
68
|
+
path.searchParams.set('session', String(id));
|
|
69
|
+
store.set({ paneActivationGeneration: generation, panePendingBinding: target, paneReady: false, paneSuspended: false,
|
|
70
|
+
paneSessionId: id, panePinPending: false, wsPath: path.pathname + path.search });
|
|
71
|
+
block(true);
|
|
72
|
+
store.set({ paneActivationTimer: setTimeout(function () { failed(target.requestId); }, 12000) });
|
|
73
|
+
ws.send(JSON.stringify({ type: 'switch_session', id: id, paneActivation: target }));
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Runs before replay batching, so an obsolete history_meta cannot open a batch
|
|
78
|
+
// that later consumes destination messages.
|
|
79
|
+
export function filterPaneActivation(message) {
|
|
80
|
+
if (!enabled()) return false;
|
|
81
|
+
var pending = store.get('panePendingBinding'), committed = store.get('paneCommittedBinding');
|
|
82
|
+
if (message.type === 'pane_activation_rejected') { failed(message.requestId); return true; }
|
|
83
|
+
if (!pending && !committed) return !!message.paneActivation;
|
|
84
|
+
if (['session_list', 'split_groups', 'session_presence', 'pong', 'account_auth_required', 'project_removed', 'project_access_revoked'].indexOf(message.type) >= 0 && !message.paneActivation) return false;
|
|
85
|
+
var expected = pending || committed;
|
|
86
|
+
if (!samePaneBinding(expected, message.paneActivation)) return true;
|
|
87
|
+
if (message.type === 'pane_activation_started') return true;
|
|
88
|
+
if (message.type === 'pane_activation_snapshot_done') {
|
|
89
|
+
if (!pending) return true;
|
|
90
|
+
var draft = (store.get('paneRichDrafts') || {})[String(store.get('myUserId')) + ':' + expected.sessionOriginId];
|
|
91
|
+
restoreComposerDraft(draft || { text: '', start: 0, end: 0 });
|
|
92
|
+
function paint() {
|
|
93
|
+
if (store.get('panePendingBinding') !== expected || !store.get('connected') || store.get('paneSuspended')) return;
|
|
94
|
+
var current = paneTarget(store.snap(), getCachedSessions(), expected.sessionId);
|
|
95
|
+
var messages = document.getElementById('messages');
|
|
96
|
+
if (!samePaneTarget(current, expected) ||
|
|
97
|
+
store.get('activeSessionId') !== expected.sessionId || store.get('sessionOriginId') !== expected.sessionOriginId || store.get('replayingHistory') ||
|
|
98
|
+
!messages || messages.dataset.controlSessionId !== String(expected.sessionId) || messages.dataset.controlSessionOrigin !== expected.sessionOriginId) { failed(expected.requestId); return; }
|
|
99
|
+
clearDeadline();
|
|
100
|
+
store.set({ paneCommittedBinding: expected, panePendingBinding: null, paneReady: true });
|
|
101
|
+
block(false);
|
|
102
|
+
var frame = window.frameElement;
|
|
103
|
+
if (frame) frame.dispatchEvent(new window.parent.CustomEvent('clay-pane-ready'));
|
|
104
|
+
updateContextPanel();
|
|
105
|
+
}
|
|
106
|
+
requestAnimationFrame(function () { requestAnimationFrame(paint); });
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function paneOutgoing(data) {
|
|
113
|
+
var message;
|
|
114
|
+
try { message = JSON.parse(data); } catch (error) { return null; }
|
|
115
|
+
if (['ping', 'pong', 'clay_ui_ready'].indexOf(message.type) >= 0) return data;
|
|
116
|
+
if (store.get('deferredSessionSwitch') && store.get('deferredSessionSwitch').draining) return null;
|
|
117
|
+
if (message.type === 'switch_session') {
|
|
118
|
+
var entry = store.get('pendingSessionEntryNavigation');
|
|
119
|
+
if (entry && entry.projectSlug === store.get('currentSlug') && String(entry.sessionId) !== String(message.id)) store.set({ pendingSessionEntryNavigation: null });
|
|
120
|
+
if (enabled() && (store.get('panePendingBinding') || store.get('paneCommittedBinding')) && !message.paneActivation) return null;
|
|
121
|
+
return data;
|
|
122
|
+
}
|
|
123
|
+
if (store.get('deferredSessionSwitch')) {
|
|
124
|
+
var source = store.get('deferredSessionSwitch');
|
|
125
|
+
if (source.draining) return null;
|
|
126
|
+
if (message.type === 'stop') return JSON.stringify(Object.assign({}, message, { sessionId: source.sessionId, sessionOriginId: source.origin }));
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
if (!enabled()) return data;
|
|
130
|
+
var binding = store.get('paneCommittedBinding');
|
|
131
|
+
if (store.get('panePendingBinding') || store.get('paneSuspended') || binding && !store.get('paneReady')) return null;
|
|
132
|
+
if (!binding) return ['stop', 'user_message', 'permission_response', 'ask_user_response', 'elicitation_response', 'user_dialog_response', 'input_sync'].indexOf(message.type) >= 0 ? null : data;
|
|
133
|
+
if (message.paneBinding && !samePaneBinding(binding, message.paneBinding)) return null;
|
|
134
|
+
var gesture = (store.get('paneActionEvents') || []).find(function (entry) { return entry.event.isTrusted && entry.event.eventPhase && samePaneBinding(entry.binding, binding); });
|
|
135
|
+
var action = store.get('paneCallbackBinding') || gesture && gesture.binding;
|
|
136
|
+
var read = /^(get_|list_)|(_get|_list|_options)$/.test(message.type) || message.type === 'load_more_history';
|
|
137
|
+
if (!message.paneBinding && !read && !samePaneBinding(action, binding)) return null;
|
|
138
|
+
return JSON.stringify(Object.assign({}, message, { paneBinding: message.paneBinding || action || binding }));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function refreshPaneActivation() {
|
|
142
|
+
if (!enabled()) return;
|
|
143
|
+
var binding = store.get('panePendingBinding') || store.get('paneCommittedBinding');
|
|
144
|
+
if (!binding) return;
|
|
145
|
+
var current = paneTarget(store.snap(), getCachedSessions(), binding.sessionId);
|
|
146
|
+
if (samePaneTarget(current, binding)) return;
|
|
147
|
+
if (store.get('panePendingBinding')) failed(binding.requestId);
|
|
148
|
+
else { store.set({ paneReady: false }); block(true); }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function initPaneActivation() {
|
|
152
|
+
if (!enabled() || !window.frameElement) return;
|
|
153
|
+
var frame = window.frameElement;
|
|
154
|
+
var latestId = Number(frame.dataset && (frame.dataset.requestedSessionId || frame.dataset.sessionId));
|
|
155
|
+
if (Number.isSafeInteger(latestId) && latestId > 0 && frame.dataset.projectSlug === store.get('currentSlug')) {
|
|
156
|
+
var latestPath = new URL(store.get('wsPath'), location.origin);
|
|
157
|
+
latestPath.searchParams.set('session', String(latestId));
|
|
158
|
+
store.set({ paneSessionId: latestId, panePinPending: true, wsPath: latestPath.pathname + latestPath.search });
|
|
159
|
+
}
|
|
160
|
+
block(true);
|
|
161
|
+
frame.clayPane = {
|
|
162
|
+
source: window, document: document,
|
|
163
|
+
identity: function () {
|
|
164
|
+
var binding = store.get('paneCommittedBinding');
|
|
165
|
+
return binding && { ownerId: binding.viewerId, projectSlug: binding.projectSlug, sessionId: binding.sessionId,
|
|
166
|
+
binding: binding, ready: store.get('connected') && store.get('paneReady') && !store.get('paneSuspended') };
|
|
167
|
+
},
|
|
168
|
+
activate: activatePaneSession,
|
|
169
|
+
checkDraft: function () { if (!store.get('panePendingBinding')) { var draft = takeComposerDraft(); restoreComposerDraft(draft); } },
|
|
170
|
+
suspend: function () { store.set({ paneSuspended: true, paneReady: false }); block(true); },
|
|
171
|
+
};
|
|
172
|
+
// A pointer that began on the previous conversation cannot become a Stop or
|
|
173
|
+
// approval click on the replacement conversation after hydration.
|
|
174
|
+
document.addEventListener('pointerdown', function () { store.set({ panePointerBinding: store.get('paneCommittedBinding') }); }, true);
|
|
175
|
+
document.addEventListener('click', function (event) {
|
|
176
|
+
if (store.get('paneSuspended') || !store.get('paneReady') && store.get('panePendingBinding') || event.detail && store.get('panePointerBinding') !== store.get('paneCommittedBinding')) {
|
|
177
|
+
event.preventDefault(); event.stopImmediatePropagation();
|
|
178
|
+
}
|
|
179
|
+
}, true);
|
|
180
|
+
['click', 'input', 'change', 'keydown', 'keyup', 'submit', 'blur'].forEach(function (name) {
|
|
181
|
+
document.addEventListener(name, function (event) {
|
|
182
|
+
var keys = store.get('paneHeldKeys') || {};
|
|
183
|
+
var binding = store.get('paneCommittedBinding');
|
|
184
|
+
var previousKey = keys[event.code || event.key];
|
|
185
|
+
var staleKey = (name === 'keyup' || name === 'keydown' && event.repeat) && previousKey && !samePaneBinding(previousKey, binding);
|
|
186
|
+
if (name === 'keydown' && !event.repeat) keys[event.code || event.key] = binding;
|
|
187
|
+
if (name === 'keyup') delete keys[event.code || event.key];
|
|
188
|
+
if (staleKey || store.get('panePendingBinding') || store.get('paneSuspended') || !store.get('paneReady')) {
|
|
189
|
+
event.preventDefault(); event.stopImmediatePropagation(); return;
|
|
190
|
+
}
|
|
191
|
+
var events = (store.get('paneActionEvents') || []).filter(function (entry) { return entry.event.eventPhase; });
|
|
192
|
+
events.push({ event: event, binding: binding });
|
|
193
|
+
store.set({ paneHeldKeys: keys, paneActionEvents: events });
|
|
194
|
+
}, true);
|
|
195
|
+
});
|
|
196
|
+
store.subscribe(function (state, previous) {
|
|
197
|
+
if (state.activeSessionMode === 'tui' && previous.activeSessionMode !== 'tui') {
|
|
198
|
+
delete frame.clayPane; store.set({ paneActivationUnsupported: true }); block(false); return;
|
|
199
|
+
}
|
|
200
|
+
if (!state.connected && previous.connected) {
|
|
201
|
+
clearDeadline();
|
|
202
|
+
store.set({ paneReady: false, panePendingBinding: null, paneCommittedBinding: null }); block(true);
|
|
203
|
+
}
|
|
204
|
+
if (state.connected && (state.splitGroups !== previous.splitGroups || state.splitGroupsProjectSlug !== previous.splitGroupsProjectSlug || state.paneBindingSessions !== previous.paneBindingSessions || state.authIdentityLoaded !== previous.authIdentityLoaded || state.myUserId !== previous.myUserId || state.activeSessionId !== previous.activeSessionId || state.connected !== previous.connected)) {
|
|
205
|
+
queueMicrotask(function () {
|
|
206
|
+
refreshPaneActivation();
|
|
207
|
+
if (!store.get('panePendingBinding') && !store.get('paneSuspended')) {
|
|
208
|
+
if (store.get('panePinPending')) primePaneSession();
|
|
209
|
+
else if (store.get('activeSessionId') === store.get('paneSessionId')) activatePaneSession(store.get('paneSessionId'));
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { getWs } from './ws-ref.js';
|
|
3
|
+
|
|
4
|
+
export function captureSessionAction() {
|
|
5
|
+
var state = store.snap();
|
|
6
|
+
return { projectSlug: state.currentSlug, ownerId: state.myUserId, sessionId: state.activeSessionId,
|
|
7
|
+
origin: state.sessionOriginId, socket: getWs(), binding: state.paneCommittedBinding || null,
|
|
8
|
+
epoch: state.sessionActionEpoch || 0 };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function sessionActionCurrent(action, draftCompletion) {
|
|
12
|
+
var state = store.snap();
|
|
13
|
+
return !!action && action.socket === getWs() && state.connected &&
|
|
14
|
+
action.projectSlug === state.currentSlug && action.ownerId === state.myUserId &&
|
|
15
|
+
action.sessionId === state.activeSessionId && action.origin === state.sessionOriginId &&
|
|
16
|
+
action.epoch === (state.sessionActionEpoch || 0) &&
|
|
17
|
+
(draftCompletion || !state.deferredSessionSwitch) && !state.panePendingBinding && !state.paneSuspended &&
|
|
18
|
+
(action.binding ? state.paneReady && samePaneBinding(action.binding, state.paneCommittedBinding) : !state.paneCommittedBinding);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Capture at callback creation, never when an old callback eventually sends.
|
|
22
|
+
export function sessionCallback(callback, draftCompletion) {
|
|
23
|
+
var action = captureSessionAction();
|
|
24
|
+
return function () {
|
|
25
|
+
if (!sessionActionCurrent(action, draftCompletion)) return;
|
|
26
|
+
var previous = store.get('paneCallbackBinding');
|
|
27
|
+
store.set({ paneCallbackBinding: action.binding });
|
|
28
|
+
try { return callback.apply(this, arguments); }
|
|
29
|
+
finally { store.set({ paneCallbackBinding: previous }); }
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function samePaneBinding(a, b) {
|
|
34
|
+
return !!a && !!b && ['requestId', 'generation', 'projectSlug', 'viewerId', 'ownerId', 'sessionId', 'sessionOriginId', 'role', 'workerGeneration', 'groupStamp', 'parentSessionOriginId'].every(function (key) { return a[key] === b[key]; });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function samePaneTarget(a, b) {
|
|
38
|
+
return !!a && !!b && ['projectSlug', 'viewerId', 'ownerId', 'sessionId', 'sessionOriginId', 'role', 'workerGeneration', 'groupStamp', 'parentSessionOriginId'].every(function (key) { return a[key] === b[key]; });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function paneTarget(state, sessions, id) {
|
|
42
|
+
var session = (sessions || []).find(function (item) { return item.id === id; });
|
|
43
|
+
if (!session || session.isMate || !session.sessionOriginId || (session.runtimeMode || session.mode || 'gui') !== 'gui' || session.tuiSuspended) return null;
|
|
44
|
+
var group = (state.splitGroups || []).find(function (item) { return item.members.indexOf(id) >= 0; });
|
|
45
|
+
var parent = session.sessionRole === 'worker' && sessions.find(function (item) { return item.id === session.parentSessionId; });
|
|
46
|
+
if (session.sessionRole === 'worker' && (!parent || !session.parentAvailable || !Number.isSafeInteger(session.workerGeneration) || !parent.sessionOriginId || parent.ownerId !== session.ownerId || !group || !group.pair || group.pair.driverId !== parent.id)) return null;
|
|
47
|
+
return {
|
|
48
|
+
projectSlug: state.currentSlug, viewerId: state.myUserId || null, ownerId: session.ownerId || null,
|
|
49
|
+
sessionId: session.id, sessionOriginId: session.sessionOriginId,
|
|
50
|
+
role: session.sessionRole === 'worker' ? 'worker' : 'driver', workerGeneration: session.workerGeneration || null,
|
|
51
|
+
parentSessionOriginId: parent ? parent.sessionOriginId : null,
|
|
52
|
+
groupStamp: group ? JSON.stringify({ id: group.id, members: group.members, pair: group.pair || null }) : null,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Ordinary reusable panes have a live document binding; legacy/canonical frames
|
|
57
|
+
// retain their existing URL and receipt checks in their own bridge paths.
|
|
58
|
+
export function livePaneBinding(frame, message) {
|
|
59
|
+
if (!frame || !frame.clayPane) return false;
|
|
60
|
+
var api = frame.clayPane, live = api.identity();
|
|
61
|
+
var state = store.snap(), split = state.splitPanes;
|
|
62
|
+
var target = live && paneTarget(state, state.paneBindingSessions || [], live.sessionId);
|
|
63
|
+
return api.source === frame.contentWindow && api.document === frame.contentDocument && live && live.ready &&
|
|
64
|
+
samePaneTarget(target, live.binding) &&
|
|
65
|
+
live.ownerId === (state.myUserId || null) && live.projectSlug === state.currentSlug &&
|
|
66
|
+
split && split.panes.some(function (pane) { return pane.slug === live.projectSlug && pane.sessionId === live.sessionId; }) &&
|
|
67
|
+
(!message || samePaneBinding(live.binding, message.paneBinding));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function paneMessageBinding() {
|
|
71
|
+
return store.get('paneReady') && !store.get('paneSuspended') ? store.get('paneCommittedBinding') : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function currentPaneReview(container) {
|
|
75
|
+
if (!container || !container.paneBinding) return true;
|
|
76
|
+
return container.isConnected !== false && store.get('paneReady') && !store.get('paneSuspended') && samePaneBinding(container.paneBinding, store.get('paneCommittedBinding'));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function paneCallbackCurrent(binding) {
|
|
80
|
+
var current = store.get('panePendingBinding') || store.get('paneCommittedBinding');
|
|
81
|
+
return current ? samePaneBinding(current, binding) : !binding;
|
|
82
|
+
}
|
|
@@ -5,6 +5,7 @@ import { forwardStudioPresentation } from './clay-studio-render.js';
|
|
|
5
5
|
// Same-origin messages only, both directions.
|
|
6
6
|
|
|
7
7
|
import { store } from './store.js';
|
|
8
|
+
import { paneMessageBinding, samePaneBinding } from './pane-binding.js';
|
|
8
9
|
import { getEffectiveContextView } from './context-view-preference.js';
|
|
9
10
|
import { forceExternalLinkToNewTab } from './pane-links.js';
|
|
10
11
|
|
|
@@ -12,6 +13,7 @@ export function reportPaneContext(data) {
|
|
|
12
13
|
if (!store.get('paneMode') || window.parent === window) return;
|
|
13
14
|
window.parent.postMessage({
|
|
14
15
|
type: "clay-pane-context",
|
|
16
|
+
paneBinding: paneMessageBinding(),
|
|
15
17
|
sessionId: store.get('activeSessionId'),
|
|
16
18
|
pct: data.pct,
|
|
17
19
|
used: data.used,
|
|
@@ -27,7 +29,7 @@ export function reportPaneContext(data) {
|
|
|
27
29
|
// Hand the event to the parent shell, which owns the single flow.
|
|
28
30
|
export function forwardPaneAuthRequired(message) {
|
|
29
31
|
if (!store.get('paneMode') || window.parent === window) return false;
|
|
30
|
-
window.parent.postMessage({ type: "clay-pane-auth-required", message: message }, window.location.origin);
|
|
32
|
+
window.parent.postMessage({ type: "clay-pane-auth-required", paneBinding: paneMessageBinding(), message: message }, window.location.origin);
|
|
31
33
|
return true;
|
|
32
34
|
}
|
|
33
35
|
|
|
@@ -38,7 +40,7 @@ export function forwardPaneMarkdownPresentation(message) {
|
|
|
38
40
|
|
|
39
41
|
export function forwardPaneIssueReference(ref, projectSlug) {
|
|
40
42
|
if (!store.get('paneMode') || window.parent === window) return false;
|
|
41
|
-
window.parent.postMessage({ type: "clay-pane-open-issue", ref: ref, projectSlug: projectSlug || store.get('currentSlug') }, window.location.origin);
|
|
43
|
+
window.parent.postMessage({ type: "clay-pane-open-issue", paneBinding: paneMessageBinding(), ref: ref, projectSlug: projectSlug || store.get('currentSlug') }, window.location.origin);
|
|
42
44
|
return true;
|
|
43
45
|
}
|
|
44
46
|
|
|
@@ -70,6 +72,7 @@ export function initPaneBridge() {
|
|
|
70
72
|
if (event.origin !== window.location.origin) return;
|
|
71
73
|
var msg = event.data;
|
|
72
74
|
if (!msg || msg.type !== "clay-pane-toggle-context") return;
|
|
75
|
+
if (event.source !== window.parent || store.get('paneCommittedBinding') && !samePaneBinding(paneMessageBinding(), msg.paneBinding)) return;
|
|
73
76
|
togglePaneContextPanel();
|
|
74
77
|
});
|
|
75
78
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { store } from './store.js';
|
|
2
|
+
import { livePaneBinding, paneMessageBinding } from './pane-binding.js';
|
|
2
3
|
import { showToast } from './utils.js';
|
|
3
4
|
|
|
4
5
|
export function forwardPaneBrowserLink(url) {
|
|
5
6
|
if (!store.get('paneMode') || window.parent === window) return false;
|
|
6
|
-
window.parent.postMessage({ type: 'clay-pane-open-browser', url: url, projectSlug: store.get('currentSlug'), sessionId: String(store.get('activeSessionId')) }, window.location.origin);
|
|
7
|
+
window.parent.postMessage({ type: 'clay-pane-open-browser', paneBinding: paneMessageBinding(), url: url, projectSlug: store.get('currentSlug'), sessionId: String(store.get('activeSessionId')) }, window.location.origin);
|
|
7
8
|
return true;
|
|
8
9
|
}
|
|
9
10
|
export function handlePaneBrowserMessage(event, host) {
|
|
@@ -13,8 +14,9 @@ export function handlePaneBrowserMessage(event, host) {
|
|
|
13
14
|
var pane = frame && s.splitPanes && s.splitPanes.panes && s.splitPanes.panes.find(function (item) { return String(item.slug) === frame.dataset.projectSlug && String(item.sessionId) === frame.dataset.sessionId; });
|
|
14
15
|
if (!pane || msg.projectSlug !== s.currentSlug || msg.projectSlug !== frame.dataset.projectSlug || String(msg.sessionId) !== frame.dataset.sessionId) return true;
|
|
15
16
|
var expected = new URL('/p/' + encodeURIComponent(pane.slug) + '/?pane=1&session=' + encodeURIComponent(pane.sessionId), window.location.href).href;
|
|
16
|
-
if (frame.src !== expected) return true;
|
|
17
|
+
if (frame.clayPane ? !livePaneBinding(frame, msg) : frame.src !== expected) return true;
|
|
17
18
|
// The authenticated host owns the browser; a live same-project pane supplies only a URL.
|
|
18
|
-
|
|
19
|
+
var binding = frame.clayPane && frame.clayPane.identity().binding;
|
|
20
|
+
import('./shared-browser.js').then(function (module) { if (binding && (!livePaneBinding(frame, msg) || frame.clayPane.identity().binding !== binding)) return; module.openSharedBrowserAt(msg.url); }).catch(function (error) { showToast(error.message || 'Could not open the browser.', 'warn'); });
|
|
19
21
|
return true;
|
|
20
22
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { store } from './store.js';
|
|
2
|
+
import { livePaneBinding, paneMessageBinding } from './pane-binding.js';
|
|
2
3
|
|
|
3
4
|
function paneFrame(host, source) {
|
|
4
5
|
if (!host || !source) return null;
|
|
@@ -19,6 +20,7 @@ function matchingPane(frame, split) {
|
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
function hasLivePaneSource(frame, pane) {
|
|
23
|
+
if (frame && frame.clayPane) return livePaneBinding(frame);
|
|
22
24
|
if (!frame || !pane || typeof frame.src !== "string") return false;
|
|
23
25
|
var expected = "/p/" + encodeURIComponent(pane.slug) + "/?pane=1&session=" + encodeURIComponent(pane.sessionId);
|
|
24
26
|
try { return new URL(expected, window.location.href).href === frame.src; } catch (e) { return false; }
|
|
@@ -41,6 +43,7 @@ export function forwardPaneFileReference(path, opts) {
|
|
|
41
43
|
if (!store.get('paneMode') || window.parent === window || typeof path !== "string" || !path) return false;
|
|
42
44
|
window.parent.postMessage({
|
|
43
45
|
type: "clay-pane-open-file",
|
|
46
|
+
paneBinding: paneMessageBinding(),
|
|
44
47
|
path: path,
|
|
45
48
|
line: opts && opts.line != null ? opts.line : null,
|
|
46
49
|
column: opts && opts.column != null ? opts.column : null,
|
|
@@ -57,6 +60,7 @@ export function handlePaneFileMessage(event, host, openFile, openFiles) {
|
|
|
57
60
|
var frame = paneFrame(host, event.source);
|
|
58
61
|
var state = store.snap();
|
|
59
62
|
var pane = matchingPane(frame, state.splitPanes);
|
|
63
|
+
if (frame && frame.clayPane && !livePaneBinding(frame, msg)) return true;
|
|
60
64
|
if (!pane || !hasLivePaneSource(frame, pane) || String(pane.slug) !== String(state.currentSlug) || msg.projectSlug !== frame.dataset.projectSlug || String(msg.sessionId) !== String(frame.dataset.sessionId) || !validLocation(msg.line) || !validLocation(msg.column)) return true;
|
|
61
65
|
if (typeof msg.path !== "string" || !msg.path) return true;
|
|
62
66
|
if (!state.connected || typeof openFile !== "function") return true;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { store } from './store.js';
|
|
2
|
+
import { livePaneBinding } from './pane-binding.js';
|
|
2
3
|
import { openIssues } from './issues.js';
|
|
3
4
|
|
|
4
5
|
function isIssueReference(ref) {
|
|
@@ -8,7 +9,7 @@ function isIssueReference(ref) {
|
|
|
8
9
|
function isPaneSource(host, source) {
|
|
9
10
|
var frames = host.querySelectorAll(".split-pane-frame");
|
|
10
11
|
for (var i = 0; i < frames.length; i++) {
|
|
11
|
-
if (frames[i].contentWindow === source) return
|
|
12
|
+
if (frames[i].contentWindow === source) return !frames[i].clayPane || livePaneBinding(frames[i]);
|
|
12
13
|
}
|
|
13
14
|
return false;
|
|
14
15
|
}
|
|
@@ -18,6 +19,8 @@ export function handlePaneIssueMessage(event, host) {
|
|
|
18
19
|
var msg = event.data;
|
|
19
20
|
if (!msg || msg.type !== "clay-pane-open-issue") return false;
|
|
20
21
|
if (!isIssueReference(msg.ref) || !isPaneSource(host, event.source)) return true;
|
|
22
|
+
var frame = Array.from(host.querySelectorAll('.split-pane-frame')).find(function (item) { return item.contentWindow === event.source; });
|
|
23
|
+
if (frame && frame.clayPane && !livePaneBinding(frame, msg)) return true;
|
|
21
24
|
|
|
22
25
|
var issueTarget = store.get('issuesCache') && store.get('issuesCache')[msg.ref];
|
|
23
26
|
var projectSlug = (issueTarget && issueTarget.projectSlug) || msg.projectSlug || store.get('currentSlug');
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { escapeHtml } from './utils.js';
|
|
2
2
|
import { iconHtml, refreshIcons } from './icons.js';
|
|
3
|
+
import { store } from './store.js';
|
|
4
|
+
import { captureSessionAction, sessionActionCurrent } from './pane-binding.js';
|
|
3
5
|
|
|
4
6
|
var ctx;
|
|
5
7
|
var rewindMode = false;
|
|
@@ -81,6 +83,7 @@ function initiateRewind(uuid) {
|
|
|
81
83
|
return;
|
|
82
84
|
}
|
|
83
85
|
pendingRewindUuid = uuid;
|
|
86
|
+
store.set({ rewindAction: captureSessionAction() });
|
|
84
87
|
rewindPreviewInFlight = true;
|
|
85
88
|
if (ctx.ws && ctx.connected) {
|
|
86
89
|
ctx.ws.send(JSON.stringify({ type: "rewind_preview", uuid: uuid }));
|
|
@@ -120,6 +123,7 @@ function updateSummaryForMode() {
|
|
|
120
123
|
}
|
|
121
124
|
|
|
122
125
|
export function showRewindModal(data) {
|
|
126
|
+
if (!sessionActionCurrent(store.get('rewindAction'))) return;
|
|
123
127
|
rewindPreviewInFlight = false;
|
|
124
128
|
|
|
125
129
|
// Ignore stale preview results that don't match current pending UUID
|
|
@@ -367,6 +371,7 @@ export function initRewind(_ctx) {
|
|
|
367
371
|
});
|
|
368
372
|
|
|
369
373
|
rewindConfirmBtn.addEventListener("click", function() {
|
|
374
|
+
if (!sessionActionCurrent(store.get('rewindAction'))) { hideRewindModal(); return; }
|
|
370
375
|
if (rewindExecuting) return;
|
|
371
376
|
if (pendingRewindUuid && ctx.ws && ctx.connected) {
|
|
372
377
|
rewindExecuting = true;
|
|
@@ -4,6 +4,7 @@ import { addSystemMessage, VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.
|
|
|
4
4
|
import { openPairDialog } from './split-pair-ui.js';
|
|
5
5
|
import { buildAgentVendorSelect, fillAgentModels, buildAgentEffortSelect, fillAgentEffort } from './agent-config-selects.js';
|
|
6
6
|
import { showToast } from './utils.js';
|
|
7
|
+
import { sessionCallback, captureSessionAction, sessionActionCurrent } from './pane-binding.js';
|
|
7
8
|
|
|
8
9
|
var addWorkerButton = null;
|
|
9
10
|
var handoffButton = null;
|
|
@@ -33,6 +34,7 @@ function handoffField(labelText, control) {
|
|
|
33
34
|
function requestHandoffDialog() {
|
|
34
35
|
var ws = getWs();
|
|
35
36
|
if (!ws || ws.readyState !== 1) return;
|
|
37
|
+
store.set({ handoffAction: captureSessionAction() });
|
|
36
38
|
ws.send(JSON.stringify({ type: "handoff_session_options" }));
|
|
37
39
|
}
|
|
38
40
|
|
|
@@ -47,6 +49,7 @@ function openWorkerDialog() {
|
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
function showHandoffDialog(options) {
|
|
52
|
+
if (!sessionActionCurrent(store.get('handoffAction'))) return;
|
|
50
53
|
closeHandoffDialog();
|
|
51
54
|
var installed = options.installedVendors || [];
|
|
52
55
|
var vendorInfo = store.get('vendorInfo') || {};
|
|
@@ -147,7 +150,7 @@ function showHandoffDialog(options) {
|
|
|
147
150
|
if (event.key === "Escape") closeHandoffDialog();
|
|
148
151
|
};
|
|
149
152
|
document.addEventListener("keydown", handoffEscapeHandler);
|
|
150
|
-
submit.addEventListener("click", function () {
|
|
153
|
+
submit.addEventListener("click", sessionCallback(function () {
|
|
151
154
|
var ws = getWs();
|
|
152
155
|
if (!ws || ws.readyState !== 1) return;
|
|
153
156
|
ws.send(JSON.stringify({
|
|
@@ -158,7 +161,7 @@ function showHandoffDialog(options) {
|
|
|
158
161
|
}));
|
|
159
162
|
submit.disabled = true;
|
|
160
163
|
submit.textContent = "Continuing…";
|
|
161
|
-
});
|
|
164
|
+
}));
|
|
162
165
|
vendorSelect.focus();
|
|
163
166
|
}
|
|
164
167
|
|
|
@@ -109,6 +109,8 @@ function currentRenameCanSend(state) {
|
|
|
109
109
|
String(store.get('activeSessionId')) === String(state.sessionId) :
|
|
110
110
|
state && state.row && String(state.row.dataset.sessionId) === String(state.sessionId);
|
|
111
111
|
return !!(state && state.row && state.row.isConnected && sameSession &&
|
|
112
|
+
(state.source !== 'header' || state.origin === store.get('sessionOriginId')) &&
|
|
113
|
+
state.epoch === (store.get('sessionActionEpoch') || 0) && !store.get('panePendingBinding') && !store.get('deferredSessionSwitch') &&
|
|
112
114
|
store.get('connected') && state.ws === ws && ws && ws.readyState === 1 &&
|
|
113
115
|
state.slug === store.get('currentSlug') &&
|
|
114
116
|
(!permissions || permissions.sessionRename !== false));
|
|
@@ -158,6 +160,8 @@ function beginSessionInlineRename(row, title, control, session, source, inputCla
|
|
|
158
160
|
input: input,
|
|
159
161
|
originalNodes: originalNodes,
|
|
160
162
|
sessionId: session.id,
|
|
163
|
+
origin: session.sessionOriginId,
|
|
164
|
+
epoch: store.get('sessionActionEpoch') || 0,
|
|
161
165
|
currentTitle: session.title || "New Session",
|
|
162
166
|
control: control || null,
|
|
163
167
|
controlDisplay: controlDisplay,
|
|
@@ -202,6 +206,7 @@ export function startHeaderSessionInlineRename(title, control, session) {
|
|
|
202
206
|
|
|
203
207
|
store.subscribe(function (state, previous) {
|
|
204
208
|
if (state.currentSlug !== previous.currentSlug || state.dmMode !== previous.dmMode ||
|
|
209
|
+
state.sessionActionEpoch !== previous.sessionActionEpoch || state.panePendingBinding !== previous.panePendingBinding ||
|
|
205
210
|
(previous.connected && !state.connected)) {
|
|
206
211
|
closeSessionContextMenu(false);
|
|
207
212
|
cancelSessionInlineRename(false);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { takeComposerDraft, restoreComposerDraft } from './input.js';
|
|
3
|
+
|
|
4
|
+
function key() { return [store.get('myUserId') || '', store.get('currentSlug') || '', store.get('sessionOriginId') || ''].join(':'); }
|
|
5
|
+
export function checkComposerDeparture() {
|
|
6
|
+
var draft = takeComposerDraft();
|
|
7
|
+
restoreComposerDraft(draft);
|
|
8
|
+
}
|
|
9
|
+
export function saveSessionRichDraft() {
|
|
10
|
+
if (store.get('activeProjectMateId') || !store.get('sessionOriginId') || store.get('panePendingBinding')) return;
|
|
11
|
+
var drafts = Object.assign({}, store.get('sessionRichDrafts') || {});
|
|
12
|
+
var draft = takeComposerDraft();
|
|
13
|
+
drafts[key()] = draft;
|
|
14
|
+
restoreComposerDraft(draft);
|
|
15
|
+
store.set({ sessionRichDrafts: drafts });
|
|
16
|
+
}
|
|
17
|
+
export function restoreSessionRichDraft() {
|
|
18
|
+
if (store.get('activeProjectMateId') || store.get('panePendingBinding')) return;
|
|
19
|
+
var draft = (store.get('sessionRichDrafts') || {})[key()];
|
|
20
|
+
restoreComposerDraft(draft || { text: (store.get('sessionDrafts') || {})[store.get('activeSessionId')] || '', start: 0, end: 0 });
|
|
21
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { getWs } from './ws-ref.js';
|
|
3
|
+
import { composerDepartureReason } from './input.js';
|
|
4
|
+
import { processMessage } from './app-message-router.js';
|
|
5
|
+
|
|
6
|
+
function notice(show, message) {
|
|
7
|
+
var element = document.getElementById('session-switch-wait');
|
|
8
|
+
if (!show) { if (element) element.remove(); return; }
|
|
9
|
+
if (element) { if (message) element.textContent = message; return; }
|
|
10
|
+
var area = document.getElementById('input-area');
|
|
11
|
+
if (!area) return;
|
|
12
|
+
element = document.createElement('div'); element.id = 'session-switch-wait';
|
|
13
|
+
element.className = 'session-switch-wait'; element.setAttribute('role', 'status');
|
|
14
|
+
element.textContent = 'Session change waiting. Finish or discard voice input, or wait for attachments. Your draft stays with this conversation.';
|
|
15
|
+
area.prepend(element);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Keep the source composer and its async completions together. The destination
|
|
19
|
+
// replay waits in order; it cannot change identity midway through an upload.
|
|
20
|
+
export function deferSessionSwitch(message) {
|
|
21
|
+
var pending = store.get('deferredSessionSwitch');
|
|
22
|
+
if (pending && pending.draining) return false;
|
|
23
|
+
if (['account_auth_required', 'project_removed', 'project_access_revoked'].indexOf(message.type) >= 0) {
|
|
24
|
+
store.set({ deferredSessionSwitch: null }); notice(false); return false;
|
|
25
|
+
}
|
|
26
|
+
if (!pending && message.type === 'session_switched' && composerDepartureReason()) {
|
|
27
|
+
pending = { socket: getWs(), projectSlug: store.get('currentSlug'), ownerId: store.get('myUserId'),
|
|
28
|
+
sessionId: store.get('activeSessionId'), origin: store.get('sessionOriginId'), processing: store.get('processing'), messages: [] };
|
|
29
|
+
var speech = store.get('speechActive');
|
|
30
|
+
if (speech) speech.autoSend = false;
|
|
31
|
+
store.set({ deferredSessionSwitch: pending }); notice(true);
|
|
32
|
+
}
|
|
33
|
+
if (!pending) return false;
|
|
34
|
+
if (pending.projectSlug === store.get('currentSlug') && pending.ownerId === store.get('myUserId')) pending.socket = getWs();
|
|
35
|
+
if (store.get('connected') && pending.processing && !store.get('processing')) store.set({ processing: true });
|
|
36
|
+
pending.messages.push(message);
|
|
37
|
+
if (message.type === 'session_list' || message.type === 'session_switched') queueMicrotask(drain);
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function drain() {
|
|
42
|
+
var pending = store.get('deferredSessionSwitch');
|
|
43
|
+
if (!pending || pending.draining || composerDepartureReason() || !store.get('connected')) return;
|
|
44
|
+
if (pending.socket !== getWs() || pending.projectSlug !== store.get('currentSlug') || pending.ownerId !== store.get('myUserId')) return;
|
|
45
|
+
var latestSwitch, latestList;
|
|
46
|
+
pending.messages.forEach(function (message) {
|
|
47
|
+
if (message.type === 'session_switched') latestSwitch = message;
|
|
48
|
+
if (message.type === 'session_list') latestList = message;
|
|
49
|
+
});
|
|
50
|
+
if (!latestSwitch || !latestList) return;
|
|
51
|
+
var target = (latestList.sessions || []).find(function (session) { return session.id === latestSwitch.id; });
|
|
52
|
+
if (!target || target.sessionOriginId !== latestSwitch.sessionOriginId) {
|
|
53
|
+
notice(true, 'The conversation changed while your draft was waiting. Your draft is preserved. Choose a conversation to continue.');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
// Restoring superseded snapshots must not trigger outbound session effects
|
|
57
|
+
// before the final authoritative target is installed.
|
|
58
|
+
pending.draining = true;
|
|
59
|
+
pending.finalSwitch = latestSwitch;
|
|
60
|
+
pending.messages.forEach(processMessage);
|
|
61
|
+
store.set({ deferredSessionSwitch: null }); notice(false);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
store.subscribe(function (state, previous) {
|
|
65
|
+
if (state.deferredSessionSwitch && (state.composerUploadingCount !== previous.composerUploadingCount ||
|
|
66
|
+
state.speechActive !== previous.speechActive || state.speechRetry !== previous.speechRetry || state.connected !== previous.connected)) queueMicrotask(drain);
|
|
67
|
+
});
|