synabun 2.2.0 → 2.2.1
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/CHANGELOG.md +18 -0
- package/README.md +1 -1
- package/mcp-server/package-lock.json +2 -2
- package/mcp-server/package.json +1 -1
- package/neural-interface/i18n/en.json +3 -0
- package/neural-interface/i18n/pt-BR.json +3 -0
- package/neural-interface/lib/assistant-brains/codex.js +6 -1
- package/neural-interface/lib/assistant-dispatch.js +4 -1
- package/neural-interface/lib/mcp-external-servers.js +27 -3
- package/neural-interface/lib/native-loop-providers.js +3 -2
- package/neural-interface/lib/opencode-native-boundary.js +155 -21
- package/neural-interface/lib/peer-identity.js +49 -5
- package/neural-interface/package-lock.json +2 -2
- package/neural-interface/package.json +1 -1
- package/neural-interface/public/shared/assistant/asst-api.js +14 -2
- package/neural-interface/public/shared/assistant/asst-computer.js +26 -2
- package/neural-interface/public/shared/assistant/asst-motion-gate.js +80 -0
- package/neural-interface/public/shared/assistant/asst-panel.js +1 -1
- package/neural-interface/public/shared/assistant/asst-render.js +62 -7
- package/neural-interface/public/shared/assistant/asst-scene-styles.js +55 -23
- package/neural-interface/public/shared/assistant/asst-scenes.js +225 -79
- package/neural-interface/public/shared/assistant/asst-sidepanel-styles.js +4 -4
- package/neural-interface/public/shared/assistant/asst-style-cache.js +44 -0
- package/neural-interface/public/shared/assistant/asst-styles.js +24 -4
- package/neural-interface/public/shared/cp/cp-permissions.js +12 -0
- package/neural-interface/public/shared/loading-teardown.js +34 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +6 -1
- package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +1 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +10 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +6 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +88 -72
- package/neural-interface/public/shared/storage-retry.js +47 -0
- package/neural-interface/public/shared/storage.js +25 -4
- package/neural-interface/public/shared/styles.css +8 -1
- package/neural-interface/public/shared/synabun-mascot.js +27 -8
- package/neural-interface/public/shared/synabun-ticker.js +147 -12
- package/neural-interface/public/shared/ui-claude-panel.js +79 -61
- package/neural-interface/public/shared/ui-loading.js +5 -4
- package/neural-interface/server.js +87 -40
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -9,6 +9,18 @@
|
|
|
9
9
|
// requestFrames(ms) lifts the animation subscribers (≥ 24 fps) to 60 fps for a
|
|
10
10
|
// moment: a pose blend, a hop.
|
|
11
11
|
//
|
|
12
|
+
// A frame is only requested for the vsync a subscriber is due on: a 30 fps
|
|
13
|
+
// rig costs 30 frames a second, not one per vsync. Every frame the page draws
|
|
14
|
+
// makes the browser restyle each running CSS animation on the main thread
|
|
15
|
+
// (composited or not), so a frame nobody needs is never free. Between due
|
|
16
|
+
// frames the loop sleeps on a timer that wakes half a frame before the vsync
|
|
17
|
+
// it wants; the frame interval is learnt from back-to-back frames (60 Hz,
|
|
18
|
+
// 120 Hz displays).
|
|
19
|
+
//
|
|
20
|
+
// A subscriber may name a `target` element: it runs only while that element
|
|
21
|
+
// is on screen (one shared IntersectionObserver), and a loop whose every
|
|
22
|
+
// subscriber is off screen requests nothing at all until one comes back.
|
|
23
|
+
//
|
|
12
24
|
// The loop stops while the tab is hidden (document.hidden, body.app-hidden)
|
|
13
25
|
// and freezes while the UI is dragged (body.ui-interacting): subscribers are
|
|
14
26
|
// skipped, and the first frame after either hands each one a dt of at most
|
|
@@ -20,7 +32,7 @@
|
|
|
20
32
|
// pointermove listener and one keydown listener (both capture phase, so a
|
|
21
33
|
// handler that stops propagation cannot hide input from them).
|
|
22
34
|
//
|
|
23
|
-
// subscribe(fn(now, dt), { fps }) → unsubscribe() (unsubscribe.setFps(n) re-caps)
|
|
35
|
+
// subscribe(fn(now, dt), { fps, target }) → unsubscribe() (unsubscribe.setFps(n) re-caps)
|
|
24
36
|
// requestFrames(ms) 60 fps for the next ms
|
|
25
37
|
// isReducedMotion() live prefers-reduced-motion
|
|
26
38
|
// onReducedMotionChange(fn(on)) → unsubscribe()
|
|
@@ -41,8 +53,10 @@
|
|
|
41
53
|
const FRAME_60 = 1000 / 60;
|
|
42
54
|
const SLOP = 4; // ms of rAF jitter a capped subscriber tolerates before it skips a frame
|
|
43
55
|
const RESUME_DT = 50; // the first dt after a stop or a freeze is at most this
|
|
44
|
-
const SLEEP_OVER = 48; // a wait longer than this sleeps on a timer instead of frames
|
|
45
56
|
const POLL_MS = 250; // how often a stopped loop re-checks when no MutationObserver exists
|
|
57
|
+
const PHASE_MS = 1500; // a frame newer than this still tells where the vsyncs fall
|
|
58
|
+
const MIN_FRAME = 4; // ms: the shortest frame interval believed (240 Hz)
|
|
59
|
+
const MAX_FRAME = 34; // ms: the longest (30 Hz)
|
|
46
60
|
const MAX_BOOST = 5000;
|
|
47
61
|
const LISTEN = { passive: true, capture: true };
|
|
48
62
|
|
|
@@ -84,10 +98,73 @@ let pollId = 0;
|
|
|
84
98
|
let boostUntil = -Infinity;
|
|
85
99
|
let watching = false;
|
|
86
100
|
let observer = null;
|
|
101
|
+
let frameMs = FRAME_60; // the display's frame interval, as back-to-back frames show it
|
|
102
|
+
let lastTs = -Infinity; // the last frame's timestamp: where the vsyncs fall
|
|
103
|
+
let inFrame = false; // frame() is running
|
|
104
|
+
let chained = false; // the frame in flight was requested by the previous one, with no wait between
|
|
105
|
+
const counts = { frames: 0, requests: 0, sleeps: 0 }; // for tests and the idle-CPU suite
|
|
87
106
|
|
|
88
107
|
const capFps = fps => Math.max(1, Math.min(60, Number(fps) > 0 ? Number(fps) : 30));
|
|
89
108
|
const boosted = (sub, t) => t < boostUntil && sub.fps >= 24;
|
|
90
109
|
const intervalOf = (sub, t) => (boosted(sub, t) ? FRAME_60 : 1000 / sub.fps);
|
|
110
|
+
/** A subscriber with a target off screen is parked: it is neither called nor waited for. */
|
|
111
|
+
const parked = sub => sub.target && !sub.seen;
|
|
112
|
+
|
|
113
|
+
// ── Targets: one IntersectionObserver for every subscriber that names an element ──
|
|
114
|
+
|
|
115
|
+
let targetIo = null; // undefined while unavailable
|
|
116
|
+
const targetSubs = new Map(); // element → Set of subs
|
|
117
|
+
const targetSeen = new Map(); // element → on screen as last reported (absent until the first report)
|
|
118
|
+
|
|
119
|
+
function targetObserver() {
|
|
120
|
+
if (targetIo !== null) return targetIo;
|
|
121
|
+
const IO = env ? env.IntersectionObserver : globalThis.IntersectionObserver;
|
|
122
|
+
if (typeof IO !== 'function') { targetIo = undefined; return targetIo; }
|
|
123
|
+
try {
|
|
124
|
+
targetIo = new IO((entries) => {
|
|
125
|
+
let woke = false;
|
|
126
|
+
for (const entry of entries) {
|
|
127
|
+
const set = targetSubs.get(entry.target);
|
|
128
|
+
if (!set) continue;
|
|
129
|
+
const on = !!entry.isIntersecting;
|
|
130
|
+
targetSeen.set(entry.target, on);
|
|
131
|
+
for (const sub of set) {
|
|
132
|
+
if (sub.seen === on) continue;
|
|
133
|
+
sub.seen = on;
|
|
134
|
+
if (on) { sub.resume = true; woke = true; }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (woke && subs.size) wakeNow();
|
|
138
|
+
});
|
|
139
|
+
} catch { targetIo = undefined; }
|
|
140
|
+
return targetIo;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function watchTarget(sub) {
|
|
144
|
+
const el = sub.target;
|
|
145
|
+
const io = targetObserver();
|
|
146
|
+
if (!io) { sub.target = null; return; } // no observer: the target counts as on screen
|
|
147
|
+
let set = targetSubs.get(el);
|
|
148
|
+
if (!set) {
|
|
149
|
+
set = new Set();
|
|
150
|
+
targetSubs.set(el, set);
|
|
151
|
+
try { io.observe(el); } catch { targetSubs.delete(el); sub.target = null; return; }
|
|
152
|
+
}
|
|
153
|
+
set.add(sub);
|
|
154
|
+
// Until the observer's first report the target counts as on screen (it draws at once, as before).
|
|
155
|
+
sub.seen = targetSeen.has(el) ? targetSeen.get(el) : true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function unwatchTarget(sub) {
|
|
159
|
+
const el = sub.target;
|
|
160
|
+
const set = el ? targetSubs.get(el) : null;
|
|
161
|
+
if (!set) return;
|
|
162
|
+
set.delete(sub);
|
|
163
|
+
if (set.size) return;
|
|
164
|
+
targetSubs.delete(el);
|
|
165
|
+
targetSeen.delete(el);
|
|
166
|
+
try { targetIo?.unobserve(el); } catch { /* already gone */ }
|
|
167
|
+
}
|
|
91
168
|
|
|
92
169
|
/** Why the loop is not running: '' (it runs), 'hidden' or 'interacting'. */
|
|
93
170
|
export function tickerPaused() {
|
|
@@ -102,17 +179,20 @@ export function tickerPaused() {
|
|
|
102
179
|
|
|
103
180
|
/**
|
|
104
181
|
* Call `fn(now, dt)` from the shared loop, at most `fps` times a second
|
|
105
|
-
* (1-60, default 30).
|
|
106
|
-
*
|
|
182
|
+
* (1-60, default 30). With `target` (an element) it is called only while that
|
|
183
|
+
* element is on screen. Returns unsubscribe(); unsubscribe.setFps(n) changes
|
|
184
|
+
* the cap in place.
|
|
107
185
|
*/
|
|
108
|
-
export function subscribe(fn, { fps = 30 } = {}) {
|
|
186
|
+
export function subscribe(fn, { fps = 30, target = null } = {}) {
|
|
109
187
|
if (typeof fn !== 'function') return Object.assign(() => {}, { setFps() {} });
|
|
110
|
-
const sub = { fn, fps: capFps(fps), last: -1, born: now(), resume: false };
|
|
188
|
+
const sub = { fn, fps: capFps(fps), last: -1, born: now(), resume: false, target: target && typeof target === 'object' ? target : null, seen: true };
|
|
111
189
|
subs.add(sub);
|
|
190
|
+
if (sub.target) watchTarget(sub);
|
|
112
191
|
watch();
|
|
113
192
|
wakeNow();
|
|
114
193
|
const unsubscribe = () => {
|
|
115
194
|
if (!subs.delete(sub)) return;
|
|
195
|
+
unwatchTarget(sub);
|
|
116
196
|
if (!subs.size) halt();
|
|
117
197
|
};
|
|
118
198
|
unsubscribe.setFps = (next) => {
|
|
@@ -138,31 +218,71 @@ function wakeNow() {
|
|
|
138
218
|
schedule();
|
|
139
219
|
}
|
|
140
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Request the next frame a subscriber is due on, and no other: when the next
|
|
223
|
+
* vsync serves it, at once; otherwise a timer wakes half a frame before the
|
|
224
|
+
* vsync it wants. Nothing at all while every subscriber is parked.
|
|
225
|
+
*/
|
|
141
226
|
function schedule() {
|
|
142
227
|
if (rafId || timerId || !subs.size) return;
|
|
143
228
|
const raf = rafFn();
|
|
144
229
|
if (!raf) return;
|
|
145
230
|
if (tickerPaused()) { stopped(); return; }
|
|
146
231
|
const t = now();
|
|
147
|
-
let
|
|
232
|
+
let due = Infinity;
|
|
148
233
|
for (const sub of subs) {
|
|
149
|
-
|
|
150
|
-
|
|
234
|
+
if (parked(sub)) continue;
|
|
235
|
+
const at = sub.last < 0 ? t : sub.last + intervalOf(sub, t) - SLOP;
|
|
236
|
+
if (at < due) due = at;
|
|
151
237
|
}
|
|
152
|
-
if (
|
|
153
|
-
|
|
238
|
+
if (!Number.isFinite(due)) return; // every subscriber is off screen: the observer wakes the loop
|
|
239
|
+
const phase = lastTs > -Infinity && t - lastTs < PHASE_MS ? lastTs : null;
|
|
240
|
+
let lead;
|
|
241
|
+
if (due <= t) lead = 0;
|
|
242
|
+
else if (phase === null) lead = due - t - frameMs; // no recent vsync to count from: the frame before `due`
|
|
243
|
+
else {
|
|
244
|
+
const k = Math.max(1, Math.ceil((due - phase) / frameMs - 1e-6));
|
|
245
|
+
lead = k === 1 ? 0 : phase + (k - 0.5) * frameMs - t; // half a frame before the vsync that serves `due`
|
|
246
|
+
}
|
|
247
|
+
if (lead > 1) {
|
|
248
|
+
counts.sleeps += 1;
|
|
249
|
+
timerId = later(() => { timerId = 0; schedule(); }, lead);
|
|
154
250
|
return;
|
|
155
251
|
}
|
|
252
|
+
counts.requests += 1;
|
|
253
|
+
chained = inFrame;
|
|
156
254
|
rafId = raf(frame);
|
|
157
255
|
}
|
|
158
256
|
|
|
257
|
+
/** Learn the display's frame interval from two frames requested back to back. */
|
|
258
|
+
function learnInterval(ts) {
|
|
259
|
+
if (!chained || !(lastTs > -Infinity)) return;
|
|
260
|
+
const d = ts - lastTs;
|
|
261
|
+
if (!(d >= MIN_FRAME && d <= MAX_FRAME)) return;
|
|
262
|
+
// Snap down to the shortest seen; drift back up slowly (a dropped frame is two, not a slower display).
|
|
263
|
+
frameMs = Math.min(d, Math.min(MAX_FRAME, frameMs + 0.05));
|
|
264
|
+
}
|
|
265
|
+
|
|
159
266
|
function frame(ts) {
|
|
160
267
|
rafId = 0;
|
|
161
268
|
if (!subs.size) return;
|
|
162
269
|
if (tickerPaused()) { stopped(); return; }
|
|
163
270
|
const t = typeof ts === 'number' && Number.isFinite(ts) ? ts : now();
|
|
271
|
+
counts.frames += 1;
|
|
272
|
+
learnInterval(t);
|
|
273
|
+
lastTs = t;
|
|
274
|
+
chained = false;
|
|
275
|
+
inFrame = true;
|
|
276
|
+
try {
|
|
277
|
+
runSubscribers(t);
|
|
278
|
+
schedule(); // inside the frame: a frame requested from here is the next vsync's (it teaches the interval)
|
|
279
|
+
} finally { inFrame = false; }
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function runSubscribers(t) {
|
|
164
283
|
for (const sub of Array.from(subs)) {
|
|
165
284
|
if (!subs.has(sub)) continue; // unsubscribed by an earlier subscriber this frame
|
|
285
|
+
if (parked(sub)) continue;
|
|
166
286
|
const interval = intervalOf(sub, t);
|
|
167
287
|
if (sub.last >= 0 && t - sub.last < interval - SLOP) continue;
|
|
168
288
|
let dt = t - (sub.last >= 0 ? sub.last : sub.born);
|
|
@@ -172,7 +292,6 @@ function frame(ts) {
|
|
|
172
292
|
sub.last = t;
|
|
173
293
|
try { sub.fn(t, dt); } catch (err) { report(err); }
|
|
174
294
|
}
|
|
175
|
-
schedule();
|
|
176
295
|
}
|
|
177
296
|
|
|
178
297
|
/** The loop stopped (hidden) or froze (drag): mark the jump, wait for the watchers. */
|
|
@@ -334,6 +453,17 @@ function reset() {
|
|
|
334
453
|
halt();
|
|
335
454
|
subs.clear();
|
|
336
455
|
boostUntil = -Infinity;
|
|
456
|
+
try { targetIo?.disconnect(); } catch { /* gone */ }
|
|
457
|
+
targetIo = null;
|
|
458
|
+
targetSubs.clear();
|
|
459
|
+
targetSeen.clear();
|
|
460
|
+
frameMs = FRAME_60;
|
|
461
|
+
lastTs = -Infinity;
|
|
462
|
+
inFrame = false;
|
|
463
|
+
chained = false;
|
|
464
|
+
counts.frames = 0;
|
|
465
|
+
counts.requests = 0;
|
|
466
|
+
counts.sleeps = 0;
|
|
337
467
|
const q = motionQuery;
|
|
338
468
|
if (q && motionFns.size) {
|
|
339
469
|
if (q.removeEventListener) q.removeEventListener('change', onMotionQuery);
|
|
@@ -372,5 +502,10 @@ export const __ticker = {
|
|
|
372
502
|
keyListeners: keyFns.size,
|
|
373
503
|
keyAttached: keyOn,
|
|
374
504
|
motionListeners: motionFns.size,
|
|
505
|
+
parked: Array.from(subs).filter(parked).length,
|
|
506
|
+
targets: targetSubs.size,
|
|
507
|
+
frameMs,
|
|
375
508
|
}),
|
|
509
|
+
/** Frames run, frames requested and timer sleeps since the last reset (the idle-CPU suite reads them). */
|
|
510
|
+
counts: () => ({ ...counts }),
|
|
376
511
|
};
|
|
@@ -419,6 +419,7 @@ function buildPanel() {
|
|
|
419
419
|
</div>
|
|
420
420
|
<div class="cp-input-area">
|
|
421
421
|
<div class="cp-input-wrap">
|
|
422
|
+
<span class="cp-spin-ring" aria-hidden="true"><i></i></span>
|
|
422
423
|
<div class="cp-image-preview" id="cp-image-preview"></div>
|
|
423
424
|
<div class="cp-input-inner">
|
|
424
425
|
<textarea class="cp-input" id="cp-input" placeholder="Message SynaBun..." rows="1" autocomplete="off" spellcheck="false"></textarea>
|
|
@@ -1061,26 +1062,17 @@ function injectStyles() {
|
|
|
1061
1062
|
margin-top: 8px; transition: opacity 0.3s, border-color 0.2s, box-shadow 0.3s;
|
|
1062
1063
|
}
|
|
1063
1064
|
.perm-card { position: relative; }
|
|
1064
|
-
.
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
padding: 1px;
|
|
1069
|
-
pointer-events: none;
|
|
1065
|
+
/* A card that waits on you turns an amber ring inside its edge: the shared .cp-spin-ring (with the
|
|
1066
|
+
composer's, below), turning only while the card is .active-perm. */
|
|
1067
|
+
.perm-card > .cp-spin-ring { border-radius: 8px; }
|
|
1068
|
+
.perm-card > .cp-spin-ring > i {
|
|
1070
1069
|
background: conic-gradient(
|
|
1071
|
-
from var(--cp-border-angle, 0deg),
|
|
1072
1070
|
rgba(245,180,60,0.0) 0%,
|
|
1073
1071
|
rgba(245,180,60,0.55) 25%,
|
|
1074
1072
|
rgba(245,180,60,0.1) 50%,
|
|
1075
1073
|
rgba(245,180,60,0.55) 75%,
|
|
1076
1074
|
rgba(245,180,60,0.0) 100%
|
|
1077
1075
|
);
|
|
1078
|
-
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
|
1079
|
-
-webkit-mask-composite: xor;
|
|
1080
|
-
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
|
1081
|
-
mask-composite: exclude;
|
|
1082
|
-
opacity: 0;
|
|
1083
|
-
transition: opacity 0.4s;
|
|
1084
1076
|
}
|
|
1085
1077
|
.perm-card.active-perm {
|
|
1086
1078
|
border-color: rgba(245,180,60,0.25);
|
|
@@ -1088,9 +1080,10 @@ function injectStyles() {
|
|
|
1088
1080
|
box-shadow: 0 0 16px rgba(245,180,60,0.08), 0 0 40px rgba(245,180,60,0.04);
|
|
1089
1081
|
animation: cp-perm-glow 2.5s ease-in-out infinite;
|
|
1090
1082
|
}
|
|
1091
|
-
.perm-card.active-perm
|
|
1092
|
-
|
|
1093
|
-
animation: cp-
|
|
1083
|
+
.perm-card.active-perm > .cp-spin-ring { opacity: 1; }
|
|
1084
|
+
.perm-card.active-perm > .cp-spin-ring > i {
|
|
1085
|
+
animation: cp-spin-ring-turn 3s linear infinite;
|
|
1086
|
+
will-change: rotate;
|
|
1094
1087
|
}
|
|
1095
1088
|
.perm-card:hover { border-color: var(--b-light); }
|
|
1096
1089
|
.perm-card.active-perm:hover { border-color: rgba(245,180,60,0.4); }
|
|
@@ -1234,47 +1227,64 @@ function injectStyles() {
|
|
|
1234
1227
|
transition: background 0.4s;
|
|
1235
1228
|
min-width: 0;
|
|
1236
1229
|
}
|
|
1237
|
-
|
|
1238
|
-
|
|
1230
|
+
/* ── Spinning border ring (composer focus, a waiting permission card) ──
|
|
1231
|
+
A conic gradient turning inside the 1px edge, the mask keeping only the ring. The gradient is a
|
|
1232
|
+
square box that turns (rotate, run by the compositor) under the mask, and only while its owner's
|
|
1233
|
+
state is on; an animated custom property (the old --cp-border-angle, @property + a conic gradient
|
|
1234
|
+
from it) restyled and repainted the owner on the main thread 60 times a second. The ring is a size
|
|
1235
|
+
container and the square 142cqmax + 4px, wider than the owner's diagonal at any shape, so what
|
|
1236
|
+
the mask shows is the same conic gradient at every angle. Markup: <span class="cp-spin-ring"
|
|
1237
|
+
aria-hidden="true"><i></i></span>, the owner's first child. */
|
|
1238
|
+
.cp-spin-ring {
|
|
1239
1239
|
position: absolute; inset: 0;
|
|
1240
|
-
border-radius: 14px;
|
|
1241
1240
|
padding: 1px;
|
|
1241
|
+
overflow: hidden;
|
|
1242
1242
|
pointer-events: none;
|
|
1243
|
+
container-type: size;
|
|
1244
|
+
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
|
1245
|
+
-webkit-mask-composite: xor;
|
|
1246
|
+
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
|
1247
|
+
mask-composite: exclude;
|
|
1248
|
+
opacity: 0;
|
|
1249
|
+
transition: opacity 0.4s;
|
|
1250
|
+
}
|
|
1251
|
+
.cp-spin-ring > i {
|
|
1252
|
+
position: absolute; left: 50%; top: 50%;
|
|
1253
|
+
width: calc(142cqmax + 4px);
|
|
1254
|
+
aspect-ratio: 1;
|
|
1255
|
+
translate: -50% -50%;
|
|
1256
|
+
}
|
|
1257
|
+
@keyframes cp-spin-ring-turn {
|
|
1258
|
+
to { rotate: 360deg; }
|
|
1259
|
+
}
|
|
1260
|
+
body.ui-interacting .cp-spin-ring > i { animation-play-state: paused !important; }
|
|
1261
|
+
/* Reduced motion: the ring holds still where the turn starts (the focus / waiting look stays). */
|
|
1262
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1263
|
+
.cp-spin-ring > i { animation: none !important; will-change: auto !important; }
|
|
1264
|
+
}
|
|
1265
|
+
.cp-input-wrap > .cp-spin-ring { border-radius: 14px; }
|
|
1266
|
+
.cp-input-wrap > .cp-spin-ring > i {
|
|
1243
1267
|
background: conic-gradient(
|
|
1244
|
-
from var(--cp-border-angle, 0deg),
|
|
1245
1268
|
rgba(212,162,127,0.0) 0%,
|
|
1246
1269
|
rgba(212,162,127,0.35) 25%,
|
|
1247
1270
|
rgba(212,162,127,0.10) 50%,
|
|
1248
1271
|
rgba(212,162,127,0.35) 75%,
|
|
1249
1272
|
rgba(212,162,127,0.0) 100%
|
|
1250
1273
|
);
|
|
1251
|
-
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
|
1252
|
-
-webkit-mask-composite: xor;
|
|
1253
|
-
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
|
1254
|
-
mask-composite: exclude;
|
|
1255
|
-
opacity: 0;
|
|
1256
|
-
transition: opacity 0.4s;
|
|
1257
1274
|
}
|
|
1258
|
-
.cp-input-wrap:focus-within
|
|
1259
|
-
|
|
1260
|
-
animation: cp-
|
|
1275
|
+
.cp-input-wrap:focus-within > .cp-spin-ring { opacity: 1; }
|
|
1276
|
+
.cp-input-wrap:focus-within > .cp-spin-ring > i {
|
|
1277
|
+
animation: cp-spin-ring-turn 3s linear infinite;
|
|
1278
|
+
will-change: rotate;
|
|
1261
1279
|
}
|
|
1262
1280
|
.cp-input-wrap:focus-within {
|
|
1263
1281
|
background: rgba(212,162,127,0.03);
|
|
1264
1282
|
box-shadow: 0 0 20px rgba(212,162,127,0.04), 0 0 60px rgba(212,162,127,0.015);
|
|
1265
1283
|
}
|
|
1266
|
-
@keyframes cp-border-spin {
|
|
1267
|
-
to { --cp-border-angle: 360deg; }
|
|
1268
|
-
}
|
|
1269
1284
|
@keyframes cp-perm-glow {
|
|
1270
1285
|
0%, 100% { box-shadow: 0 0 16px rgba(245,180,60,0.08), 0 0 40px rgba(245,180,60,0.04); }
|
|
1271
1286
|
50% { box-shadow: 0 0 20px rgba(245,180,60,0.14), 0 0 50px rgba(245,180,60,0.07); }
|
|
1272
1287
|
}
|
|
1273
|
-
@property --cp-border-angle {
|
|
1274
|
-
syntax: '<angle>';
|
|
1275
|
-
initial-value: 0deg;
|
|
1276
|
-
inherits: false;
|
|
1277
|
-
}
|
|
1278
1288
|
|
|
1279
1289
|
.cp-input-inner {
|
|
1280
1290
|
display: flex; align-items: flex-end;
|
|
@@ -2301,52 +2311,53 @@ function injectStyles() {
|
|
|
2301
2311
|
}
|
|
2302
2312
|
|
|
2303
2313
|
/* ── Claude session pills in shared tray ── */
|
|
2304
|
-
@property --cp-pill-angle {
|
|
2305
|
-
syntax: '<angle>';
|
|
2306
|
-
initial-value: 0deg;
|
|
2307
|
-
inherits: false;
|
|
2308
|
-
}
|
|
2309
2314
|
.cp-session-pill {
|
|
2310
2315
|
position: relative;
|
|
2311
2316
|
overflow: hidden;
|
|
2312
2317
|
border-color: rgba(212, 162, 127, 0.14);
|
|
2313
2318
|
transition: border-color 0.2s;
|
|
2314
2319
|
}
|
|
2315
|
-
|
|
2316
|
-
|
|
2320
|
+
/* A working pill's ring: a conic gradient turning inside the 1px border (the mask keeps only the
|
|
2321
|
+
ring). The gradient is a square box that turns (rotate, run by the compositor) under the mask; an
|
|
2322
|
+
animated custom property (the old --cp-pill-angle) repainted the pill on the main thread every
|
|
2323
|
+
frame for as long as a session worked. The square is wider than the pill's diagonal, so the ring
|
|
2324
|
+
is the same conic gradient at every angle. */
|
|
2325
|
+
.cp-pill-ring {
|
|
2317
2326
|
position: absolute; inset: 0;
|
|
2327
|
+
display: none;
|
|
2318
2328
|
border-radius: 8px;
|
|
2319
2329
|
padding: 1px;
|
|
2330
|
+
overflow: hidden;
|
|
2320
2331
|
pointer-events: none;
|
|
2321
|
-
background: transparent;
|
|
2322
2332
|
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
|
2323
2333
|
-webkit-mask-composite: xor;
|
|
2324
2334
|
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
|
2325
2335
|
mask-composite: exclude;
|
|
2326
|
-
opacity: 0;
|
|
2327
|
-
}
|
|
2328
|
-
.cp-session-pill.cp-pill-running {
|
|
2329
|
-
border-color: transparent;
|
|
2330
2336
|
}
|
|
2331
|
-
.cp-
|
|
2337
|
+
.cp-pill-ring > i {
|
|
2338
|
+
position: absolute; left: 50%; top: 50%;
|
|
2339
|
+
width: calc(100% + 32px);
|
|
2340
|
+
aspect-ratio: 1;
|
|
2341
|
+
translate: -50% -50%;
|
|
2332
2342
|
background: conic-gradient(
|
|
2333
|
-
from var(--cp-pill-angle, 0deg),
|
|
2334
2343
|
rgba(212,162,127,0.0) 0%,
|
|
2335
2344
|
rgba(212,162,127,0.6) 25%,
|
|
2336
2345
|
rgba(212,162,127,0.12) 50%,
|
|
2337
2346
|
rgba(212,162,127,0.6) 75%,
|
|
2338
2347
|
rgba(212,162,127,0.0) 100%
|
|
2339
2348
|
);
|
|
2340
|
-
animation: cp-pill-
|
|
2341
|
-
opacity: 1;
|
|
2349
|
+
animation: cp-pill-ring-turn 2.4s linear infinite;
|
|
2342
2350
|
}
|
|
2343
|
-
|
|
2344
|
-
|
|
2351
|
+
.cp-session-pill.cp-pill-running {
|
|
2352
|
+
border-color: transparent;
|
|
2353
|
+
}
|
|
2354
|
+
.cp-session-pill.cp-pill-running .cp-pill-ring { display: block; }
|
|
2355
|
+
@keyframes cp-pill-ring-turn {
|
|
2356
|
+
to { rotate: 360deg; }
|
|
2345
2357
|
}
|
|
2346
2358
|
.cp-session-pill:hover { border-color: rgba(212, 162, 127, 0.28); }
|
|
2347
|
-
.cp-session-pill:hover.cp-pill-
|
|
2359
|
+
.cp-session-pill:hover .cp-pill-ring > i {
|
|
2348
2360
|
background: conic-gradient(
|
|
2349
|
-
from var(--cp-pill-angle, 0deg),
|
|
2350
2361
|
rgba(212,162,127,0.0) 0%,
|
|
2351
2362
|
rgba(212,162,127,0.8) 25%,
|
|
2352
2363
|
rgba(212,162,127,0.20) 50%,
|
|
@@ -2362,11 +2373,11 @@ function injectStyles() {
|
|
|
2362
2373
|
html.desktop-titlebar--win32 .cp-session-pill.cp-pill-running:hover {
|
|
2363
2374
|
border-color: rgba(212, 162, 127, 0.62);
|
|
2364
2375
|
}
|
|
2365
|
-
html.platform-win32 .cp-session-pill.cp-pill-running
|
|
2366
|
-
html.desktop-titlebar--win32 .cp-session-pill.cp-pill-running
|
|
2367
|
-
|
|
2368
|
-
opacity: 0;
|
|
2376
|
+
html.platform-win32 .cp-session-pill.cp-pill-running .cp-pill-ring,
|
|
2377
|
+
html.desktop-titlebar--win32 .cp-session-pill.cp-pill-running .cp-pill-ring {
|
|
2378
|
+
display: none;
|
|
2369
2379
|
}
|
|
2380
|
+
body.ui-interacting .cp-pill-ring > i { animation-play-state: paused; }
|
|
2370
2381
|
.cp-session-pill .term-minimized-pill-icon { color: rgba(212, 162, 127, 0.45); transition: color 0.2s; }
|
|
2371
2382
|
.cp-session-pill.cp-pill-running .term-minimized-pill-icon { color: rgba(212, 162, 127, 1); }
|
|
2372
2383
|
.cp-session-pill:hover .term-minimized-pill-icon { color: rgba(212, 162, 127, 0.85); }
|
|
@@ -2401,6 +2412,12 @@ function injectStyles() {
|
|
|
2401
2412
|
pointer-events: none;
|
|
2402
2413
|
}
|
|
2403
2414
|
@keyframes cp-pill-pulse { 0%,100% { opacity: 0.4; } 50% { opacity: 1; } }
|
|
2415
|
+
/* Reduced motion: the ring, the dot and the edge hold still (the working look stays). */
|
|
2416
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2417
|
+
.cp-pill-ring > i,
|
|
2418
|
+
.cp-session-pill.cp-pill-running .term-minimized-pill-label::before,
|
|
2419
|
+
.cp-session-pill.cp-pill-running::after { animation: none; }
|
|
2420
|
+
}
|
|
2404
2421
|
|
|
2405
2422
|
/* ── Think intensity toggle ── */
|
|
2406
2423
|
/* ── Shared toggle base ── */
|
|
@@ -3238,6 +3255,7 @@ function _createTrayPill(tab) {
|
|
|
3238
3255
|
pill.className = 'term-minimized-pill cp-session-pill';
|
|
3239
3256
|
pill.dataset.tabId = tab.id;
|
|
3240
3257
|
pill.innerHTML = `
|
|
3258
|
+
<span class="cp-pill-ring" aria-hidden="true"><i></i></span>
|
|
3241
3259
|
<span class="term-minimized-pill-icon">${_CP_ICON_SVG}</span>
|
|
3242
3260
|
<span class="term-minimized-pill-label">${escH(tab.label)}</span>
|
|
3243
3261
|
<button class="term-minimized-pill-close" data-tooltip="Close" data-tooltip-pos="top">×</button>
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { fetchHealth, startHealth } from './api.js';
|
|
8
8
|
import { emit, on } from './state.js';
|
|
9
9
|
import { t } from './i18n.js';
|
|
10
|
+
import { concealOverlay, revealOverlay } from './loading-teardown.js';
|
|
10
11
|
import {
|
|
11
12
|
START_LIMITS, START_STORAGE, createStartBridge, fetchStartBeacon, isMacPlatform, isWindowsPlatform,
|
|
12
13
|
manualStartCommands, openStartLink, startBeaconTarget, startButtonUsable, startStatusView,
|
|
@@ -127,7 +128,7 @@ export function showLoadingError(title, sub, canStart, serverOffline) {
|
|
|
127
128
|
*/
|
|
128
129
|
export function hideLoading(delay = 400) {
|
|
129
130
|
const $overlay = $('loading-overlay');
|
|
130
|
-
if ($overlay) setTimeout(() => $overlay
|
|
131
|
+
if ($overlay) setTimeout(() => concealOverlay($overlay), delay);
|
|
131
132
|
}
|
|
132
133
|
|
|
133
134
|
/**
|
|
@@ -135,7 +136,7 @@ export function hideLoading(delay = 400) {
|
|
|
135
136
|
*/
|
|
136
137
|
export function showLoading() {
|
|
137
138
|
const $overlay = $('loading-overlay');
|
|
138
|
-
if ($overlay) $overlay
|
|
139
|
+
if ($overlay) revealOverlay($overlay);
|
|
139
140
|
}
|
|
140
141
|
|
|
141
142
|
/**
|
|
@@ -479,7 +480,7 @@ function beginRestart({ request = null, announced = false, boot = '' } = {}) {
|
|
|
479
480
|
_offlineTitle = t('loading.serverOffline');
|
|
480
481
|
// `over-app`: no fade-in and above whatever is open. The screen is there in the frame of the click.
|
|
481
482
|
$overlay.classList.add('over-app', 'error', 'server-offline');
|
|
482
|
-
$overlay
|
|
483
|
+
revealOverlay($overlay);
|
|
483
484
|
// Nothing behind the screen keeps the keyboard (a terminal, an editor).
|
|
484
485
|
try { document.activeElement?.blur?.(); } catch {}
|
|
485
486
|
if (_statusDot) _statusDot.classList.add('error');
|
|
@@ -503,7 +504,7 @@ function dismissRestart() {
|
|
|
503
504
|
if (_restartTitle !== null) document.title = _restartTitle;
|
|
504
505
|
_restartTitle = null;
|
|
505
506
|
const $overlay = $('loading-overlay');
|
|
506
|
-
if ($overlay) $overlay
|
|
507
|
+
if ($overlay) concealOverlay($overlay);
|
|
507
508
|
}
|
|
508
509
|
|
|
509
510
|
// ═══════════════════════════════════════════
|