super-dsh 0.1.0 → 0.1.2
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/agent-agy/bridge/agy_bridge.py +250 -0
- package/agent-agy/bridge/gemini_openai_server.mjs +98 -0
- package/agent-agy/cordis.patch.yml +49 -0
- package/agent-agy/dist/adapter.d.ts +15 -0
- package/agent-agy/dist/adapter.js +42 -0
- package/agent-agy/dist/agent-preset-agy.d.ts +47 -0
- package/agent-agy/dist/agent-preset-agy.js +169 -0
- package/agent-agy/dist/agent-preset-projection.d.ts +31 -0
- package/agent-agy/dist/agent-preset-projection.js +33 -0
- package/agent-agy/dist/agent.d.ts +137 -0
- package/agent-agy/dist/agent.js +493 -0
- package/agent-agy/dist/agy-cli-client.d.ts +137 -0
- package/agent-agy/dist/agy-cli-client.js +301 -0
- package/agent-agy/dist/agy-client.d.ts +160 -0
- package/agent-agy/dist/agy-client.js +252 -0
- package/agent-agy/dist/agy-sessions.d.ts +24 -0
- package/agent-agy/dist/agy-sessions.js +116 -0
- package/agent-agy/dist/agy-store.d.ts +39 -0
- package/agent-agy/dist/agy-store.js +83 -0
- package/agent-agy/dist/inbox.d.ts +26 -0
- package/agent-agy/dist/inbox.js +53 -0
- package/agent-agy/dist/index.d.ts +80 -0
- package/agent-agy/dist/index.js +505 -0
- package/agent-agy/dist/models.d.ts +41 -0
- package/agent-agy/dist/models.js +74 -0
- package/agent-agy/dist/world-plugin.d.ts +9 -0
- package/agent-agy/dist/world-plugin.js +97 -0
- package/agent-agy/package.json +64 -0
- package/agent-claude/cordis.patch.yml +42 -0
- package/agent-claude/dist/adapter.js +62 -0
- package/agent-claude/dist/agent-preset-claude.js +169 -0
- package/agent-claude/dist/agent-preset-projection.js +30 -0
- package/agent-claude/dist/agent.js +1271 -0
- package/agent-claude/dist/claude-client.js +357 -0
- package/agent-claude/dist/claude-events.js +223 -0
- package/agent-claude/dist/claude-home.js +29 -0
- package/agent-claude/dist/commands.js +182 -0
- package/agent-claude/dist/content.js +134 -0
- package/agent-claude/dist/inbox.js +53 -0
- package/agent-claude/dist/index.js +555 -0
- package/agent-claude/dist/input-queue.js +64 -0
- package/agent-claude/dist/interaction.js +271 -0
- package/agent-claude/dist/models.js +158 -0
- package/agent-claude/dist/permission.js +120 -0
- package/agent-claude/dist/session-id.js +20 -0
- package/agent-claude/dist/session-map.js +165 -0
- package/agent-claude/dist/world-plugin.js +14 -0
- package/agent-claude/package.json +74 -0
- package/agent-codex/README.md +74 -0
- package/agent-codex/cordis.patch.yml +77 -0
- package/agent-codex/dist/adapter.js +55 -0
- package/agent-codex/dist/agent-preset-codex.js +171 -0
- package/agent-codex/dist/agent-preset-projection.js +37 -0
- package/agent-codex/dist/agent.js +1056 -0
- package/agent-codex/dist/app-home.js +65 -0
- package/agent-codex/dist/codex-client.js +469 -0
- package/agent-codex/dist/codex-events.js +297 -0
- package/agent-codex/dist/codex-store.js +190 -0
- package/agent-codex/dist/inbox.js +53 -0
- package/agent-codex/dist/index.js +605 -0
- package/agent-codex/dist/models.js +152 -0
- package/agent-codex/dist/permission.js +71 -0
- package/agent-codex/dist/session-map.js +170 -0
- package/agent-codex/dist/spike.js +46 -0
- package/agent-codex/dist/world-plugin.js +96 -0
- package/agent-codex/package.json +78 -0
- package/agent-hermes/README.md +144 -0
- package/agent-hermes/cordis.patch.yml +83 -0
- package/agent-hermes/dist/adapter.js +83 -0
- package/agent-hermes/dist/agent-preset-hermes.js +171 -0
- package/agent-hermes/dist/agent-preset-projection.js +34 -0
- package/agent-hermes/dist/agent.js +1068 -0
- package/agent-hermes/dist/hermes-client.js +953 -0
- package/agent-hermes/dist/hermes-events.js +289 -0
- package/agent-hermes/dist/hermes-store.js +158 -0
- package/agent-hermes/dist/inbox.js +53 -0
- package/agent-hermes/dist/index.js +541 -0
- package/agent-hermes/dist/models.js +324 -0
- package/agent-hermes/dist/permission.js +60 -0
- package/agent-hermes/dist/world-plugin.js +101 -0
- package/agent-hermes/package.json +76 -0
- package/agent-hub/cordis.patch.yml +12 -0
- package/agent-hub/dist/agent-roster.d.ts +34 -0
- package/agent-hub/dist/agent-roster.js +27 -0
- package/agent-hub/dist/carrier.d.ts +43 -0
- package/agent-hub/dist/carrier.js +241 -0
- package/agent-hub/dist/client-shim.d.ts +28 -0
- package/agent-hub/dist/client-shim.js +251 -0
- package/agent-hub/dist/envelope.d.ts +54 -0
- package/agent-hub/dist/envelope.js +92 -0
- package/agent-hub/dist/gateway.d.ts +28 -0
- package/agent-hub/dist/gateway.js +18 -0
- package/agent-hub/dist/index-pass.d.ts +22 -0
- package/agent-hub/dist/index-pass.js +47 -0
- package/agent-hub/dist/index.d.ts +29 -0
- package/agent-hub/dist/index.js +29 -0
- package/agent-hub/dist/labels.d.ts +45 -0
- package/agent-hub/dist/labels.js +83 -0
- package/agent-hub/dist/ownership.d.ts +37 -0
- package/agent-hub/dist/ownership.js +54 -0
- package/agent-hub/dist/roster.d.ts +28 -0
- package/agent-hub/dist/roster.js +38 -0
- package/agent-hub/dist/rpc.d.ts +38 -0
- package/agent-hub/dist/rpc.js +52 -0
- package/agent-hub/dist/spawn-world.d.ts +46 -0
- package/agent-hub/dist/spawn-world.js +72 -0
- package/agent-hub/dist/targets.d.ts +33 -0
- package/agent-hub/dist/targets.js +30 -0
- package/agent-hub/dist/world-entry.d.ts +31 -0
- package/agent-hub/dist/world-entry.js +51 -0
- package/agent-hub/dist/world-host.d.ts +69 -0
- package/agent-hub/dist/world-host.js +44 -0
- package/agent-hub/dist/world-join.d.ts +16 -0
- package/agent-hub/dist/world-join.js +122 -0
- package/agent-hub/dist/world-mount.d.ts +52 -0
- package/agent-hub/dist/world-mount.js +52 -0
- package/agent-hub/dist/world-mux.d.ts +72 -0
- package/agent-hub/dist/world-mux.js +224 -0
- package/agent-hub/dist/world-provision.d.ts +20 -0
- package/agent-hub/dist/world-provision.js +122 -0
- package/agent-hub/dist/world-web-server.d.ts +162 -0
- package/agent-hub/dist/world-web-server.js +248 -0
- package/agent-hub/lib/client/index.js +242 -0
- package/agent-hub/package.json +65 -0
- package/agent-omp/README.md +77 -0
- package/agent-omp/cordis.patch.yml +92 -0
- package/agent-omp/dist/adapter.js +36 -0
- package/agent-omp/dist/agent-preset-omp.js +171 -0
- package/agent-omp/dist/agent-preset-projection.js +36 -0
- package/agent-omp/dist/agent.js +1145 -0
- package/agent-omp/dist/app-home.js +86 -0
- package/agent-omp/dist/discovery.js +116 -0
- package/agent-omp/dist/inbox.js +53 -0
- package/agent-omp/dist/index.js +574 -0
- package/agent-omp/dist/knobs.js +50 -0
- package/agent-omp/dist/lazy-rpc.js +161 -0
- package/agent-omp/dist/mobile/gesture.js +148 -0
- package/agent-omp/dist/mobile/zoom-guard.js +294 -0
- package/agent-omp/dist/mobile-boot.js +38 -0
- package/agent-omp/dist/models.js +299 -0
- package/agent-omp/dist/omp-cli.js +147 -0
- package/agent-omp/dist/omp-disk-discovery.js +178 -0
- package/agent-omp/dist/omp-store.js +518 -0
- package/agent-omp/dist/pairing.js +45 -0
- package/agent-omp/dist/permission.js +96 -0
- package/agent-omp/dist/protocol.js +13 -0
- package/agent-omp/dist/rpc-types.js +1 -0
- package/agent-omp/dist/sdk-client.js +378 -0
- package/agent-omp/dist/sidecar-client.js +195 -0
- package/agent-omp/dist/store/adapter.js +47 -0
- package/agent-omp/dist/store/db.js +210 -0
- package/agent-omp/dist/store/index.js +59 -0
- package/agent-omp/dist/store/reconcile.js +51 -0
- package/agent-omp/dist/store/schema.js +40 -0
- package/agent-omp/dist/world-plugin.js +96 -0
- package/agent-omp/package.json +97 -0
- package/agent-omp/sidecar/main.ts +481 -0
- package/agent-pi/README.md +67 -0
- package/agent-pi/cordis.patch.yml +80 -0
- package/agent-pi/dist/adapter.js +53 -0
- package/agent-pi/dist/agent-preset-pi.js +169 -0
- package/agent-pi/dist/agent-preset-projection.js +29 -0
- package/agent-pi/dist/agent.js +949 -0
- package/agent-pi/dist/inbox.js +53 -0
- package/agent-pi/dist/index.js +595 -0
- package/agent-pi/dist/knobs.js +24 -0
- package/agent-pi/dist/models.js +200 -0
- package/agent-pi/dist/permission.js +66 -0
- package/agent-pi/dist/pi-client.js +224 -0
- package/agent-pi/dist/pi-events.js +155 -0
- package/agent-pi/dist/pi-home.js +55 -0
- package/agent-pi/dist/safe-json.js +25 -0
- package/agent-pi/dist/session-map.js +119 -0
- package/agent-pi/dist/world-plugin.js +97 -0
- package/agent-pi/package.json +77 -0
- package/cordis.patch.yml +22 -19
- package/package.json +88 -25
- package/client.js +0 -4
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { OmpSdkClient } from "./sdk-client.js";
|
|
2
|
+
/** Synthetic state for a not-yet-spawned child: quiescent by definition. */
|
|
3
|
+
const IDLE_STATE = { isStreaming: false };
|
|
4
|
+
export class LazyOmpRpc {
|
|
5
|
+
#spawnArgs;
|
|
6
|
+
#cwd;
|
|
7
|
+
#client;
|
|
8
|
+
#spawn;
|
|
9
|
+
#closed = false;
|
|
10
|
+
#eventListeners = new Set();
|
|
11
|
+
#titleListeners = new Set();
|
|
12
|
+
#failureListeners = new Set();
|
|
13
|
+
#pendingModel;
|
|
14
|
+
#onSpawned;
|
|
15
|
+
constructor(spawnArgs, cwd) {
|
|
16
|
+
this.#spawnArgs = spawnArgs;
|
|
17
|
+
this.#cwd = cwd;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Bridge hook: runs once when the child first exists (the caller writes the
|
|
21
|
+
* index row + supervisor held-baseline there — the create-time counterparts
|
|
22
|
+
* were deliberately skipped). Registered before or after the spawn; both
|
|
23
|
+
* orders fire exactly once.
|
|
24
|
+
*/
|
|
25
|
+
onSpawned(handler) {
|
|
26
|
+
this.#onSpawned = handler;
|
|
27
|
+
if (this.#client !== undefined)
|
|
28
|
+
handler(this.#client);
|
|
29
|
+
}
|
|
30
|
+
get spawned() {
|
|
31
|
+
return this.#client !== undefined;
|
|
32
|
+
}
|
|
33
|
+
/** Spawn once; concurrent and later callers share the same child. */
|
|
34
|
+
#ensure() {
|
|
35
|
+
if (this.#client !== undefined)
|
|
36
|
+
return Promise.resolve(this.#client);
|
|
37
|
+
if (this.#spawn === undefined) {
|
|
38
|
+
this.#spawn = OmpSdkClient.spawn(this.#spawnArgs, this.#cwd).then((client) => {
|
|
39
|
+
// A close() that raced the in-flight spawn wins: dispose immediately.
|
|
40
|
+
if (this.#closed) {
|
|
41
|
+
client.close();
|
|
42
|
+
return client;
|
|
43
|
+
}
|
|
44
|
+
this.#client = client;
|
|
45
|
+
for (const listener of this.#eventListeners)
|
|
46
|
+
client.on(listener);
|
|
47
|
+
for (const listener of this.#titleListeners)
|
|
48
|
+
client.onTitle(listener);
|
|
49
|
+
for (const listener of this.#failureListeners)
|
|
50
|
+
client.onFailure(listener);
|
|
51
|
+
this.#onSpawned?.(client);
|
|
52
|
+
return client;
|
|
53
|
+
}).catch((error) => {
|
|
54
|
+
// Allow a later dispatch to retry the spawn.
|
|
55
|
+
this.#spawn = undefined;
|
|
56
|
+
throw error;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return this.#spawn;
|
|
60
|
+
}
|
|
61
|
+
on(listener) {
|
|
62
|
+
this.#eventListeners.add(listener);
|
|
63
|
+
if (this.#client !== undefined)
|
|
64
|
+
this.#client.on(listener);
|
|
65
|
+
return () => {
|
|
66
|
+
this.#eventListeners.delete(listener);
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Title frames must survive the deferred spawn: OMP generates the title from
|
|
71
|
+
* the first prompt, which is exactly the moment this RPC materializes, so a
|
|
72
|
+
* listener registered before then would otherwise miss the only frame.
|
|
73
|
+
*/
|
|
74
|
+
onTitle(listener) {
|
|
75
|
+
this.#titleListeners.add(listener);
|
|
76
|
+
if (this.#client !== undefined)
|
|
77
|
+
this.#client.onTitle(listener);
|
|
78
|
+
return () => {
|
|
79
|
+
this.#titleListeners.delete(listener);
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
onFailure(listener) {
|
|
83
|
+
this.#failureListeners.add(listener);
|
|
84
|
+
if (this.#client !== undefined)
|
|
85
|
+
this.#client.onFailure(listener);
|
|
86
|
+
return () => {
|
|
87
|
+
this.#failureListeners.delete(listener);
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
async prompt(message) {
|
|
91
|
+
const client = await this.#ensure();
|
|
92
|
+
// Apply a pre-spawn model selection now, ahead of the prompt (the sidecar
|
|
93
|
+
// records it as the session's model_change, like a pre-turn set_model).
|
|
94
|
+
const pending = this.#pendingModel;
|
|
95
|
+
if (pending !== undefined) {
|
|
96
|
+
this.#pendingModel = undefined;
|
|
97
|
+
await client.setModel(pending.provider, pending.modelId).catch(() => { });
|
|
98
|
+
}
|
|
99
|
+
return client.prompt(message);
|
|
100
|
+
}
|
|
101
|
+
async followUp(message) {
|
|
102
|
+
return (await this.#ensure()).followUp(message);
|
|
103
|
+
}
|
|
104
|
+
async steer(message) {
|
|
105
|
+
return (await this.#ensure()).steer(message);
|
|
106
|
+
}
|
|
107
|
+
async compact(instructions) {
|
|
108
|
+
return (await this.#ensure()).compact(instructions);
|
|
109
|
+
}
|
|
110
|
+
async contextUsage() {
|
|
111
|
+
if (this.#client === undefined)
|
|
112
|
+
return undefined;
|
|
113
|
+
return this.#client.contextUsage();
|
|
114
|
+
}
|
|
115
|
+
async abort() {
|
|
116
|
+
if (this.#client === undefined)
|
|
117
|
+
return undefined;
|
|
118
|
+
return this.#client.abort();
|
|
119
|
+
}
|
|
120
|
+
async ensureStarted() {
|
|
121
|
+
await this.#ensure();
|
|
122
|
+
}
|
|
123
|
+
async getState() {
|
|
124
|
+
if (this.#client === undefined)
|
|
125
|
+
return IDLE_STATE;
|
|
126
|
+
return this.#client.getState();
|
|
127
|
+
}
|
|
128
|
+
async getSubagents() {
|
|
129
|
+
if (this.#client === undefined)
|
|
130
|
+
return [];
|
|
131
|
+
return this.#client.getSubagents();
|
|
132
|
+
}
|
|
133
|
+
async getSessionStats() {
|
|
134
|
+
if (this.#client === undefined)
|
|
135
|
+
return {};
|
|
136
|
+
return this.#client.getSessionStats();
|
|
137
|
+
}
|
|
138
|
+
async getSystemPrompt() {
|
|
139
|
+
if (this.#client === undefined)
|
|
140
|
+
return undefined;
|
|
141
|
+
return this.#client.getSystemPrompt();
|
|
142
|
+
}
|
|
143
|
+
async setModel(provider, modelId) {
|
|
144
|
+
if (this.#client === undefined) {
|
|
145
|
+
// Park it: applied by prompt() immediately after the spawn.
|
|
146
|
+
this.#pendingModel = { provider, modelId };
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
return this.#client.setModel(provider, modelId);
|
|
150
|
+
}
|
|
151
|
+
sendRaw(command) {
|
|
152
|
+
if (this.#client !== undefined)
|
|
153
|
+
this.#client.sendRaw(command);
|
|
154
|
+
}
|
|
155
|
+
close() {
|
|
156
|
+
this.#closed = true;
|
|
157
|
+
const client = this.#client;
|
|
158
|
+
this.#client = undefined;
|
|
159
|
+
client?.close();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mobile swipe recognition (pure, unit-tested) — an EXACT port of the
|
|
3
|
+
* operator's own ui-layout patch (retired `z_dsh-alpha` checkout, commit
|
|
4
|
+
* `1706b81` "ui-layout: mobile swipe opens Better Sidebar right panel via
|
|
5
|
+
* its DOM toggle", `packages/client/ui-layout/src/client/AppFrame.tsx`)
|
|
6
|
+
* from patched-source form into plugin form, plus exactly ONE addition:
|
|
7
|
+
* an average-velocity gate (`swipeVelocityPxPerMs`, the 2026-09-02
|
|
8
|
+
* supplemental requirement — the only constant here without a source
|
|
9
|
+
* precedent; `0` disarms it and restores the source's behavior verbatim).
|
|
10
|
+
*
|
|
11
|
+
* Source semantics, preserved number-for-number:
|
|
12
|
+
* - **Start admission** (pointerdown): with BOTH panels closed, only the
|
|
13
|
+
* left band `x ≤ 120` (the source's X120) or the right three quarters
|
|
14
|
+
* `x ≥ viewport/4` may start a gesture — the center band stays inert so
|
|
15
|
+
* content swipes don't misfire. With a panel open, ANY position may
|
|
16
|
+
* start (the dismiss swipe naturally starts on the panel body).
|
|
17
|
+
* - **Firing** (pointermove, mid-gesture): the gesture fires DURING the
|
|
18
|
+
* drag as soon as `|dx| ≥ 40` AND `|dx| > |dy| × 1.3` (horizontal
|
|
19
|
+
* dominance over scroll intent) — then the drag is spent (one shot).
|
|
20
|
+
* Firing mid-gesture rather than at pointerup is what keeps a lazy
|
|
21
|
+
* swipe triggerable: a pointerup-time decision loses the pointer to the
|
|
22
|
+
* browser's scroll/pan heuristics (pointercancel) whenever the drag is
|
|
23
|
+
* slow, which reads to the user as "I must flick fast to trigger".
|
|
24
|
+
* - **State machine** (symmetric, from the source):
|
|
25
|
+
* left-swipe: close the sidebar if open, else open the right panel
|
|
26
|
+
* (Better Sidebar) when a session is current;
|
|
27
|
+
* right-swipe: close the right panel if open, else open the sidebar
|
|
28
|
+
* when collapsed.
|
|
29
|
+
* - **Band admission (2026-09-11 mobile wave)**: no own pixel cut-off. The
|
|
30
|
+
* feature is live on coarse pointers (phones/tablets) and wherever a
|
|
31
|
+
* panel is already in its narrow-viewport state — i.e. it follows the
|
|
32
|
+
* upstream layout STATE, never a threshold we invented. Upstream may
|
|
33
|
+
* move their auto-collapse boundary (1024 today) freely; we track the
|
|
34
|
+
* resulting state attributes.
|
|
35
|
+
*
|
|
36
|
+
* Both panel reads address OFFICIAL upstream surfaces ONLY (hard rule,
|
|
37
|
+
* 2026-09-11): the left sidebar through the layout service
|
|
38
|
+
* (`ctx.layout.toggleSidebar()`) and AppFrame's `[data-sidebar-collapsed]`;
|
|
39
|
+
* the right panel through the official 0.1.5 controls — expand
|
|
40
|
+
* `[data-sidebar-right-expand]` (conversation header corner) and toggle
|
|
41
|
+
* `[data-sidebar-right-toggle]` (dock chrome) — with its open state read
|
|
42
|
+
* off AppFrame's `[data-rightbar-collapsed]`. Third-party plugin DOM is
|
|
43
|
+
* never a state input again (the retired Better Sidebar coupling poisoned
|
|
44
|
+
* the whole state machine once that plugin was removed).
|
|
45
|
+
*
|
|
46
|
+
* No DOM here: these are pure predicates over gesture facts plus the
|
|
47
|
+
* panel state the caller reads off live DOM.
|
|
48
|
+
*
|
|
49
|
+
* @module omp-web/mobile/gesture
|
|
50
|
+
*/
|
|
51
|
+
/** Resolved defaults — the `1706b81` constants (120 / viewport÷4 / 40 / 1.3)
|
|
52
|
+
* plus the one addition, the velocity gate. A slow text-selection drag runs
|
|
53
|
+
* ~0.13 px/ms; 0.15 px/ms filters those while any comfortable sweep (0.4
|
|
54
|
+
* px/ms and up) passes.
|
|
55
|
+
*/
|
|
56
|
+
export const DEFAULT_SWIPE_THRESHOLDS = {
|
|
57
|
+
swipeDistancePx: 40,
|
|
58
|
+
dominanceRatio: 1.3,
|
|
59
|
+
leftEdgeBandPx: 120,
|
|
60
|
+
rightZoneRatio: 0.25,
|
|
61
|
+
swipeVelocityPxPerMs: 0.15,
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Whether a pointerdown may START a gesture — the source's pointerdown
|
|
65
|
+
* gate, verbatim: with both panels closed, only the left band (X120) or
|
|
66
|
+
* the right three quarters of the screen admit; with a panel open, any
|
|
67
|
+
* position admits (that swipe is a dismiss).
|
|
68
|
+
*
|
|
69
|
+
* @param x - pointerdown client X.
|
|
70
|
+
* @param viewportWidth - viewport width at gesture time (px).
|
|
71
|
+
* @param panels - panel state at pointerdown.
|
|
72
|
+
* @param thresholds - resolved recognition thresholds.
|
|
73
|
+
* @returns `true` when the origin is admitted into tracking.
|
|
74
|
+
*/
|
|
75
|
+
export function admitsSwipeStart(x, viewportWidth, panels, thresholds) {
|
|
76
|
+
if (!(panels.leftCollapsed && !panels.rightOpen))
|
|
77
|
+
return true;
|
|
78
|
+
return x <= thresholds.leftEdgeBandPx || x >= viewportWidth * thresholds.rightZoneRatio;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Classify one in-flight drag into a panel action — the source's
|
|
82
|
+
* pointermove decision, verbatim, plus the velocity gate. Callers fire
|
|
83
|
+
* ONCE (then drop the drag): a `null` here means "not yet / not this
|
|
84
|
+
* gesture", and the same drag may classify on a LATER move event as its
|
|
85
|
+
* displacement or velocity grows.
|
|
86
|
+
*
|
|
87
|
+
* @param progress - the drag's displacement and elapsed time so far.
|
|
88
|
+
* @param panels - panel state read live at this event.
|
|
89
|
+
* @param sessionLive - whether a session is current (gates opening the
|
|
90
|
+
* right panel, mirroring the source's `detailsSession !== undefined`).
|
|
91
|
+
* @param thresholds - resolved recognition thresholds.
|
|
92
|
+
* @returns the action the drag maps to, or `null` when no panel admits
|
|
93
|
+
* it yet.
|
|
94
|
+
*/
|
|
95
|
+
export function classifySwipeProgress(progress, panels, sessionLive, thresholds) {
|
|
96
|
+
const adx = Math.abs(progress.dx);
|
|
97
|
+
const ady = Math.abs(progress.dy);
|
|
98
|
+
if (adx < thresholds.swipeDistancePx)
|
|
99
|
+
return null;
|
|
100
|
+
if (adx <= ady * thresholds.dominanceRatio)
|
|
101
|
+
return null;
|
|
102
|
+
if (thresholds.swipeVelocityPxPerMs > 0) {
|
|
103
|
+
if (progress.dtMs <= 0)
|
|
104
|
+
return null;
|
|
105
|
+
if (adx / progress.dtMs < thresholds.swipeVelocityPxPerMs)
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
if (progress.dx < 0) {
|
|
109
|
+
// Left swipe: close the sidebar if open, otherwise open the right
|
|
110
|
+
// panel when a session is current.
|
|
111
|
+
if (!panels.leftCollapsed)
|
|
112
|
+
return 'close-left';
|
|
113
|
+
if (sessionLive && !panels.rightOpen)
|
|
114
|
+
return 'open-right';
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
// Right swipe: close the right panel if open, otherwise open the
|
|
118
|
+
// sidebar when collapsed.
|
|
119
|
+
if (panels.rightOpen)
|
|
120
|
+
return 'close-right';
|
|
121
|
+
if (panels.leftCollapsed)
|
|
122
|
+
return 'open-left';
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Merge the host-injected page config over the defaults. An absent config
|
|
127
|
+
* (host feature off, or an old cached page) resolves to enabled=false — the
|
|
128
|
+
* client half is inert unless the host opted the page in.
|
|
129
|
+
*
|
|
130
|
+
* @param pageConfig - `window.__OMP_WEB_MOBILE__` as left by the boot script.
|
|
131
|
+
* @returns resolved thresholds plus the enable flag.
|
|
132
|
+
*/
|
|
133
|
+
export function resolveMobileConfig(pageConfig) {
|
|
134
|
+
if (pageConfig === undefined || pageConfig.enabled !== true) {
|
|
135
|
+
return { enabled: false, ...DEFAULT_SWIPE_THRESHOLDS };
|
|
136
|
+
}
|
|
137
|
+
const n = (value, fallback) => typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
138
|
+
// The velocity gate alone accepts 0 (disarm) as a configured value.
|
|
139
|
+
const v = (value, fallback) => typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
140
|
+
return {
|
|
141
|
+
enabled: true,
|
|
142
|
+
swipeDistancePx: n(pageConfig.swipeDistancePx, DEFAULT_SWIPE_THRESHOLDS.swipeDistancePx),
|
|
143
|
+
dominanceRatio: n(pageConfig.dominanceRatio, DEFAULT_SWIPE_THRESHOLDS.dominanceRatio),
|
|
144
|
+
leftEdgeBandPx: n(pageConfig.leftEdgeBandPx, DEFAULT_SWIPE_THRESHOLDS.leftEdgeBandPx),
|
|
145
|
+
rightZoneRatio: n(pageConfig.rightZoneRatio, DEFAULT_SWIPE_THRESHOLDS.rightZoneRatio),
|
|
146
|
+
swipeVelocityPxPerMs: v(pageConfig.swipeVelocityPxPerMs, DEFAULT_SWIPE_THRESHOLDS.swipeVelocityPxPerMs),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* iOS focus auto-zoom suppression — pure decision core (host half, change
|
|
3
|
+
* `2026-09-03-ios-focus-zoom-suppression`).
|
|
4
|
+
*
|
|
5
|
+
* iOS Safari auto-zooms the page (~115–120%) whenever ANY focusable control
|
|
6
|
+
* with a computed font-size below 16px receives focus; DSH's input surfaces
|
|
7
|
+
* are all 13–14px, so every focus on an iPhone trips it. Solution B (user
|
|
8
|
+
* ruling, 2026-09-03): rewrite the viewport meta early — appending
|
|
9
|
+
* `maximum-scale=1, user-scalable=no` — which suppresses the focus zoom on
|
|
10
|
+
* iOS 10+ WITHOUT disabling pinch zoom (Discourse PR #30877 empirics).
|
|
11
|
+
*
|
|
12
|
+
* v0.2.5 refinement (change `2026-09-03-zoomguard-standalone-font-floor`):
|
|
13
|
+
* that empiric holds only IN-BROWSER. Standalone (Add to Home Screen) iOS
|
|
14
|
+
* HONORS `user-scalable=no` — the meta rewrite kills pinch there, and the
|
|
15
|
+
* user's primary usage IS the PWA (real-device datum, 2026-09-03). The guard
|
|
16
|
+
* therefore forks by display mode: browser keeps the meta rewrite untouched;
|
|
17
|
+
* standalone never touches the meta at all and instead injects a 16px
|
|
18
|
+
* font-floor stylesheet (the font-size mechanism kills the focus zoom while
|
|
19
|
+
* pinch stays fully engine-available).
|
|
20
|
+
*
|
|
21
|
+
* The five exported predicates here are the entire decision surface:
|
|
22
|
+
* everything else (state machine, resize re-evaluation, stock-meta
|
|
23
|
+
* reconciliation) lives in the boot script section {@link buildZoomGuardSection}
|
|
24
|
+
* emits. That section embeds these functions via `Function.prototype.toString`
|
|
25
|
+
* — the unit-tested source IS the shipped source, byte for byte — which
|
|
26
|
+
* constrains their shape:
|
|
27
|
+
*
|
|
28
|
+
* - **ES5 syntax only** (the inline head script targets ancient parsers):
|
|
29
|
+
* `var`, function declarations, string concatenation; no arrows, no
|
|
30
|
+
* let/const, no template literals, no destructuring.
|
|
31
|
+
* - **Fully self-contained bodies**: no module-scope identifiers, no helper
|
|
32
|
+
* calls, no shared constants. A bundler may rename the declaration, but a
|
|
33
|
+
* renamed free reference would be a `ReferenceError` on the page. The
|
|
34
|
+
* eval-style tests in `test/web-trust.spec.ts` execute the real generated
|
|
35
|
+
* script against a stub DOM, so any such leak fails the suite.
|
|
36
|
+
*
|
|
37
|
+
* @module omp-web/mobile/zoom-guard
|
|
38
|
+
*/
|
|
39
|
+
/** The two zoom tokens the guard merges into the stock viewport content. */
|
|
40
|
+
export const ZOOM_GUARD_TOKENS = {
|
|
41
|
+
'maximum-scale': '1',
|
|
42
|
+
'user-scalable': 'no',
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Whether a UA string + touch-capability pair is an iOS-class browser
|
|
46
|
+
* (design D2, mirroring Discourse `capabilities.isIOS`): iPhone/iPod/iPad,
|
|
47
|
+
* plus iPadOS 13+ desktop-Safari masquerade (`Macintosh` UA with a
|
|
48
|
+
* multi-touch digitizer). Deliberately NOT `pointer:coarse` — Android Chrome
|
|
49
|
+
* never focus-zooms and `maximum-scale` has historical side effects there,
|
|
50
|
+
* so the gate is tightened to the iOS family. UA-shape drift degrades
|
|
51
|
+
* benignly: a novel UA simply reads as non-iOS and no rewriting happens.
|
|
52
|
+
*
|
|
53
|
+
* @param ua - `navigator.userAgent`.
|
|
54
|
+
* @param maxTouchPoints - `navigator.maxTouchPoints` (0 when absent).
|
|
55
|
+
* @returns `true` for iOS-class browsers.
|
|
56
|
+
*/
|
|
57
|
+
export function isIOSClassUA(ua, maxTouchPoints) {
|
|
58
|
+
return /iPhone|iPod|iPad/.test(ua) || (/Macintosh/.test(ua) && maxTouchPoints > 1);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Token-level merge of viewport directives into a viewport meta `content`
|
|
62
|
+
* string (design D4): existing tokens keep their positions, a token whose key
|
|
63
|
+
* (compared case-insensitively) is in `tokens` is REPLACED by the configured
|
|
64
|
+
* key=value, and configured keys not present are APPENDED in insertion order.
|
|
65
|
+
* Idempotent by construction — merging always re-derives from the input, so
|
|
66
|
+
* repeated evaluation cannot accumulate duplicates.
|
|
67
|
+
*
|
|
68
|
+
* mergeViewportTokens('width=device-width, initial-scale=1', T)
|
|
69
|
+
* → 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no'
|
|
70
|
+
* mergeViewportTokens('width=device-width, maximum-scale=5', T)
|
|
71
|
+
* → 'width=device-width, maximum-scale=1, user-scalable=no' // same-key override, position kept
|
|
72
|
+
*
|
|
73
|
+
* @param content - the stock (or stock-equivalent) content string.
|
|
74
|
+
* @param tokens - the directives to merge, keyed lowercase.
|
|
75
|
+
* @returns the merged content string.
|
|
76
|
+
*/
|
|
77
|
+
export function mergeViewportTokens(content, tokens) {
|
|
78
|
+
var out = [];
|
|
79
|
+
var seen = {};
|
|
80
|
+
var parts = content.split(',');
|
|
81
|
+
for (var i = 0; i < parts.length; i++) {
|
|
82
|
+
var part = parts[i];
|
|
83
|
+
var raw = (part === undefined ? '' : part).replace(/^\s+|\s+$/g, '');
|
|
84
|
+
if (raw === '')
|
|
85
|
+
continue;
|
|
86
|
+
var eq = raw.indexOf('=');
|
|
87
|
+
var key = eq < 0 ? raw : raw.slice(0, eq).replace(/^\s+|\s+$/g, '');
|
|
88
|
+
var hit = null;
|
|
89
|
+
for (var k in tokens) {
|
|
90
|
+
if (tokens.hasOwnProperty(k) && k.toLowerCase() === key.toLowerCase()) {
|
|
91
|
+
hit = k;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (hit === null) {
|
|
96
|
+
out[out.length] = raw;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
out[out.length] = hit + '=' + tokens[hit];
|
|
100
|
+
seen[hit] = true;
|
|
101
|
+
}
|
|
102
|
+
for (var j in tokens) {
|
|
103
|
+
if (tokens.hasOwnProperty(j) && !seen[j])
|
|
104
|
+
out[out.length] = j + '=' + tokens[j];
|
|
105
|
+
}
|
|
106
|
+
return out.join(', ');
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The double gate (design D2/D3 + the `zoomGuard` config): rewrite applies
|
|
110
|
+
* only on an iOS-class browser AND a narrow viewport AND a non-`off`
|
|
111
|
+
* `zoomGuard`. An absent payload (or absent key) means the `'meta'` default —
|
|
112
|
+
* the same default-on posture as the mobile leg itself — so only an explicit
|
|
113
|
+
* `'off'` disarms the guard. Unknown non-`off` values read as `'meta'`
|
|
114
|
+
* (schema validation makes them unreachable in production; benign here).
|
|
115
|
+
*
|
|
116
|
+
* @param config - the `__OMP_WEB_MOBILE__` payload (only `zoomGuard` is read).
|
|
117
|
+
* @param isIOS - {@link isIOSClassUA} verdict for this page load.
|
|
118
|
+
* @param isNarrow - `matchMedia('(max-width: breakpoint-0.02px)')` verdict.
|
|
119
|
+
* @returns `true` when the viewport rewrite should be applied.
|
|
120
|
+
*/
|
|
121
|
+
export function shouldApplyZoomGuard(config, isIOS, isNarrow) {
|
|
122
|
+
if (!isIOS || !isNarrow)
|
|
123
|
+
return false;
|
|
124
|
+
if (!config)
|
|
125
|
+
return true;
|
|
126
|
+
return config.zoomGuard !== 'off';
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Standalone display-mode verdict (design D4 of change
|
|
130
|
+
* `2026-09-03-zoomguard-standalone-font-floor`): standalone PWAs HONOR
|
|
131
|
+
* `user-scalable=no` (the real-device pinch regression that motivated the
|
|
132
|
+
* display-mode split), so they must never enter the meta-rewrite path. Two
|
|
133
|
+
* sources, strict-`true` OR: the `(display-mode: standalone)` media query
|
|
134
|
+
* (primary) and legacy `navigator.standalone` (older iOS Safari exposes only
|
|
135
|
+
* the latter). Anything else — `undefined` (engine without the source),
|
|
136
|
+
* `false`, or truthy junk — reads as NOT standalone.
|
|
137
|
+
*
|
|
138
|
+
* @param mqMatchesStandalone - `matchMedia('(display-mode: standalone)').matches`.
|
|
139
|
+
* @param navStandalone - `navigator.standalone === true`.
|
|
140
|
+
* @returns `true` in standalone display mode (either source).
|
|
141
|
+
*/
|
|
142
|
+
export function isStandaloneDisplay(mqMatchesStandalone, navStandalone) {
|
|
143
|
+
return mqMatchesStandalone === true || navStandalone === true;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* The 16px font floor stylesheet (design D3): iOS auto-zooms on focus into
|
|
147
|
+
* ANY control whose computed font-size is below 16px, so in standalone mode
|
|
148
|
+
* — where the meta rewrite is off the table (pinch must stay
|
|
149
|
+
* engine-available) — the floor IS the suppression. Scoped by the same
|
|
150
|
+
* breakpoint band the meta leg uses and derived the same way
|
|
151
|
+
* (`breakpoint - 0.02`: 768 → 767.98, 900 → 899.98); `!important` beats the
|
|
152
|
+
* 13/14px CSS module rules and the body inline var chain;
|
|
153
|
+
* `[contenteditable="true"]` matches only the editable state (React
|
|
154
|
+
* serializes `contentEditable={bool}` to the literal attribute value
|
|
155
|
+
* "true"/"false").
|
|
156
|
+
*
|
|
157
|
+
* @param breakpointPx - the configured `mobile.breakpoint` (raw — this
|
|
158
|
+
* function derives the `-0.02` band itself, do NOT pre-subtract).
|
|
159
|
+
* @returns the complete stylesheet text.
|
|
160
|
+
*/
|
|
161
|
+
export function buildFontFloorCss(breakpointPx) {
|
|
162
|
+
var bp = breakpointPx - 0.02;
|
|
163
|
+
return '@media (max-width:' + bp + 'px){ input,textarea,select,[contenteditable="true"]{ font-size:16px !important } }';
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Build the boot script's zoomGuard section: the five decision predicates
|
|
167
|
+
* embedded via `toString()` plus the DOM state machine that applies them.
|
|
168
|
+
* The machine implements design D3/D4 with one discovered refinement —
|
|
169
|
+
*
|
|
170
|
+
* **The standalone fork (2026-09-03, change
|
|
171
|
+
* `2026-09-03-zoomguard-standalone-font-floor`).** Real-device datum:
|
|
172
|
+
* standalone (Add to Home Screen) iOS HONORS `user-scalable=no`, so the
|
|
173
|
+
* meta rewrite that is benign in-browser (pinch stays engine-controlled)
|
|
174
|
+
* kills pinch in the PWA — the user's primary surface. The section
|
|
175
|
+
* therefore forks right after the iOS gate, BEFORE any meta machinery: a
|
|
176
|
+
* one-shot startup verdict (display-mode never flips mid-session) via the
|
|
177
|
+
* `(display-mode: standalone)` MQ or legacy `navigator.standalone` (D4).
|
|
178
|
+
* Standalone → inject the `ios-zoom-font-floor` style element into the
|
|
179
|
+
* head (16px floor, media-query-scoped to the same breakpoint band, D3)
|
|
180
|
+
* and RETURN — no provisional meta, no reconcile observer, no ladder
|
|
181
|
+
* timer, no listeners, byte-identical stock meta (D1). The width dimension
|
|
182
|
+
* lives inside the stylesheet's media query, so the injection itself is
|
|
183
|
+
* NOT width-gated: a wide standalone load still gets the element; the rule
|
|
184
|
+
* simply does not match above the breakpoint. Browser mode runs the v0.2.4
|
|
185
|
+
* machinery unchanged.
|
|
186
|
+
* **Why reconciliation exists.** The webserver splices head injections
|
|
187
|
+
* immediately after the opening `<head>` tag, i.e. BEFORE the stock
|
|
188
|
+
* `<meta name="viewport">` in upstream's `index.html`. At boot-script time
|
|
189
|
+
* the stock meta is therefore NOT yet parsed, so the "meta missing"
|
|
190
|
+
* branch is the NORMAL path on the served shell, not an edge case. A
|
|
191
|
+
* provisional meta is created right away (the timing requirement: the
|
|
192
|
+
* suppressive tokens are live before any application bundle can focus an
|
|
193
|
+
* input), and a MutationObserver watches for the parser to insert the stock
|
|
194
|
+
* meta; the moment it appears, the guard records the true stock content,
|
|
195
|
+
* rewrites the STOCK meta (token merge, D4's actual semantics — restore
|
|
196
|
+
* later writes these exact bytes back), and removes the provisional one —
|
|
197
|
+
* leaving a single-meta document instead of relying on engines'
|
|
198
|
+
* unspecified multi-meta merge behavior. Engines without MutationObserver
|
|
199
|
+
* keep the provisional meta for the page's lifetime (degraded but still
|
|
200
|
+
* suppressive); the observer is never started once the stock meta was
|
|
201
|
+
* found synchronously.
|
|
202
|
+
*
|
|
203
|
+
* **Why the re-evaluation ladder exists (2026-09-03 rework, lead CDP
|
|
204
|
+
* finding on the 4999 instance).** The boot script runs during head parsing,
|
|
205
|
+
* BEFORE the engine applies the stock viewport meta — the layout viewport at
|
|
206
|
+
* first evaluation is the pre-meta engine default (980px observed), so
|
|
207
|
+
* `mq.matches` reads false on an initial narrow load, and the narrowing that
|
|
208
|
+
* follows meta application does NOT reliably dispatch `resize` to the page
|
|
209
|
+
* during load (headless Chrome: never). A ~10ms poll ladder therefore
|
|
210
|
+
* re-evaluates until the guard IS applied, `document.readyState` reaches
|
|
211
|
+
* `'complete'`, or ~2s elapse — the first layout always precedes deferred
|
|
212
|
+
* scripts, so the ladder lands the guard before any application bundle can
|
|
213
|
+
* focus an input. Steady-state ticks are idempotent no-ops; a wide load
|
|
214
|
+
* idles to the readyState stop and leaves no timer behind (a pending tick
|
|
215
|
+
* is also cancelled the moment the guard lands). MediaQuery `change` events
|
|
216
|
+
* (with the legacy `addListener` fallback) form a second re-evaluation
|
|
217
|
+
* channel alongside window `resize`.
|
|
218
|
+
*
|
|
219
|
+
* State machine (ES5, zero dependencies, synchronous first evaluation):
|
|
220
|
+
* `meta` is the element under management (provisional or reconciled stock),
|
|
221
|
+
* `stock` its recorded stock content, `mine` whether we created it,
|
|
222
|
+
* `applied` the last applied gate verdict. `apply(on)` is idempotent per
|
|
223
|
+
* state (no-op when the verdict is unchanged); merges always re-derive
|
|
224
|
+
* from the recorded stock, never from the live content, so resize storms
|
|
225
|
+
* cannot accumulate tokens. Non-iOS pages never wire any listener.
|
|
226
|
+
*
|
|
227
|
+
* Emitted only when the mobile payload exists and `zoomGuard !== 'off'`.
|
|
228
|
+
*
|
|
229
|
+
* @returns the inline section text (truthy `var` declarations + IIFE).
|
|
230
|
+
*/
|
|
231
|
+
export function buildZoomGuardSection() {
|
|
232
|
+
return [
|
|
233
|
+
`var ZI=${isIOSClassUA.toString()};`,
|
|
234
|
+
`var ZM=${mergeViewportTokens.toString()};`,
|
|
235
|
+
`var ZS=${shouldApplyZoomGuard.toString()};`,
|
|
236
|
+
`var ZD=${isStandaloneDisplay.toString()};`,
|
|
237
|
+
`var ZF=${buildFontFloorCss.toString()};`,
|
|
238
|
+
'(function(){',
|
|
239
|
+
'var m=window.__OMP_WEB_MOBILE__;',
|
|
240
|
+
'var ios=ZI(navigator.userAgent,(navigator.maxTouchPoints||0));',
|
|
241
|
+
'if(!ios)return;',
|
|
242
|
+
"var bpx=(m&&typeof m.breakpoint==='number'&&m.breakpoint>0)?m.breakpoint:768;",
|
|
243
|
+
'var bp=bpx-0.02;',
|
|
244
|
+
"var sa=ZD((window.matchMedia&&window.matchMedia('(display-mode: standalone)').matches),(navigator.standalone===true));",
|
|
245
|
+
"if(sa){var f=document.createElement('style');f.setAttribute('id','ios-zoom-font-floor');f.setAttribute('data-plugin','omp-web');f.setAttribute('data-plugin-css','omp-web/zoom-font-floor');f.textContent=ZF(bpx);headEl().appendChild(f);return}",
|
|
246
|
+
"var mq=(typeof window.matchMedia==='function')?window.matchMedia('(max-width:'+bp+'px)'):null;",
|
|
247
|
+
"var T={'maximum-scale':'1','user-scalable':'no'};",
|
|
248
|
+
'var meta=null,stock=null,mine=false,applied=false,obs=null,lad=null,lN=0;',
|
|
249
|
+
"function findStock(){var s=document.getElementsByTagName('meta');for(var i=0;i<s.length;i++){var e=s[i];if(e!==meta&&(e.getAttribute('name')||'').toLowerCase()==='viewport')return e}return null}",
|
|
250
|
+
"function headEl(){return document.head||document.getElementsByTagName('head')[0]||document.documentElement}",
|
|
251
|
+
'function stopWatch(){if(obs){obs.disconnect();obs=null}}',
|
|
252
|
+
'function stopLadder(){if(lad){clearTimeout(lad);lad=null}}',
|
|
253
|
+
"function dropMine(){if(meta&&mine){if(meta.parentNode)meta.parentNode.removeChild(meta);meta=null;mine=false}}",
|
|
254
|
+
'function apply(on){',
|
|
255
|
+
'if(on===applied)return;',
|
|
256
|
+
'applied=on;',
|
|
257
|
+
'if(on)stopLadder();',
|
|
258
|
+
'if(on){',
|
|
259
|
+
'var el=findStock();',
|
|
260
|
+
"if(el){meta=el;mine=false;stock=el.getAttribute('content')||''}",
|
|
261
|
+
"else if(!meta){meta=document.createElement('meta');meta.setAttribute('name','viewport');mine=true;stock='';headEl().appendChild(meta)}",
|
|
262
|
+
"meta.setAttribute('content',ZM(stock,T));",
|
|
263
|
+
"if(mine&&typeof MutationObserver==='function'&&!obs){obs=new MutationObserver(check);obs.observe(document.documentElement,{childList:true,subtree:true})}",
|
|
264
|
+
'}else{',
|
|
265
|
+
'stopWatch();',
|
|
266
|
+
'if(!meta)return;',
|
|
267
|
+
'if(mine)dropMine();',
|
|
268
|
+
"else meta.setAttribute('content',stock);", // restore = exact stock bytes (D4)
|
|
269
|
+
'}',
|
|
270
|
+
'}',
|
|
271
|
+
'function check(){',
|
|
272
|
+
'var el=findStock();',
|
|
273
|
+
'if(!el)return;',
|
|
274
|
+
'stopWatch();',
|
|
275
|
+
"stock=el.getAttribute('content')||'';",
|
|
276
|
+
"if(applied){dropMine();meta=el;mine=false;meta.setAttribute('content',ZM(stock,T))}",
|
|
277
|
+
'else{dropMine();meta=null;stock=null}',
|
|
278
|
+
'}',
|
|
279
|
+
'function re(){apply(ZS(m,true,mq?mq.matches:false))}',
|
|
280
|
+
'function tick(){',
|
|
281
|
+
'lad=null;',
|
|
282
|
+
're();',
|
|
283
|
+
'if(applied)return;',
|
|
284
|
+
'lN++;',
|
|
285
|
+
"if(document.readyState==='complete'||lN>=200)return;",
|
|
286
|
+
'lad=setTimeout(tick,10);',
|
|
287
|
+
'}',
|
|
288
|
+
're();',
|
|
289
|
+
"window.addEventListener('resize',re);",
|
|
290
|
+
"if(mq&&mq.addEventListener)mq.addEventListener('change',re);else if(mq&&mq.addListener)mq.addListener(re);",
|
|
291
|
+
"if(!applied&&typeof setTimeout==='function')lad=setTimeout(tick,10);",
|
|
292
|
+
'})()',
|
|
293
|
+
].join('');
|
|
294
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { buildZoomGuardSection } from './mobile/zoom-guard.js';
|
|
2
|
+
/**
|
|
3
|
+
* Build the boot script text. Pure: same constants in, same script out — the
|
|
4
|
+
* unit tests pin the shape (mobile global present, zoom-guard section present,
|
|
5
|
+
* wrapped in a non-throwing IIFE).
|
|
6
|
+
*
|
|
7
|
+
* @returns the script text.
|
|
8
|
+
*/
|
|
9
|
+
export function buildMobileBootScript() {
|
|
10
|
+
const parts = ['(function(){try{'];
|
|
11
|
+
// Ship default ON: a bare page carries the mobile global (the client's
|
|
12
|
+
// `resolveMobileConfig` merges the threshold defaults over `enabled: true`).
|
|
13
|
+
parts.push(`window.__OMP_WEB_MOBILE__=${JSON.stringify({ enabled: true })};`);
|
|
14
|
+
// The zoom guard rides the mobile leg (default 'meta' mode): emitted always.
|
|
15
|
+
parts.push(buildZoomGuardSection());
|
|
16
|
+
parts.push('}catch(e){}})();');
|
|
17
|
+
return parts.join('');
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Mount the boot-script injection: one `webserver/index-inject` listener
|
|
21
|
+
* pushing the rendered row. Conditional on the webServer service, so the
|
|
22
|
+
* plugin loads (with the feature dormant) in compositions without one.
|
|
23
|
+
* Fail-open: a push failure is logged, never thrown.
|
|
24
|
+
*
|
|
25
|
+
* @param ctx - host plugin context.
|
|
26
|
+
*/
|
|
27
|
+
export function installMobileBootScript(ctx) {
|
|
28
|
+
ctx.inject(['webServer'], () => {
|
|
29
|
+
ctx.on('webserver/index-inject', (table) => {
|
|
30
|
+
try {
|
|
31
|
+
table.push({ kind: 'script', placement: 'head', text: buildMobileBootScript() });
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
ctx.logger.warn(`omp-web: mobile boot script injection failed: ${String(error)}`);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|