@linxin666/dsh-remote-web-ui 0.4.2 → 0.4.4
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 +12 -10
- package/README.zh.md +12 -10
- package/lib/client.js +278 -599
- package/lib/client.js.map +1 -1
- package/lib/index.js +396 -913
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +8 -9
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +1 -1
- package/lib/types/client/index.d.ts +32 -3
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +51 -8
- package/lib/types/client/locales.d.ts +0 -84
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +0 -84
- package/lib/types/client/mobile-adapt.d.ts.map +1 -1
- package/lib/types/client/mobile-adapt.js +142 -8
- package/lib/types/client/settings-entry-form.d.ts +50 -0
- package/lib/types/client/settings-entry-form.d.ts.map +1 -0
- package/lib/types/client/settings-entry-form.js +98 -0
- package/lib/types/index.d.ts +99 -7
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +150 -149
- package/lib/types/lan-bind-plan.d.ts +14 -0
- package/lib/types/lan-bind-plan.d.ts.map +1 -1
- package/lib/types/lan-bind-plan.js +16 -0
- package/lib/types/lan-bind.d.ts +1 -1
- package/lib/types/lan-bind.js +1 -1
- package/lib/types/mount-once.d.ts +38 -7
- package/lib/types/mount-once.d.ts.map +1 -1
- package/lib/types/mount-once.js +96 -12
- 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/lib/types/tunnel.d.ts +60 -3
- package/lib/types/tunnel.d.ts.map +1 -1
- package/lib/types/tunnel.js +136 -3
- package/package.json +20 -20
- package/src/client/RemoteEntry.tsx +7 -9
- package/src/client/RemoteSettingsCard.tsx +0 -2
- package/src/client/index.ts +53 -9
- package/src/client/locales.ts +0 -84
- package/src/client/mobile-adapt.ts +144 -5
- package/src/client/remote.module.css +50 -200
- package/src/client/settings-entry-form.ts +142 -0
- package/src/index.ts +202 -164
- package/src/lan-bind-plan.ts +22 -0
- package/src/lan-bind.ts +1 -1
- package/src/mount-once.ts +107 -13
- package/src/pairing.ts +44 -1
- package/src/remote-api.ts +14 -1
- package/src/tunnel.ts +142 -3
- package/lib/types/client/UpdateEntry.d.ts +0 -14
- package/lib/types/client/UpdateEntry.d.ts.map +0 -1
- package/lib/types/client/UpdateEntry.js +0 -112
- package/lib/types/client/UpdatePanel.d.ts +0 -42
- package/lib/types/client/UpdatePanel.d.ts.map +0 -1
- package/lib/types/client/UpdatePanel.js +0 -101
- package/lib/types/client/update-api.d.ts +0 -29
- package/lib/types/client/update-api.d.ts.map +0 -1
- package/lib/types/client/update-api.js +0 -47
- package/lib/types/console-output.d.ts +0 -41
- package/lib/types/console-output.d.ts.map +0 -1
- package/lib/types/console-output.js +0 -89
- package/lib/types/update-routes.d.ts +0 -30
- package/lib/types/update-routes.d.ts.map +0 -1
- package/lib/types/update-routes.js +0 -47
- package/lib/types/update.d.ts +0 -282
- package/lib/types/update.d.ts.map +0 -1
- package/lib/types/update.js +0 -773
- package/src/client/UpdateEntry.tsx +0 -144
- package/src/client/UpdatePanel.tsx +0 -256
- package/src/client/update-api.ts +0 -49
- package/src/console-output.ts +0 -97
- package/src/update-routes.ts +0 -65
- package/src/update.ts +0 -915
|
@@ -30,6 +30,16 @@ 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
45
|
/** Compact picker: synthesized model button id. */
|
|
@@ -319,6 +329,8 @@ export function startMobileAdapt() {
|
|
|
319
329
|
let whaleObserver = null;
|
|
320
330
|
/** Header subtree observer: marks the geometry measurement dirty on re-render. */
|
|
321
331
|
let headerObserver = null;
|
|
332
|
+
/** Document child-list observer: invalidates the cached-absent selector set. */
|
|
333
|
+
let domObserver = null;
|
|
322
334
|
let observedHeader = null;
|
|
323
335
|
/** Whether the seated-actions geometry needs re-measuring (see alignActionsText). */
|
|
324
336
|
let headerGeometryDirty = true;
|
|
@@ -340,18 +352,29 @@ export function startMobileAdapt() {
|
|
|
340
352
|
* rail compaction) while the body class stays.
|
|
341
353
|
*/
|
|
342
354
|
function ensureAdaptStyle() {
|
|
343
|
-
if (nodeOf(
|
|
355
|
+
if (nodeOf(STYLE_SELECTOR) !== null)
|
|
344
356
|
return;
|
|
345
357
|
const tag = document.createElement('style');
|
|
346
358
|
tag.dataset.plugin = 'remote-web-ui';
|
|
347
359
|
tag.dataset.pluginCss = ADAPT_CSS_ID;
|
|
348
360
|
tag.textContent = ADAPT_CSS.join('');
|
|
349
361
|
document.head.appendChild(tag);
|
|
362
|
+
// Seed the lookup cache: the tag is created here, outside nodeOf, so the
|
|
363
|
+
// negative cache could otherwise report it missing for one more tick and
|
|
364
|
+
// stack a duplicate stylesheet.
|
|
365
|
+
nodeCache.set(STYLE_SELECTOR, tag);
|
|
366
|
+
nodeMissCache.delete(STYLE_SELECTOR);
|
|
350
367
|
}
|
|
351
368
|
function apply() {
|
|
352
369
|
if (active)
|
|
353
370
|
return;
|
|
354
371
|
active = true;
|
|
372
|
+
// First statement of the activation, before any lookup: nothing observed the
|
|
373
|
+
// document while the layer was reverted (the observer is created below), so
|
|
374
|
+
// no "absent" verdict recorded earlier may be served now. Clearing here
|
|
375
|
+
// rather than beside the observer also covers the lookups this path runs
|
|
376
|
+
// before that point.
|
|
377
|
+
forgetAbsentSelectors();
|
|
355
378
|
document.body.classList.add(ACTIVE_CLASS);
|
|
356
379
|
// A details panel opened before the viewport rotated into portrait (or
|
|
357
380
|
// restored across reloads) would sit behind the display:none above;
|
|
@@ -371,6 +394,7 @@ export function startMobileAdapt() {
|
|
|
371
394
|
meta.setAttribute('content', `${savedViewportContent}, viewport-fit=cover`);
|
|
372
395
|
}
|
|
373
396
|
ensureWhale();
|
|
397
|
+
ensureDomObserver();
|
|
374
398
|
syncWhale();
|
|
375
399
|
// React builds nodes with attributes before inserting them, so
|
|
376
400
|
// attribute observers miss the initial collapsed state — a light
|
|
@@ -404,6 +428,45 @@ export function startMobileAdapt() {
|
|
|
404
428
|
whaleObserver.disconnect();
|
|
405
429
|
whaleObserver = null;
|
|
406
430
|
}
|
|
431
|
+
if (domObserver !== null) {
|
|
432
|
+
domObserver.disconnect();
|
|
433
|
+
domObserver = null;
|
|
434
|
+
}
|
|
435
|
+
// Nothing observes the document from here on; a verdict recorded now could
|
|
436
|
+
// not be trusted after the next apply().
|
|
437
|
+
forgetAbsentSelectors();
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Observe the document for the two kinds of change that can make a cached-absent
|
|
441
|
+
* target discoverable again (see nodeOf): node insertion/removal, and the
|
|
442
|
+
* class / compat-stamp attributes the cached selectors match on.
|
|
443
|
+
*
|
|
444
|
+
* Body class writes are excluded by target: the layer toggles three body
|
|
445
|
+
* classes every tick, and counting those would invalidate the negative cache
|
|
446
|
+
* on every tick and restore the very scan this cache removes. They cannot
|
|
447
|
+
* create a target either — every cached selector matches an element other
|
|
448
|
+
* than <body>.
|
|
449
|
+
*
|
|
450
|
+
* The record queue is drained on a microtask, so this stays off the layout
|
|
451
|
+
* path; a chat turn's insertions are a handful per second, not per element.
|
|
452
|
+
*/
|
|
453
|
+
function ensureDomObserver() {
|
|
454
|
+
if (domObserver !== null || typeof MutationObserver === 'undefined' || !document.body)
|
|
455
|
+
return;
|
|
456
|
+
domObserver = new MutationObserver((records) => {
|
|
457
|
+
for (const record of records) {
|
|
458
|
+
if (record.type === 'childList' || record.target !== document.body) {
|
|
459
|
+
noteDomChange();
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
domObserver.observe(document.body, {
|
|
465
|
+
childList: true,
|
|
466
|
+
subtree: true,
|
|
467
|
+
attributes: true,
|
|
468
|
+
attributeFilter: ['class', 'data-dsh-frame'],
|
|
469
|
+
});
|
|
407
470
|
}
|
|
408
471
|
/** Start/stop the 600ms sync tick; a no-op when already in the asked state. */
|
|
409
472
|
function setWhaleTimer(on) {
|
|
@@ -701,7 +764,7 @@ export function startMobileAdapt() {
|
|
|
701
764
|
function disableRowDrag() {
|
|
702
765
|
if (!active)
|
|
703
766
|
return;
|
|
704
|
-
const rows =
|
|
767
|
+
const rows = sidebarRows();
|
|
705
768
|
for (const row of rows) {
|
|
706
769
|
if (row.getAttribute('draggable') !== 'false') {
|
|
707
770
|
if (!dragOverridden.has(row))
|
|
@@ -732,17 +795,88 @@ export function startMobileAdapt() {
|
|
|
732
795
|
* node on a major re-render, which the isConnected guard detects.
|
|
733
796
|
*/
|
|
734
797
|
const nodeCache = new Map();
|
|
798
|
+
/**
|
|
799
|
+
* Selectors known to be absent as of {@link domGeneration}. A miss is the
|
|
800
|
+
* common case, not the exception: the layer holds no overlay of its own, and
|
|
801
|
+
* several official surfaces it looks for (tabs row, tools row, sidebar rows)
|
|
802
|
+
* legitimately do not exist at once. Re-walking a conversation-sized document
|
|
803
|
+
* for the same absent selector on every 600ms tick was waste that almost
|
|
804
|
+
* always resolved to null: five document-wide lookups per tick on the measured
|
|
805
|
+
* fixture, zero after this cache. That is a lookup count — the wall-clock
|
|
806
|
+
* saving is real while the mounted content is quiet and shrinks to the
|
|
807
|
+
* row-scope saving when a turn streams (see the Agent Note for the harness and
|
|
808
|
+
* the before/after numbers).
|
|
809
|
+
*
|
|
810
|
+
* A miss is trusted only while the observed content has not changed, so a
|
|
811
|
+
* surface that appears is still found on the very next tick — the same
|
|
812
|
+
* discovery latency the unconditional probe had. The cache is never aged by a
|
|
813
|
+
* clock: it is cleared by {@link noteDomChange} while the observer is live and
|
|
814
|
+
* by {@link forgetAbsentSelectors} across the windows where it is not (apply
|
|
815
|
+
* and revert). One blind spot is deliberate and load-bearing: the observer
|
|
816
|
+
* watches <body>, so a head-resident target is not covered by that signal —
|
|
817
|
+
* the one such target is seeded into the cache where it is created (see
|
|
818
|
+
* ensureAdaptStyle).
|
|
819
|
+
*/
|
|
820
|
+
const nodeMissCache = new Set();
|
|
821
|
+
/**
|
|
822
|
+
* Bumped whenever a cached target may have appeared or disappeared. A surface
|
|
823
|
+
* qualifies on a class token or on the aggregate compat stamp
|
|
824
|
+
* (`data-dsh-frame`), so both insertions/removals AND those two attributes
|
|
825
|
+
* can change the answer. Nothing the layer writes per tick lands here: its own
|
|
826
|
+
* class writes are all on <body>, and its row/transform/label writes touch
|
|
827
|
+
* other attributes entirely (see ensureDomObserver).
|
|
828
|
+
*/
|
|
829
|
+
let domGeneration = 0;
|
|
830
|
+
/** The generation the negative cache was recorded against. */
|
|
831
|
+
let missGeneration = -1;
|
|
832
|
+
function noteDomChange() {
|
|
833
|
+
domGeneration += 1;
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Drop every cached "absent" verdict. Called on apply and revert: while the
|
|
837
|
+
* observer is disconnected the layer is blind to insertions, so a verdict
|
|
838
|
+
* recorded before the gap could otherwise outlive the change that falsified
|
|
839
|
+
* it and be served without a probe. The positive cache needs no equivalent —
|
|
840
|
+
* its `isConnected` guard already re-resolves a replaced node.
|
|
841
|
+
*/
|
|
842
|
+
function forgetAbsentSelectors() {
|
|
843
|
+
nodeMissCache.clear();
|
|
844
|
+
domGeneration += 1;
|
|
845
|
+
}
|
|
735
846
|
function nodeOf(selector) {
|
|
736
847
|
const cached = nodeCache.get(selector);
|
|
737
|
-
if (cached !== undefined
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
848
|
+
if (cached !== undefined) {
|
|
849
|
+
// A node React replaced is no longer connected; drop it and fall through
|
|
850
|
+
// so the lookup below can find its replacement.
|
|
851
|
+
if (cached.isConnected)
|
|
852
|
+
return cached;
|
|
741
853
|
nodeCache.delete(selector);
|
|
742
|
-
|
|
743
|
-
|
|
854
|
+
}
|
|
855
|
+
if (missGeneration !== domGeneration) {
|
|
856
|
+
nodeMissCache.clear();
|
|
857
|
+
missGeneration = domGeneration;
|
|
858
|
+
}
|
|
859
|
+
if (nodeMissCache.has(selector))
|
|
860
|
+
return null;
|
|
861
|
+
const found = document.querySelector(selector);
|
|
862
|
+
if (found === null) {
|
|
863
|
+
nodeMissCache.add(selector);
|
|
864
|
+
return null;
|
|
865
|
+
}
|
|
866
|
+
nodeCache.set(selector, found);
|
|
744
867
|
return found;
|
|
745
868
|
}
|
|
869
|
+
/**
|
|
870
|
+
* The session/project rows the drag suppression covers, scoped to the cached
|
|
871
|
+
* sidebar root. Each row carries the sidebar column class as an ancestor, so
|
|
872
|
+
* the document-wide scan this replaces only ever returned rows from here.
|
|
873
|
+
*/
|
|
874
|
+
function sidebarRows() {
|
|
875
|
+
const sidebar = nodeOf(SIDEBAR_SELECTOR);
|
|
876
|
+
if (sidebar === null)
|
|
877
|
+
return [];
|
|
878
|
+
return Array.from(sidebar.querySelectorAll(ROW_SELECTOR));
|
|
879
|
+
}
|
|
746
880
|
/** The official application frame, through the same cached lookup. */
|
|
747
881
|
function frameEl() {
|
|
748
882
|
const frame = nodeOf(APP_FRAME_SELECTOR);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Family settings form bound to the profile entry id a Host actually serves.
|
|
3
|
+
*
|
|
4
|
+
* Under the 0.1.7 settings model a form is addressed by PROFILE ENTRY ID, while
|
|
5
|
+
* a family card knows only its own settings namespace. The family binder
|
|
6
|
+
* (`ctx.get('webUiSettings')`, published by dsh-web-settings) resolves one onto
|
|
7
|
+
* the other and is the preferred transport; this module is the fallback for a
|
|
8
|
+
* page that serves no binder — the group plugin is not installed, or its client
|
|
9
|
+
* half has not applied yet (the aggregate mounts its children in order, and a
|
|
10
|
+
* child still waiting on its own injects publishes nothing).
|
|
11
|
+
*
|
|
12
|
+
* The shared describe mirror is the only local evidence of which row id this
|
|
13
|
+
* profile serves, but it answers asynchronously: at plugin activation it usually
|
|
14
|
+
* holds nothing yet. A one-shot guess therefore cannot be right for every
|
|
15
|
+
* deployment, and the form is bound once per session:
|
|
16
|
+
*
|
|
17
|
+
* - binding the bare namespace made every save answer `No configurable plugin
|
|
18
|
+
* entry "<namespace>"` on an aggregate install (the namespace is not an entry
|
|
19
|
+
* id there), which the card reported as "the deployment did not accept these
|
|
20
|
+
* values";
|
|
21
|
+
* - binding the aggregate row id would break a standalone install, whose row id
|
|
22
|
+
* is the namespace or the package's own `ui-*` row.
|
|
23
|
+
*
|
|
24
|
+
* So the fallback starts on the most likely row and REBINDS the moment the
|
|
25
|
+
* mirror names one of this package's rows. Until the mirror answers, the form
|
|
26
|
+
* reports the binding it holds (usually `loading`), which the card renders as an
|
|
27
|
+
* unavailable form rather than a doomed one.
|
|
28
|
+
*/
|
|
29
|
+
import type { ConfigForm, ConfigForms } from '@deepseek-ai/dsh-client-ui-settings/client';
|
|
30
|
+
/** Options of {@link createServedEntryForm}. */
|
|
31
|
+
export interface ServedEntryFormOptions<T> {
|
|
32
|
+
/** The shared configuration forms service (`ctx.configForms`). */
|
|
33
|
+
forms: ConfigForms;
|
|
34
|
+
/**
|
|
35
|
+
* Profile entry ids this package's rows carry, most likely first: the
|
|
36
|
+
* aggregate's generated row, the package's own standalone row, then the bare
|
|
37
|
+
* namespace. The first is the fallback while the mirror has no answer; the
|
|
38
|
+
* last is the answer when the mirror serves none of them.
|
|
39
|
+
*/
|
|
40
|
+
entryIds: readonly string[];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Create a settings form bound to the profile entry id this Host serves, and
|
|
44
|
+
* rebound whenever the shared describe mirror names a different one of this
|
|
45
|
+
* package's rows.
|
|
46
|
+
* @param options - the shared forms service and this package's candidate row ids.
|
|
47
|
+
* @returns a form delegating to the currently resolved entry's own form.
|
|
48
|
+
*/
|
|
49
|
+
export declare function createServedEntryForm<T>(options: ServedEntryFormOptions<T>): ConfigForm<T>;
|
|
50
|
+
//# sourceMappingURL=settings-entry-form.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings-entry-form.d.ts","sourceRoot":"","sources":["../../../src/client/settings-entry-form.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,KAAK,EAAE,UAAU,EAAsB,WAAW,EAAE,MAAM,4CAA4C,CAAA;AAE7G,gDAAgD;AAChD,MAAM,WAAW,sBAAsB,CAAC,CAAC;IACvC,kEAAkE;IAClE,KAAK,EAAE,WAAW,CAAA;IAClB;;;;;OAKG;IACH,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;CAC5B;AAeD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,OAAO,EAAE,sBAAsB,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CA4E1F"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/** The snapshot a form reports before the Host has answered with this entry. */
|
|
2
|
+
function pendingSnapshot() {
|
|
3
|
+
return {
|
|
4
|
+
status: 'loading',
|
|
5
|
+
value: undefined,
|
|
6
|
+
base: undefined,
|
|
7
|
+
user: undefined,
|
|
8
|
+
revision: undefined,
|
|
9
|
+
writable: false,
|
|
10
|
+
mode: 'host',
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Create a settings form bound to the profile entry id this Host serves, and
|
|
15
|
+
* rebound whenever the shared describe mirror names a different one of this
|
|
16
|
+
* package's rows.
|
|
17
|
+
* @param options - the shared forms service and this package's candidate row ids.
|
|
18
|
+
* @returns a form delegating to the currently resolved entry's own form.
|
|
19
|
+
*/
|
|
20
|
+
export function createServedEntryForm(options) {
|
|
21
|
+
const { forms, entryIds } = options;
|
|
22
|
+
const fallbackId = entryIds[0];
|
|
23
|
+
const namespaceId = entryIds[entryIds.length - 1];
|
|
24
|
+
const listeners = new Set();
|
|
25
|
+
let boundId;
|
|
26
|
+
let bound;
|
|
27
|
+
let offBound;
|
|
28
|
+
let snapshot = pendingSnapshot();
|
|
29
|
+
/** Republish: the delegated snapshot when one is bound, the pending one otherwise. */
|
|
30
|
+
const publish = () => {
|
|
31
|
+
if (bound !== undefined)
|
|
32
|
+
snapshot = bound.getSnapshot();
|
|
33
|
+
for (const listener of [...listeners])
|
|
34
|
+
listener();
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* The entry id the mirror currently justifies. An unanswered or empty mirror
|
|
38
|
+
* is not evidence of absence, so it keeps the first candidate; a mirror that
|
|
39
|
+
* answers without any of this package's rows leaves the namespace itself.
|
|
40
|
+
* @returns the entry id to bind.
|
|
41
|
+
*/
|
|
42
|
+
const resolve = () => {
|
|
43
|
+
let served;
|
|
44
|
+
try {
|
|
45
|
+
served = forms.describe().getSnapshot().view?.namespaces.map(row => row.ns);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// A mirror that refuses the read is unanswered too, not absent.
|
|
49
|
+
served = undefined;
|
|
50
|
+
}
|
|
51
|
+
if (served === undefined || served.length === 0)
|
|
52
|
+
return fallbackId;
|
|
53
|
+
return entryIds.find(id => served.includes(id)) ?? namespaceId;
|
|
54
|
+
};
|
|
55
|
+
/** Bind (or rebind) the resolved entry's form; a no-op while it is unchanged. */
|
|
56
|
+
const bind = () => {
|
|
57
|
+
const target = resolve();
|
|
58
|
+
if (target === boundId)
|
|
59
|
+
return;
|
|
60
|
+
offBound?.();
|
|
61
|
+
offBound = undefined;
|
|
62
|
+
let form;
|
|
63
|
+
try {
|
|
64
|
+
form = forms.get(target);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// A service that refuses the id stays unbound; the next mirror answer
|
|
68
|
+
// retries rather than pinning a wrong entry for the session.
|
|
69
|
+
boundId = undefined;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
boundId = target;
|
|
73
|
+
bound = form;
|
|
74
|
+
offBound = form.subscribe(() => { publish(); });
|
|
75
|
+
publish();
|
|
76
|
+
};
|
|
77
|
+
// The mirror subscription lives for the page: the card owns its form for the
|
|
78
|
+
// session, and the ConfigForm contract carries no disposer to hang it on. The
|
|
79
|
+
// callback is cheap (a resolve plus a no-op while the id is unchanged). A
|
|
80
|
+
// mirror without a subscription seat keeps the initial binding.
|
|
81
|
+
try {
|
|
82
|
+
forms.describe().subscribe(() => { bind(); });
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// A describe face that refuses the subscription: the initial binding stands.
|
|
86
|
+
}
|
|
87
|
+
bind();
|
|
88
|
+
return {
|
|
89
|
+
getSnapshot: () => snapshot,
|
|
90
|
+
subscribe: (listener) => {
|
|
91
|
+
listeners.add(listener);
|
|
92
|
+
return () => { listeners.delete(listener); };
|
|
93
|
+
},
|
|
94
|
+
set: (field, value) => bound?.set(field, value) ?? Promise.resolve(false),
|
|
95
|
+
unset: (field) => bound?.unset(field) ?? Promise.resolve(false),
|
|
96
|
+
mutate: (ops, expectedRevision) => bound?.mutate(ops, expectedRevision) ?? Promise.resolve(false),
|
|
97
|
+
};
|
|
98
|
+
}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -10,10 +10,19 @@
|
|
|
10
10
|
import type { IncomingMessage } from 'node:http';
|
|
11
11
|
import type { Context } from '@deepseek-ai/cordis';
|
|
12
12
|
import type { SettingsNamespace } from '@deepseek-ai/dsh-settings';
|
|
13
|
-
import z from 'schemastery';
|
|
13
|
+
import z from '@deepseek-ai/schemastery';
|
|
14
14
|
import { type PairingConfig } from './pairing.ts';
|
|
15
15
|
declare module '@deepseek-ai/cordis' {
|
|
16
16
|
interface Events {
|
|
17
|
+
/**
|
|
18
|
+
* Volatile config values were committed into the running fiber without a
|
|
19
|
+
* remount; dispatched to the owning fiber only. Spelled here because the
|
|
20
|
+
* Loader package is not a dependency of this plugin, with the Loader's own
|
|
21
|
+
* shape so the two declarations merge when a Host program carries both.
|
|
22
|
+
* @param paths - changed config paths as key arrays; every value is committed before dispatch.
|
|
23
|
+
* @mode emit
|
|
24
|
+
*/
|
|
25
|
+
'loader/volatile-update'(paths: readonly (readonly string[])[]): void;
|
|
17
26
|
/**
|
|
18
27
|
* Waterfall seam on the /api transport fence: the connection plugin
|
|
19
28
|
* fires this per /api request before bridging to the API proxy on
|
|
@@ -147,16 +156,71 @@ export interface Config {
|
|
|
147
156
|
*/
|
|
148
157
|
lanBind?: boolean;
|
|
149
158
|
/**
|
|
150
|
-
* The profile whose cordis.patch.yml the LAN bind toggle manages.
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
159
|
+
* The profile whose cordis.patch.yml the LAN bind toggle manages. Defaults
|
|
160
|
+
* to the profile the Host booted (its `profileContext`), then to the
|
|
161
|
+
* DSH_PROFILE environment variable, then "web" — the Desktop client boots
|
|
162
|
+
* "desktop" without exporting DSH_PROFILE, so the environment alone names
|
|
163
|
+
* the wrong profile there. Must be a single safe path segment (the runtime
|
|
164
|
+
* and environment fallbacks bypass this schema, so the path builder asserts
|
|
165
|
+
* containment independently).
|
|
154
166
|
*/
|
|
155
167
|
profile?: string;
|
|
156
168
|
/** Master switch for the plugin (browser half + host pairing surfaces). */
|
|
157
169
|
enabled?: boolean;
|
|
158
170
|
}
|
|
159
|
-
|
|
171
|
+
/**
|
|
172
|
+
* Plugin config schema. Under the 0.1.7 settings model this schema IS the
|
|
173
|
+
* entry's settings page: the Host derives one form per profile entry from it
|
|
174
|
+
* and serves that form only when at least one field is `volatile()`. The marker
|
|
175
|
+
* is also what admits a write and what keeps the edit on the live path — the
|
|
176
|
+
* Loader commits the new value into the field's reference and announces
|
|
177
|
+
* `loader/volatile-update` on this fiber instead of remounting the row, so the
|
|
178
|
+
* pairing service, its device sessions, the tunnel and the route registrations
|
|
179
|
+
* survive a settings save (see {@link applyImpl}'s sync).
|
|
180
|
+
*
|
|
181
|
+
* The schema is left to inference rather than annotated with `z<Config>`: a
|
|
182
|
+
* volatile field's parsed output is a live reference while its accepted input
|
|
183
|
+
* stays the plain value, so the two sides no longer share one shape and the
|
|
184
|
+
* annotation would reject the schema the Host must be given.
|
|
185
|
+
*
|
|
186
|
+
* The deployment-level fields (`trustedHosts`, `devicesFile`, `profile`) are
|
|
187
|
+
* deliberately NOT volatile: they belong in the profile patch, so the form
|
|
188
|
+
* leaves them to the operator instead of offering a card control that a
|
|
189
|
+
* document write could not honor.
|
|
190
|
+
*/
|
|
191
|
+
export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
192
|
+
tokenTtlMs: z<number, number, "volatile-defined">;
|
|
193
|
+
offlineAfterMs: z<number, number, "volatile-defined">;
|
|
194
|
+
maxDevices: z<number, number, "volatile-defined">;
|
|
195
|
+
idleExpireMs: z<number, number, "volatile-defined">;
|
|
196
|
+
cookieName: z<string, string, "volatile-defined">;
|
|
197
|
+
requirePairingForLan: z<boolean, boolean, "volatile-defined">;
|
|
198
|
+
publicBaseUrl: z<string, string, "volatile">;
|
|
199
|
+
trustedHosts: z<string[], string[], "plain">;
|
|
200
|
+
devicesFile: z<string, string, "plain">;
|
|
201
|
+
autoTunnel: z<boolean, boolean, "volatile-defined">;
|
|
202
|
+
tunnelToken: z<string, string, "volatile">;
|
|
203
|
+
relay: z<boolean, boolean, "volatile-defined">;
|
|
204
|
+
lanBind: z<boolean, boolean, "volatile">;
|
|
205
|
+
profile: z<string, string, "plain">;
|
|
206
|
+
enabled: z<boolean, boolean, "volatile-defined">;
|
|
207
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
208
|
+
tokenTtlMs: z<number, number, "volatile-defined">;
|
|
209
|
+
offlineAfterMs: z<number, number, "volatile-defined">;
|
|
210
|
+
maxDevices: z<number, number, "volatile-defined">;
|
|
211
|
+
idleExpireMs: z<number, number, "volatile-defined">;
|
|
212
|
+
cookieName: z<string, string, "volatile-defined">;
|
|
213
|
+
requirePairingForLan: z<boolean, boolean, "volatile-defined">;
|
|
214
|
+
publicBaseUrl: z<string, string, "volatile">;
|
|
215
|
+
trustedHosts: z<string[], string[], "plain">;
|
|
216
|
+
devicesFile: z<string, string, "plain">;
|
|
217
|
+
autoTunnel: z<boolean, boolean, "volatile-defined">;
|
|
218
|
+
tunnelToken: z<string, string, "volatile">;
|
|
219
|
+
relay: z<boolean, boolean, "volatile-defined">;
|
|
220
|
+
lanBind: z<boolean, boolean, "volatile">;
|
|
221
|
+
profile: z<string, string, "plain">;
|
|
222
|
+
enabled: z<boolean, boolean, "volatile-defined">;
|
|
223
|
+
}>>, "plain">;
|
|
160
224
|
/**
|
|
161
225
|
* Fully resolved config: every field non-optional except `publicBaseUrl`,
|
|
162
226
|
* which legitimately resolves to `undefined` when unset (the schema keeps it
|
|
@@ -171,6 +235,34 @@ type ResolvedConfig = Required<Omit<Config, 'publicBaseUrl' | 'trustedHosts' | '
|
|
|
171
235
|
tunnelToken: string | undefined;
|
|
172
236
|
profile: string;
|
|
173
237
|
};
|
|
238
|
+
/**
|
|
239
|
+
* The stable reference a `volatile()` config field resolves to; its owner
|
|
240
|
+
* updates it in place when the user saves.
|
|
241
|
+
*/
|
|
242
|
+
interface ConfigRef<T> {
|
|
243
|
+
/** @returns the value currently committed for the running instance. */
|
|
244
|
+
get(): T;
|
|
245
|
+
}
|
|
246
|
+
/** One resolved config field: a live reference, or a plain value from a hand-built context. */
|
|
247
|
+
type ConfigField<T> = ConfigRef<T> | T;
|
|
248
|
+
/** The config the Host hands {@link applyImpl} — the runtime face of {@link Config}. */
|
|
249
|
+
export interface ResolvedConfigFields {
|
|
250
|
+
tokenTtlMs?: ConfigField<number>;
|
|
251
|
+
offlineAfterMs?: ConfigField<number>;
|
|
252
|
+
maxDevices?: ConfigField<number>;
|
|
253
|
+
idleExpireMs?: ConfigField<number>;
|
|
254
|
+
cookieName?: ConfigField<string>;
|
|
255
|
+
requirePairingForLan?: ConfigField<boolean>;
|
|
256
|
+
publicBaseUrl?: ConfigField<string>;
|
|
257
|
+
trustedHosts?: ConfigField<string[]>;
|
|
258
|
+
devicesFile?: ConfigField<string>;
|
|
259
|
+
autoTunnel?: ConfigField<boolean>;
|
|
260
|
+
tunnelToken?: ConfigField<string>;
|
|
261
|
+
relay?: ConfigField<boolean>;
|
|
262
|
+
lanBind?: ConfigField<boolean>;
|
|
263
|
+
profile?: ConfigField<string>;
|
|
264
|
+
enabled?: ConfigField<boolean>;
|
|
265
|
+
}
|
|
174
266
|
/**
|
|
175
267
|
* The single mapping from resolved plugin config to the pairing service
|
|
176
268
|
* config. Both the constructed service and every later apply of this row
|
|
@@ -185,6 +277,6 @@ export declare function defaultDevicesFile(home?: string): string;
|
|
|
185
277
|
* @param config - resolved plugin config (schema defaults applied by the loader).
|
|
186
278
|
*/
|
|
187
279
|
export declare const apply: typeof applyImpl;
|
|
188
|
-
declare function applyImpl(ctx: Context, config?:
|
|
280
|
+
declare function applyImpl(ctx: Context, config?: ResolvedConfigFields): void;
|
|
189
281
|
export {};
|
|
190
282
|
//# sourceMappingURL=index.d.ts.map
|
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;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
|