@linxin666/dsh-remote-web-ui 0.4.3 → 0.4.5
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/README.i18n.yaml +2 -2
- package/README.md +23 -8
- package/README.zh.md +22 -7
- package/lib/client.js +194 -140
- package/lib/client.js.map +1 -1
- package/lib/index.js +231 -23
- package/lib/types/client/PluginSettingsCard.d.ts +7 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +2 -2
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +2 -1
- package/lib/types/client/index.js +3 -3
- package/lib/types/client/locales.d.ts +0 -4
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +5 -9
- package/lib/types/client/mobile-adapt.d.ts +2 -2
- package/lib/types/client/mobile-adapt.d.ts.map +1 -1
- package/lib/types/client/mobile-adapt.js +163 -136
- package/lib/types/client/settings-form.d.ts +16 -0
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +27 -1
- package/lib/types/detached-work.d.ts +45 -0
- package/lib/types/detached-work.d.ts.map +1 -0
- package/lib/types/detached-work.js +54 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +67 -12
- package/lib/types/lan-bind.d.ts +21 -4
- package/lib/types/lan-bind.d.ts.map +1 -1
- package/lib/types/lan-bind.js +103 -17
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +43 -0
- package/lib/types/remote-api.d.ts +13 -1
- package/lib/types/remote-api.d.ts.map +1 -1
- package/lib/types/remote-api.js +14 -1
- package/package.json +19 -19
- package/src/client/PluginSettingsCard.tsx +9 -0
- package/src/client/RemoteSettingsCard.tsx +2 -0
- package/src/client/index.ts +3 -3
- package/src/client/locales.ts +5 -9
- package/src/client/mobile-adapt.ts +167 -138
- package/src/client/settings-card.module.css +31 -5
- package/src/client/settings-form.ts +27 -1
- package/src/detached-work.ts +57 -0
- package/src/index.ts +67 -12
- package/src/lan-bind.ts +111 -17
- package/src/pairing.ts +44 -1
- package/src/remote-api.ts +14 -1
|
@@ -30,14 +30,18 @@ const ADAPT_CSS_ID = 'dsh-remote-web-ui/mobile-adapt.css';
|
|
|
30
30
|
const ACTIVE_CLASS = 'dsh-remote-portrait';
|
|
31
31
|
/** Body class while the collapsed rail is hidden behind the whale. */
|
|
32
32
|
const RAIL_HIDDEN_CLASS = 'dsh-remote-rail-hidden';
|
|
33
|
+
/**
|
|
34
|
+
* The official sidebar column root. Also the scope of the row sweep: every row
|
|
35
|
+
* the drag suppression covers is a descendant of it, so the document-wide scan
|
|
36
|
+
* it replaces only ever returned these.
|
|
37
|
+
*/
|
|
38
|
+
const SIDEBAR_SELECTOR = '[class*="_sidebarCol"]';
|
|
39
|
+
/** The official draggable session/project rows (see disableRowDrag). */
|
|
40
|
+
const ROW_SELECTOR = '[class*="_sessionRow"], [class*="_projectRow"]';
|
|
41
|
+
/** The injected stylesheet, addressed through the same cached lookup. */
|
|
42
|
+
const STYLE_SELECTOR = `style[data-plugin-css="${ADAPT_CSS_ID}"]`;
|
|
33
43
|
/** Whale button id. */
|
|
34
44
|
const WHALE_ID = 'dshRemoteWhale';
|
|
35
|
-
/** Compact picker: synthesized model button id. */
|
|
36
|
-
const MODEL_BTN_ID = 'dshRemoteModelPick';
|
|
37
|
-
/** Compact picker: synthesized effort button id. */
|
|
38
|
-
const EFFORT_BTN_ID = 'dshRemoteEffortPick';
|
|
39
|
-
/** Body class while the compact picker buttons are wired. */
|
|
40
|
-
const COMPACT_CLASS = 'dsh-remote-compact-picker';
|
|
41
45
|
/** Body class while the header actions are seated in the tabs row. */
|
|
42
46
|
const HEADER_SEATED_CLASS = 'dsh-remote-header-seated';
|
|
43
47
|
/**
|
|
@@ -46,13 +50,6 @@ const HEADER_SEATED_CLASS = 'dsh-remote-header-seated';
|
|
|
46
50
|
* reserving the space with padding.
|
|
47
51
|
*/
|
|
48
52
|
const HEADER_RESERVE_VAR = '--dsh-remote-header-actions-reserve';
|
|
49
|
-
/** Locale-dependent fast path for the official picker cells (zh/en). */
|
|
50
|
-
const PICKER_CELL_PATTERN = {
|
|
51
|
-
model: /模型|Model/, // i18n-allow: matches official picker cell text, not plugin copy
|
|
52
|
-
effort: /推理等级|Reasoning|Effort/i, // i18n-allow: matches official picker cell text, not plugin copy
|
|
53
|
-
};
|
|
54
|
-
/** Position of each drill cell among the sheet's chevron cells. */
|
|
55
|
-
const DRILL_INDEX = { model: 0, effort: 1 };
|
|
56
53
|
/**
|
|
57
54
|
* The official application frame. The layout column classes are the anchor:
|
|
58
55
|
* `_frame` is shared by unrelated official components (the chat turn rail,
|
|
@@ -183,24 +180,12 @@ const ADAPT_CSS = [
|
|
|
183
180
|
'[class$="_composerSeat"]{transform:none !important}',
|
|
184
181
|
'[class$="_composerSeat"] [class$="_menu"]{position:fixed !important;left:8px !important;right:8px !important;top:auto !important;bottom:calc(8px + env(safe-area-inset-bottom)) !important;width:auto !important;max-width:none !important;max-height:70dvh !important;overflow-y:auto !important;z-index:2147482000}',
|
|
185
182
|
'[class$="_composerSeat"] [class$="_menu"] [class$="_cell"]{height:44px;min-height:44px;font-size:13px}',
|
|
186
|
-
// v79
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
// degrades back to the usable text trigger instead of no picker.
|
|
193
|
-
`body.${COMPACT_CLASS} [class$="_composerSeat"] [class$="_trailing"] [class$="_trigger"]:has([class$="_triggerEffort"]){display:none}`,
|
|
194
|
-
// The icon buttons sit inline in the tools row (parallel to the
|
|
195
|
-
// permission trigger), so the trailing line collapses to zero and the
|
|
196
|
-
// context ring + send re-anchor to the row itself. The ring shifts a
|
|
197
|
-
// few px right: at its desktop offset its hit box kisses the effort
|
|
198
|
-
// button.
|
|
199
|
-
`body.${COMPACT_CLASS} [class$="_composerSeat"] [class$="_trailing"]{flex-basis:auto;position:static;min-height:0;padding:0;width:0}`,
|
|
200
|
-
`body.${COMPACT_CLASS} [class$="_composerSeat"] [class$="_trailing"] > [class$="_root"]:has([class$="_track"]){right:44px}`,
|
|
201
|
-
`#${MODEL_BTN_ID},#${EFFORT_BTN_ID}{width:26px;height:32px;min-width:26px;padding:0;border-radius:9px;background:var(--dsw-alias-bg-module-platform);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:center;cursor:pointer;flex:none;margin-left:4px}`,
|
|
202
|
-
`#${MODEL_BTN_ID} svg,#${EFFORT_BTN_ID} svg{width:16px;height:16px;display:block}`,
|
|
203
|
-
`#${MODEL_BTN_ID}:active,#${EFFORT_BTN_ID}:active{opacity:.7}`,
|
|
183
|
+
// v79's compact picker (two synthesized model/effort icon buttons in the
|
|
184
|
+
// tools row, the trailing text trigger hidden behind a body class, and the
|
|
185
|
+
// trailing line collapsed to zero width) was removed at the user's request.
|
|
186
|
+
// The portrait composer keeps the official text trigger with the trailing
|
|
187
|
+
// line rules above; the bottom-sheet rules immediately above still apply to
|
|
188
|
+
// the official menu.
|
|
204
189
|
// Bottom stats line (N rounds / M steps) two sizes below the tabs; v55
|
|
205
190
|
// wraps freely but clamps at two lines (the official rule is nowrap +
|
|
206
191
|
// single-line ellipsis); v60 drops the official 32px right padding.
|
|
@@ -274,18 +259,22 @@ const ADAPT_CSS = [
|
|
|
274
259
|
// v68: settings modal on mobile — the official panel is a fixed 800px
|
|
275
260
|
// two-column layout (nav + content); switch to a column layout: the
|
|
276
261
|
// section nav becomes a horizontal scrollable row on top.
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
262
|
+
// Scoped to the portrait body class like the suppressions above: these
|
|
263
|
+
// are the only rules in this array that reshape an official two-column
|
|
264
|
+
// surface, and the overlay portal is shared with every other dialog.
|
|
265
|
+
// Unscoped, the tag — which lives in <head> for as long as the layer
|
|
266
|
+
// is active and is restored by the sync tick after a revert —
|
|
267
|
+
// collapsed the DESKTOP settings panel into a stacked column (measured
|
|
268
|
+
// on a real 1440px host: panel flex-direction row -> column, nav 800px
|
|
269
|
+
// tall -> 236px, content 612px -> 800px wide and pushed below the nav).
|
|
270
|
+
`body.${ACTIVE_CLASS} [class$="_overlay"] [class$="_panel"]{flex-direction:column;max-height:calc(100dvh - 32px)}`,
|
|
271
|
+
`body.${ACTIVE_CLASS} [class$="_overlay"] [class$="_panel"] [class$="_nav"]{flex-direction:row;gap:4px;width:100%;padding:12px 12px 0;overflow-x:auto;overflow-y:hidden}`,
|
|
272
|
+
`body.${ACTIVE_CLASS} [class$="_overlay"] [class$="_panel"] [class$="_navTitle"]{display:none}`,
|
|
273
|
+
`body.${ACTIVE_CLASS} [class$="_overlay"] [class$="_panel"] [class$="_navList"]{flex-direction:row;gap:4px}`,
|
|
274
|
+
`body.${ACTIVE_CLASS} [class$="_overlay"] [class$="_panel"] [class$="_navCell"]{height:34px;padding:0 12px;gap:6px;flex:none;border-radius:10px}`,
|
|
275
|
+
`body.${ACTIVE_CLASS} [class$="_overlay"] [class$="_panel"] [class$="_navLabel"]{font-size:13px}`,
|
|
276
|
+
`body.${ACTIVE_CLASS} [class$="_overlay"] [class$="_panel"] [class$="_content"]{flex:1;min-height:0}`,
|
|
284
277
|
];
|
|
285
|
-
/** Cube glyph for the compact model button (a plain box outline). */
|
|
286
|
-
const CUBE_ICON = '<path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/>';
|
|
287
|
-
/** Level glyph for the compact effort button (three rising bars). */
|
|
288
|
-
const LEVELS_ICON = '<path d="M4 6h16"/><path d="M7 12h10"/><path d="M10 18h4"/>';
|
|
289
278
|
/** The DeepSeek fish glyph (the official brand mark path). */
|
|
290
279
|
const FISH_PATH = 'M22.9168 1.43018C22.6713 1.31018 22.5658 1.53918 22.4223 1.65519C22.3733 1.69269 22.3318 1.74169 22.2903 1.78669C21.9317 2.1697 21.5127 2.42121 20.9657 2.39121C20.1657 2.34621 19.4827 2.59771 18.8787 3.20973C18.7502 2.45521 18.3236 2.0047 17.6746 1.71569C17.3351 1.56568 16.9916 1.41518 16.7536 1.08867C16.5876 0.856163 16.5421 0.597155 16.4591 0.341647C16.4061 0.187643 16.3536 0.0301382 16.1761 0.00363739C15.9836 -0.0263635 15.9081 0.135141 15.8326 0.270145C15.5306 0.822162 15.4136 1.43018 15.4251 2.0462C15.4516 3.43174 16.0366 4.53527 17.1991 5.3203C17.3311 5.4103 17.3651 5.5003 17.3236 5.63181C17.2441 5.90231 17.1501 6.16482 17.0671 6.43533C17.0141 6.60784 16.9351 6.64584 16.7501 6.57033C16.1121 6.30383 15.5611 5.90931 15.074 5.4328C14.2475 4.63328 13.5 3.75075 12.568 3.05973C12.349 2.89822 12.13 2.74822 11.9034 2.60522C10.9524 1.68169 12.028 0.923165 12.277 0.833162C12.5375 0.739159 12.3675 0.41615 11.5259 0.42015C10.6844 0.42365 9.91439 0.705658 8.93286 1.08117C8.78935 1.13767 8.63835 1.17867 8.48384 1.21267C7.59332 1.04367 6.66829 1.00617 5.70226 1.11517C3.88321 1.31768 2.43016 2.1777 1.36213 3.64575C0.0790928 5.4103 -0.222916 7.41536 0.146595 9.50642C0.535106 11.7105 1.66014 13.535 3.38869 14.9616C5.18125 16.4406 7.24581 17.1657 9.60138 17.0266C11.0319 16.9441 12.6245 16.7526 14.421 15.2321C14.874 15.4576 15.3496 15.5476 16.1381 15.6151C16.7456 15.6716 17.3306 15.5851 17.7836 15.4911C18.4931 15.3411 18.4441 14.6841 18.1876 14.5636C16.1081 13.595 16.5646 13.9891 16.1496 13.67C17.2061 12.42 18.8202 10.1979 19.3182 7.17235C19.3672 6.83834 19.4297 6.36783 19.4222 6.09732C19.4182 5.93231 19.4562 5.86831 19.6447 5.84931C20.1657 5.78931 20.6712 5.64681 21.1357 5.3913C22.4833 4.65528 23.0268 3.44624 23.1548 1.9972C23.1738 1.77569 23.1508 1.54668 22.9168 1.43018Z';
|
|
291
280
|
/**
|
|
@@ -319,6 +308,8 @@ export function startMobileAdapt() {
|
|
|
319
308
|
let whaleObserver = null;
|
|
320
309
|
/** Header subtree observer: marks the geometry measurement dirty on re-render. */
|
|
321
310
|
let headerObserver = null;
|
|
311
|
+
/** Document child-list observer: invalidates the cached-absent selector set. */
|
|
312
|
+
let domObserver = null;
|
|
322
313
|
let observedHeader = null;
|
|
323
314
|
/** Whether the seated-actions geometry needs re-measuring (see alignActionsText). */
|
|
324
315
|
let headerGeometryDirty = true;
|
|
@@ -340,18 +331,29 @@ export function startMobileAdapt() {
|
|
|
340
331
|
* rail compaction) while the body class stays.
|
|
341
332
|
*/
|
|
342
333
|
function ensureAdaptStyle() {
|
|
343
|
-
if (nodeOf(
|
|
334
|
+
if (nodeOf(STYLE_SELECTOR) !== null)
|
|
344
335
|
return;
|
|
345
336
|
const tag = document.createElement('style');
|
|
346
337
|
tag.dataset.plugin = 'remote-web-ui';
|
|
347
338
|
tag.dataset.pluginCss = ADAPT_CSS_ID;
|
|
348
339
|
tag.textContent = ADAPT_CSS.join('');
|
|
349
340
|
document.head.appendChild(tag);
|
|
341
|
+
// Seed the lookup cache: the tag is created here, outside nodeOf, so the
|
|
342
|
+
// negative cache could otherwise report it missing for one more tick and
|
|
343
|
+
// stack a duplicate stylesheet.
|
|
344
|
+
nodeCache.set(STYLE_SELECTOR, tag);
|
|
345
|
+
nodeMissCache.delete(STYLE_SELECTOR);
|
|
350
346
|
}
|
|
351
347
|
function apply() {
|
|
352
348
|
if (active)
|
|
353
349
|
return;
|
|
354
350
|
active = true;
|
|
351
|
+
// First statement of the activation, before any lookup: nothing observed the
|
|
352
|
+
// document while the layer was reverted (the observer is created below), so
|
|
353
|
+
// no "absent" verdict recorded earlier may be served now. Clearing here
|
|
354
|
+
// rather than beside the observer also covers the lookups this path runs
|
|
355
|
+
// before that point.
|
|
356
|
+
forgetAbsentSelectors();
|
|
355
357
|
document.body.classList.add(ACTIVE_CLASS);
|
|
356
358
|
// A details panel opened before the viewport rotated into portrait (or
|
|
357
359
|
// restored across reloads) would sit behind the display:none above;
|
|
@@ -371,6 +373,7 @@ export function startMobileAdapt() {
|
|
|
371
373
|
meta.setAttribute('content', `${savedViewportContent}, viewport-fit=cover`);
|
|
372
374
|
}
|
|
373
375
|
ensureWhale();
|
|
376
|
+
ensureDomObserver();
|
|
374
377
|
syncWhale();
|
|
375
378
|
// React builds nodes with attributes before inserting them, so
|
|
376
379
|
// attribute observers miss the initial collapsed state — a light
|
|
@@ -384,7 +387,6 @@ export function startMobileAdapt() {
|
|
|
384
387
|
active = false;
|
|
385
388
|
unseatHeaderActions();
|
|
386
389
|
restoreRowDrag();
|
|
387
|
-
removeCompactPicker();
|
|
388
390
|
document.body.classList.remove(ACTIVE_CLASS);
|
|
389
391
|
document.body.classList.remove(RAIL_HIDDEN_CLASS);
|
|
390
392
|
const tag = document.querySelector(`style[data-plugin-css="${ADAPT_CSS_ID}"]`);
|
|
@@ -404,6 +406,45 @@ export function startMobileAdapt() {
|
|
|
404
406
|
whaleObserver.disconnect();
|
|
405
407
|
whaleObserver = null;
|
|
406
408
|
}
|
|
409
|
+
if (domObserver !== null) {
|
|
410
|
+
domObserver.disconnect();
|
|
411
|
+
domObserver = null;
|
|
412
|
+
}
|
|
413
|
+
// Nothing observes the document from here on; a verdict recorded now could
|
|
414
|
+
// not be trusted after the next apply().
|
|
415
|
+
forgetAbsentSelectors();
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Observe the document for the two kinds of change that can make a cached-absent
|
|
419
|
+
* target discoverable again (see nodeOf): node insertion/removal, and the
|
|
420
|
+
* class / compat-stamp attributes the cached selectors match on.
|
|
421
|
+
*
|
|
422
|
+
* Body class writes are excluded by target: the layer toggles three body
|
|
423
|
+
* classes every tick, and counting those would invalidate the negative cache
|
|
424
|
+
* on every tick and restore the very scan this cache removes. They cannot
|
|
425
|
+
* create a target either — every cached selector matches an element other
|
|
426
|
+
* than <body>.
|
|
427
|
+
*
|
|
428
|
+
* The record queue is drained on a microtask, so this stays off the layout
|
|
429
|
+
* path; a chat turn's insertions are a handful per second, not per element.
|
|
430
|
+
*/
|
|
431
|
+
function ensureDomObserver() {
|
|
432
|
+
if (domObserver !== null || typeof MutationObserver === 'undefined' || !document.body)
|
|
433
|
+
return;
|
|
434
|
+
domObserver = new MutationObserver((records) => {
|
|
435
|
+
for (const record of records) {
|
|
436
|
+
if (record.type === 'childList' || record.target !== document.body) {
|
|
437
|
+
noteDomChange();
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
domObserver.observe(document.body, {
|
|
443
|
+
childList: true,
|
|
444
|
+
subtree: true,
|
|
445
|
+
attributes: true,
|
|
446
|
+
attributeFilter: ['class', 'data-dsh-frame'],
|
|
447
|
+
});
|
|
407
448
|
}
|
|
408
449
|
/** Start/stop the 600ms sync tick; a no-op when already in the asked state. */
|
|
409
450
|
function setWhaleTimer(on) {
|
|
@@ -415,90 +456,6 @@ export function startMobileAdapt() {
|
|
|
415
456
|
whaleTimer = null;
|
|
416
457
|
}
|
|
417
458
|
}
|
|
418
|
-
/**
|
|
419
|
-
* Compact picker (v79): a phone row cannot fit the desktop text triggers,
|
|
420
|
-
* so the model/effort entries become two icon buttons in the trailing
|
|
421
|
-
* row. Both forward to the official picker trigger (its menu renders as
|
|
422
|
-
* the bottom sheet) and then drill straight into the asked cell — model
|
|
423
|
-
* list or effort list — so one tap lands on the list, matching the
|
|
424
|
-
* cube-model / brain-effort mapping. The official context ring next to
|
|
425
|
-
* the send button keeps its own semantics untouched.
|
|
426
|
-
*/
|
|
427
|
-
function removeCompactPicker() {
|
|
428
|
-
document.body.classList.remove(COMPACT_CLASS);
|
|
429
|
-
document.getElementById(MODEL_BTN_ID)?.remove();
|
|
430
|
-
document.getElementById(EFFORT_BTN_ID)?.remove();
|
|
431
|
-
}
|
|
432
|
-
function drillIntoPicker(kind) {
|
|
433
|
-
const trigger = document.querySelector('[class$="_composerSeat"] [class$="_trailing"] [class$="_trigger"]:has([class$="_triggerEffort"])');
|
|
434
|
-
if (trigger === null)
|
|
435
|
-
return;
|
|
436
|
-
trigger.click();
|
|
437
|
-
// The sheet mount takes a beat (observed ~0.2-0.6s on a cold phone
|
|
438
|
-
// mirror); poll until the asked cell exists instead of a fixed delay.
|
|
439
|
-
let tries = 0;
|
|
440
|
-
const tapCell = () => {
|
|
441
|
-
tries += 1;
|
|
442
|
-
const cells = Array.from(document.querySelectorAll('[class$="_composerSeat"] [class$="_menu"] [class$="_cell"]'));
|
|
443
|
-
// The official cell copy is localized (zh/en/ru), so the label match is
|
|
444
|
-
// only the fast path; the sheet's drill cells (label + value + chevron)
|
|
445
|
-
// are the structural anchor: model first, effort second, on every locale.
|
|
446
|
-
const byLabel = cells.find((c) => PICKER_CELL_PATTERN[kind].test(c.textContent ?? ''));
|
|
447
|
-
const drillable = cells.filter((c) => c.querySelector('[class*="_cellChevron"], [class*="_chevron"]'));
|
|
448
|
-
const cell = byLabel ?? drillable[DRILL_INDEX[kind]];
|
|
449
|
-
if (cell !== undefined) {
|
|
450
|
-
;
|
|
451
|
-
cell.click();
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
if (tries < 8)
|
|
455
|
-
window.setTimeout(tapCell, 150);
|
|
456
|
-
};
|
|
457
|
-
window.setTimeout(tapCell, 150);
|
|
458
|
-
}
|
|
459
|
-
function makeCompactButton(id, title, icon, kind) {
|
|
460
|
-
const btn = document.createElement('button');
|
|
461
|
-
btn.id = id;
|
|
462
|
-
btn.type = 'button';
|
|
463
|
-
btn.dataset.dshPlugin = 'remote-web-ui';
|
|
464
|
-
btn.title = title;
|
|
465
|
-
btn.setAttribute('aria-label', title);
|
|
466
|
-
btn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${icon}</svg>`;
|
|
467
|
-
btn.addEventListener('click', () => { drillIntoPicker(kind); });
|
|
468
|
-
return btn;
|
|
469
|
-
}
|
|
470
|
-
function syncCompactPicker() {
|
|
471
|
-
if (!active)
|
|
472
|
-
return;
|
|
473
|
-
const tools = nodeOf('[class$="_composerSeat"] [class$="_tools"]');
|
|
474
|
-
const trigger = tools?.parentElement?.querySelector('[class$="_triggerEffort"]')?.parentElement;
|
|
475
|
-
if (tools === null || trigger === null) {
|
|
476
|
-
removeCompactPicker();
|
|
477
|
-
return;
|
|
478
|
-
}
|
|
479
|
-
if (document.getElementById(MODEL_BTN_ID) === null) {
|
|
480
|
-
// The cell pattern matches the OFFICIAL picker cell text (the official
|
|
481
|
-
// client's own zh/en copy), not plugin-owned text.
|
|
482
|
-
tools.appendChild(makeCompactButton(MODEL_BTN_ID, surfaceText('mobile.composer.pickModel', 'Pick model'), CUBE_ICON, 'model'));
|
|
483
|
-
}
|
|
484
|
-
if (document.getElementById(EFFORT_BTN_ID) === null) {
|
|
485
|
-
tools.appendChild(makeCompactButton(EFFORT_BTN_ID, surfaceText('mobile.composer.pickEffort', 'Pick reasoning effort'), LEVELS_ICON, 'effort'));
|
|
486
|
-
}
|
|
487
|
-
// Titles re-read every tick so a locale switch (or the translate seat
|
|
488
|
-
// arriving after this layer installed) is picked up without a reload.
|
|
489
|
-
for (const [id, key, fallback] of [
|
|
490
|
-
[MODEL_BTN_ID, 'mobile.composer.pickModel', 'Pick model'],
|
|
491
|
-
[EFFORT_BTN_ID, 'mobile.composer.pickEffort', 'Pick reasoning effort'],
|
|
492
|
-
]) {
|
|
493
|
-
const btn = document.getElementById(id);
|
|
494
|
-
if (btn === null)
|
|
495
|
-
continue;
|
|
496
|
-
const label = surfaceText(key, fallback);
|
|
497
|
-
btn.title = label;
|
|
498
|
-
btn.setAttribute('aria-label', label);
|
|
499
|
-
}
|
|
500
|
-
document.body.classList.add(COMPACT_CLASS);
|
|
501
|
-
}
|
|
502
459
|
/**
|
|
503
460
|
* The official sidebar toggle in the logo row. The row's own `_toggle` class
|
|
504
461
|
* is the precise anchor; the row's last button is the fallback for cohorts
|
|
@@ -701,7 +658,7 @@ export function startMobileAdapt() {
|
|
|
701
658
|
function disableRowDrag() {
|
|
702
659
|
if (!active)
|
|
703
660
|
return;
|
|
704
|
-
const rows =
|
|
661
|
+
const rows = sidebarRows();
|
|
705
662
|
for (const row of rows) {
|
|
706
663
|
if (row.getAttribute('draggable') !== 'false') {
|
|
707
664
|
if (!dragOverridden.has(row))
|
|
@@ -732,17 +689,88 @@ export function startMobileAdapt() {
|
|
|
732
689
|
* node on a major re-render, which the isConnected guard detects.
|
|
733
690
|
*/
|
|
734
691
|
const nodeCache = new Map();
|
|
692
|
+
/**
|
|
693
|
+
* Selectors known to be absent as of {@link domGeneration}. A miss is the
|
|
694
|
+
* common case, not the exception: the layer holds no overlay of its own, and
|
|
695
|
+
* several official surfaces it looks for (tabs row, tools row, sidebar rows)
|
|
696
|
+
* legitimately do not exist at once. Re-walking a conversation-sized document
|
|
697
|
+
* for the same absent selector on every 600ms tick was waste that almost
|
|
698
|
+
* always resolved to null: five document-wide lookups per tick on the measured
|
|
699
|
+
* fixture, zero after this cache. That is a lookup count — the wall-clock
|
|
700
|
+
* saving is real while the mounted content is quiet and shrinks to the
|
|
701
|
+
* row-scope saving when a turn streams (see the Agent Note for the harness and
|
|
702
|
+
* the before/after numbers).
|
|
703
|
+
*
|
|
704
|
+
* A miss is trusted only while the observed content has not changed, so a
|
|
705
|
+
* surface that appears is still found on the very next tick — the same
|
|
706
|
+
* discovery latency the unconditional probe had. The cache is never aged by a
|
|
707
|
+
* clock: it is cleared by {@link noteDomChange} while the observer is live and
|
|
708
|
+
* by {@link forgetAbsentSelectors} across the windows where it is not (apply
|
|
709
|
+
* and revert). One blind spot is deliberate and load-bearing: the observer
|
|
710
|
+
* watches <body>, so a head-resident target is not covered by that signal —
|
|
711
|
+
* the one such target is seeded into the cache where it is created (see
|
|
712
|
+
* ensureAdaptStyle).
|
|
713
|
+
*/
|
|
714
|
+
const nodeMissCache = new Set();
|
|
715
|
+
/**
|
|
716
|
+
* Bumped whenever a cached target may have appeared or disappeared. A surface
|
|
717
|
+
* qualifies on a class token or on the aggregate compat stamp
|
|
718
|
+
* (`data-dsh-frame`), so both insertions/removals AND those two attributes
|
|
719
|
+
* can change the answer. Nothing the layer writes per tick lands here: its own
|
|
720
|
+
* class writes are all on <body>, and its row/transform/label writes touch
|
|
721
|
+
* other attributes entirely (see ensureDomObserver).
|
|
722
|
+
*/
|
|
723
|
+
let domGeneration = 0;
|
|
724
|
+
/** The generation the negative cache was recorded against. */
|
|
725
|
+
let missGeneration = -1;
|
|
726
|
+
function noteDomChange() {
|
|
727
|
+
domGeneration += 1;
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Drop every cached "absent" verdict. Called on apply and revert: while the
|
|
731
|
+
* observer is disconnected the layer is blind to insertions, so a verdict
|
|
732
|
+
* recorded before the gap could otherwise outlive the change that falsified
|
|
733
|
+
* it and be served without a probe. The positive cache needs no equivalent —
|
|
734
|
+
* its `isConnected` guard already re-resolves a replaced node.
|
|
735
|
+
*/
|
|
736
|
+
function forgetAbsentSelectors() {
|
|
737
|
+
nodeMissCache.clear();
|
|
738
|
+
domGeneration += 1;
|
|
739
|
+
}
|
|
735
740
|
function nodeOf(selector) {
|
|
736
741
|
const cached = nodeCache.get(selector);
|
|
737
|
-
if (cached !== undefined
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
742
|
+
if (cached !== undefined) {
|
|
743
|
+
// A node React replaced is no longer connected; drop it and fall through
|
|
744
|
+
// so the lookup below can find its replacement.
|
|
745
|
+
if (cached.isConnected)
|
|
746
|
+
return cached;
|
|
741
747
|
nodeCache.delete(selector);
|
|
742
|
-
|
|
743
|
-
|
|
748
|
+
}
|
|
749
|
+
if (missGeneration !== domGeneration) {
|
|
750
|
+
nodeMissCache.clear();
|
|
751
|
+
missGeneration = domGeneration;
|
|
752
|
+
}
|
|
753
|
+
if (nodeMissCache.has(selector))
|
|
754
|
+
return null;
|
|
755
|
+
const found = document.querySelector(selector);
|
|
756
|
+
if (found === null) {
|
|
757
|
+
nodeMissCache.add(selector);
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
760
|
+
nodeCache.set(selector, found);
|
|
744
761
|
return found;
|
|
745
762
|
}
|
|
763
|
+
/**
|
|
764
|
+
* The session/project rows the drag suppression covers, scoped to the cached
|
|
765
|
+
* sidebar root. Each row carries the sidebar column class as an ancestor, so
|
|
766
|
+
* the document-wide scan this replaces only ever returned rows from here.
|
|
767
|
+
*/
|
|
768
|
+
function sidebarRows() {
|
|
769
|
+
const sidebar = nodeOf(SIDEBAR_SELECTOR);
|
|
770
|
+
if (sidebar === null)
|
|
771
|
+
return [];
|
|
772
|
+
return Array.from(sidebar.querySelectorAll(ROW_SELECTOR));
|
|
773
|
+
}
|
|
746
774
|
/** The official application frame, through the same cached lookup. */
|
|
747
775
|
function frameEl() {
|
|
748
776
|
const frame = nodeOf(APP_FRAME_SELECTOR);
|
|
@@ -779,7 +807,6 @@ export function startMobileAdapt() {
|
|
|
779
807
|
disableRowDrag();
|
|
780
808
|
seatHeaderActions();
|
|
781
809
|
alignActionsText();
|
|
782
|
-
syncCompactPicker();
|
|
783
810
|
}
|
|
784
811
|
// v67: on mobile the header actions (agent-preset mode label + background
|
|
785
812
|
// task badge) move from the title row into the tabs row. React re-creates
|
|
@@ -134,6 +134,8 @@ export declare class CardForm<T> {
|
|
|
134
134
|
private readonly disposeForm;
|
|
135
135
|
private disposed;
|
|
136
136
|
private saving;
|
|
137
|
+
/** A save asked for while one was in flight; it runs once the first settles (#1754). */
|
|
138
|
+
private saveQueued;
|
|
137
139
|
private failed;
|
|
138
140
|
private failedReason;
|
|
139
141
|
/** @param scope - the bound configuration form for this card's namespace. */
|
|
@@ -167,6 +169,20 @@ export declare class CardForm<T> {
|
|
|
167
169
|
* the user can correct them instead of retyping.
|
|
168
170
|
* @returns settlement after the mutation and the read-back.
|
|
169
171
|
*/
|
|
172
|
+
/**
|
|
173
|
+
* Run one save, and re-run it once if another was asked for while this one
|
|
174
|
+
* was still in flight.
|
|
175
|
+
*
|
|
176
|
+
* A save is a Host round trip that also drives the profile reconcile, and
|
|
177
|
+
* the Host runs that write inside one exclusive transaction. Answering a
|
|
178
|
+
* press that arrives mid-flight by returning immediately dropped the edit
|
|
179
|
+
* with no explanation, which is the "the save button stops working after a
|
|
180
|
+
* few rounds" report (#1754). Serializing instead means a save pressed while
|
|
181
|
+
* another is still settling runs against the settled state - which is what
|
|
182
|
+
* the operator meant by pressing it again.
|
|
183
|
+
* @returns settlement after the mutation and the read-back.
|
|
184
|
+
*/
|
|
185
|
+
requestSave(): Promise<void>;
|
|
170
186
|
save(): Promise<void>;
|
|
171
187
|
/**
|
|
172
188
|
* Every staged edit a save would write. An entry whose draft is not a value
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings-form.d.ts","sourceRoot":"","sources":["../../../src/client/settings-form.ts"],"names":[],"mappings":"AACA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAsB,MAAM,4CAA4C,CAAA;AAChG,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAA;AAGlE,yEAAyE;AACzE,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAA;AAErB,0EAA0E;AAC1E,MAAM,WAAW,SAAS;IACxB,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAA;IACb;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,2FAA2F;IAC3F,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,CAAA;IAClC;;;OAGG;IACH,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,UAAU,GAAG,SAAS,CAAA;CAChD;AAED,wCAAwC;AACxC,MAAM,WAAW,UAAU;IACzB,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAA;IACZ,oEAAoE;IACpE,UAAU,EAAE,OAAO,CAAA;IACnB,gFAAgF;IAChF,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,oDAAoD;AACpD,MAAM,WAAW,SAAS;IACxB,4EAA4E;IAC5E,SAAS,EAAE,OAAO,CAAA;IAClB;;;;;;OAMG;IACH,OAAO,EAAE,OAAO,CAAA;IAChB,gDAAgD;IAChD,QAAQ,EAAE,OAAO,CAAA;IACjB,4DAA4D;IAC5D,KAAK,EAAE,OAAO,CAAA;IACd,kEAAkE;IAClE,OAAO,EAAE,OAAO,CAAA;IAChB,2CAA2C;IAC3C,MAAM,EAAE,OAAO,CAAA;IACf,sFAAsF;IACtF,MAAM,EAAE,OAAO,CAAA;IACf;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,uDAAuD;AACvD,MAAM,WAAW,WAAW;IAC1B,sCAAsC;IACtC,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAC3C,gFAAgF;IAChF,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACnC,yEAAyE;IACzE,IAAI,EAAE,MAAM,IAAI,CAAA;IAChB,8BAA8B;IAC9B,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB;AAwBD,gEAAgE;AAChE,MAAM,WAAW,YAAY;IAC3B,+BAA+B;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,gDAAgD;IAChD,EAAE,EAAE,KAAK,GAAG,OAAO,CAAA;IACnB,2CAA2C;IAC3C,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED,6FAA6F;AAC7F,MAAM,WAAW,iBAAiB;IAChC,iDAAiD;IACjD,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,gDAAgD;IAChD,GAAG,CAAC,EAAE,MAAM,CAAA;CACb;AAED,6JAA6J;AAC7J,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,GAAE,iBAAsB,GAAG,SAAS,CAezF;AAED,0DAA0D;AAC1D,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CASlD;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAEpD;AAED,6DAA6D;AAC7D,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAYrD;AAED,yGAAyG;AACzG,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAShF;AAED;;;;;;;GAOG;AACH,qBAAa,QAAQ,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"settings-form.d.ts","sourceRoot":"","sources":["../../../src/client/settings-form.ts"],"names":[],"mappings":"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"}
|
|
@@ -95,6 +95,8 @@ export class CardForm {
|
|
|
95
95
|
disposeForm;
|
|
96
96
|
disposed = false;
|
|
97
97
|
saving = false;
|
|
98
|
+
/** A save asked for while one was in flight; it runs once the first settles (#1754). */
|
|
99
|
+
saveQueued = false;
|
|
98
100
|
failed = false;
|
|
99
101
|
failedReason;
|
|
100
102
|
/** @param scope - the bound configuration form for this card's namespace. */
|
|
@@ -156,7 +158,7 @@ export class CardForm {
|
|
|
156
158
|
resetField: (field) => {
|
|
157
159
|
this.stage(field, { text: this.specOf(field).format(this.baseValue(field)), clear: true });
|
|
158
160
|
},
|
|
159
|
-
save: () => { void this.
|
|
161
|
+
save: () => { void this.requestSave(); },
|
|
160
162
|
discard: () => {
|
|
161
163
|
if (this.staged.size === 0 && !this.failed)
|
|
162
164
|
return;
|
|
@@ -183,6 +185,30 @@ export class CardForm {
|
|
|
183
185
|
* the user can correct them instead of retyping.
|
|
184
186
|
* @returns settlement after the mutation and the read-back.
|
|
185
187
|
*/
|
|
188
|
+
/**
|
|
189
|
+
* Run one save, and re-run it once if another was asked for while this one
|
|
190
|
+
* was still in flight.
|
|
191
|
+
*
|
|
192
|
+
* A save is a Host round trip that also drives the profile reconcile, and
|
|
193
|
+
* the Host runs that write inside one exclusive transaction. Answering a
|
|
194
|
+
* press that arrives mid-flight by returning immediately dropped the edit
|
|
195
|
+
* with no explanation, which is the "the save button stops working after a
|
|
196
|
+
* few rounds" report (#1754). Serializing instead means a save pressed while
|
|
197
|
+
* another is still settling runs against the settled state - which is what
|
|
198
|
+
* the operator meant by pressing it again.
|
|
199
|
+
* @returns settlement after the mutation and the read-back.
|
|
200
|
+
*/
|
|
201
|
+
async requestSave() {
|
|
202
|
+
if (this.saving) {
|
|
203
|
+
this.saveQueued = true;
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
await this.save();
|
|
207
|
+
if (!this.saveQueued)
|
|
208
|
+
return;
|
|
209
|
+
this.saveQueued = false;
|
|
210
|
+
await this.requestSave();
|
|
211
|
+
}
|
|
186
212
|
async save() {
|
|
187
213
|
const plan = this.plan();
|
|
188
214
|
const valid = plan.filter((item) => item.judge !== undefined);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run one piece of work outside the caller's async context.
|
|
3
|
+
*
|
|
4
|
+
* The Host performs a settings mutation inside `hmr.runExclusive`, which marks
|
|
5
|
+
* the running async context with an AsyncLocalStorage store. Side effects that
|
|
6
|
+
* live on that path - writing `cordis.patch.yml`, the very file the HMR config
|
|
7
|
+
* watcher refreshes from - must not run under that mark, or the watcher's
|
|
8
|
+
* refresh can re-enter the exclusive transaction and be refused with "HMR
|
|
9
|
+
* transactions cannot be nested" (#1751, #1754).
|
|
10
|
+
*
|
|
11
|
+
* Deferring is NOT enough, and the earlier belt that this one replaces was
|
|
12
|
+
* built on a disproven premise: Node propagates an AsyncLocalStorage store into
|
|
13
|
+
* `setImmediate`, into `node:timers`, into promise continuations, and into any
|
|
14
|
+
* AsyncResource scoped to the current async id. Measured on Node 24:
|
|
15
|
+
*
|
|
16
|
+
* ```
|
|
17
|
+
* als.run(true, () => setImmediate(() => als.getStore())) // -> true
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* What DOES start clean is an AsyncResource whose `triggerAsyncId` is not the
|
|
21
|
+
* transaction. A resource created once at module scope - that is, before any
|
|
22
|
+
* transaction existed - carries no store, and everything it schedules inherits
|
|
23
|
+
* that emptiness rather than the caller's mark:
|
|
24
|
+
*
|
|
25
|
+
* ```
|
|
26
|
+
* const detached = new AsyncResource('...') // module scope
|
|
27
|
+
* als.run(true, () => detached.runInAsyncScope(() => setImmediate(() => als.getStore())))
|
|
28
|
+
* // -> undefined
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* @module
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* Run `work` outside the caller's AsyncLocalStorage context.
|
|
35
|
+
*
|
|
36
|
+
* The call itself is synchronous - this schedules, it does not defer. Work that
|
|
37
|
+
* must not block the caller should schedule its own `setImmediate` inside
|
|
38
|
+
* `work`, which then also starts clean because it inherits this resource's
|
|
39
|
+
* empty context rather than the caller's.
|
|
40
|
+
*
|
|
41
|
+
* @param work - the side effect to detach from the caller's async context.
|
|
42
|
+
* @returns whatever `work` returns.
|
|
43
|
+
*/
|
|
44
|
+
export declare function runDetached<T>(work: () => T): T;
|
|
45
|
+
//# sourceMappingURL=detached-work.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detached-work.d.ts","sourceRoot":"","sources":["../../src/detached-work.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAYH;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,CAE/C"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run one piece of work outside the caller's async context.
|
|
3
|
+
*
|
|
4
|
+
* The Host performs a settings mutation inside `hmr.runExclusive`, which marks
|
|
5
|
+
* the running async context with an AsyncLocalStorage store. Side effects that
|
|
6
|
+
* live on that path - writing `cordis.patch.yml`, the very file the HMR config
|
|
7
|
+
* watcher refreshes from - must not run under that mark, or the watcher's
|
|
8
|
+
* refresh can re-enter the exclusive transaction and be refused with "HMR
|
|
9
|
+
* transactions cannot be nested" (#1751, #1754).
|
|
10
|
+
*
|
|
11
|
+
* Deferring is NOT enough, and the earlier belt that this one replaces was
|
|
12
|
+
* built on a disproven premise: Node propagates an AsyncLocalStorage store into
|
|
13
|
+
* `setImmediate`, into `node:timers`, into promise continuations, and into any
|
|
14
|
+
* AsyncResource scoped to the current async id. Measured on Node 24:
|
|
15
|
+
*
|
|
16
|
+
* ```
|
|
17
|
+
* als.run(true, () => setImmediate(() => als.getStore())) // -> true
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* What DOES start clean is an AsyncResource whose `triggerAsyncId` is not the
|
|
21
|
+
* transaction. A resource created once at module scope - that is, before any
|
|
22
|
+
* transaction existed - carries no store, and everything it schedules inherits
|
|
23
|
+
* that emptiness rather than the caller's mark:
|
|
24
|
+
*
|
|
25
|
+
* ```
|
|
26
|
+
* const detached = new AsyncResource('...') // module scope
|
|
27
|
+
* als.run(true, () => detached.runInAsyncScope(() => setImmediate(() => als.getStore())))
|
|
28
|
+
* // -> undefined
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* @module
|
|
32
|
+
*/
|
|
33
|
+
import { AsyncResource } from 'node:async_hooks';
|
|
34
|
+
/**
|
|
35
|
+
* The escape hatch, created at module scope on purpose: a resource built while
|
|
36
|
+
* a transaction is running would capture that transaction, so this single
|
|
37
|
+
* instance is the only one the module ever needs. It carries no context of its
|
|
38
|
+
* own, so every job run through it starts from the process's root context.
|
|
39
|
+
*/
|
|
40
|
+
const DETACHED = new AsyncResource('dsh-remote-web-ui:detached-work', { triggerAsyncId: 0 });
|
|
41
|
+
/**
|
|
42
|
+
* Run `work` outside the caller's AsyncLocalStorage context.
|
|
43
|
+
*
|
|
44
|
+
* The call itself is synchronous - this schedules, it does not defer. Work that
|
|
45
|
+
* must not block the caller should schedule its own `setImmediate` inside
|
|
46
|
+
* `work`, which then also starts clean because it inherits this resource's
|
|
47
|
+
* empty context rather than the caller's.
|
|
48
|
+
*
|
|
49
|
+
* @param work - the side effect to detach from the caller's async context.
|
|
50
|
+
* @returns whatever `work` returns.
|
|
51
|
+
*/
|
|
52
|
+
export function runDetached(work) {
|
|
53
|
+
return DETACHED.runInAsyncScope(work);
|
|
54
|
+
}
|
package/lib/types/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAA;AAClE,OAAO,CAAC,MAAM,0BAA0B,CAAA;AAOxC,OAAO,EAA0C,KAAK,aAAa,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
|