dsh-vision-router 2.2.7 → 2.3.0
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.md +8 -8
- package/README.zh.md +8 -8
- package/docs/architecture/2.3.0-compat-retirement.md +147 -0
- package/docs/architecture/2.3.0-execution-plan.md +410 -0
- package/docs/architecture/2.3.0-final-audit.md +279 -0
- package/docs/architecture/2.3.0-reopened-plan.md +720 -0
- package/docs/architecture/compat-inventory.md +26 -34
- package/docs/architecture/dsh-020-rc2-adaptation.md +23 -0
- package/docs/architecture/dsh-compatibility-matrix.md +65 -54
- package/docs/architecture/dsh-support-window.md +29 -21
- package/docs/architecture/final-closure-audit.md +2 -0
- package/docs/architecture/host-first-proxy-convergence.md +1 -1
- package/docs/releases/v2.2.8.md +25 -0
- package/docs/releases/v2.3.0.md +53 -0
- package/entry.js +8 -56
- package/index.js +161 -691
- package/lib/catalog-corrections.js +1 -2
- package/lib/client-host-compat-prelude.js +7 -12
- package/lib/client-maintenance-actions.js +145 -0
- package/lib/client-presentation-boundary-main.js +82 -32
- package/lib/client-presentation-boundary.js +60 -40
- package/lib/client-script-carrier.js +90 -0
- package/lib/client.js +248 -146
- package/lib/core-apply-lifecycle.js +47 -0
- package/lib/core-primitives.js +88 -135
- package/lib/desktop-screenshot-tool.js +262 -0
- package/lib/dsh-contract-compat.js +11 -26
- package/lib/dsh-support-window.js +4 -4
- package/lib/guide-vision-toggle-highlight.js +7 -10
- package/lib/http-compat.js +1 -2
- package/lib/live-model-client-prelude.js +7 -14
- package/lib/local-remote-settings-permission.js +41 -16
- package/lib/maintenance-routes.js +136 -0
- package/lib/public-config.js +57 -0
- package/lib/public-entry.js +2 -7
- package/lib/remote-settings-bridge.js +11 -0
- package/lib/remote-settings-risk-client-boundary.js +155 -0
- package/lib/remote-settings-risk-confirmation.js +17 -105
- package/lib/runtime-composition.js +105 -30
- package/lib/runtime-i18n-boundary.js +10 -11
- package/lib/session-message-source-compat.js +7 -3
- package/lib/session-vision-index.js +154 -9
- package/lib/session-vision-mode-boundary.js +43 -8
- package/lib/settings-client-loader-lifecycle.js +129 -0
- package/lib/settings-factory-lifecycle.js +25 -14
- package/lib/settings-ia-client-prelude.js +11 -11
- package/lib/settings-limit-client-prelude.js +7 -9
- package/lib/sharp-runtime.js +0 -7
- package/lib/strict-live-model-client-prelude.js +7 -10
- package/lib/structured-flow-hardening.js +3 -2
- package/lib/v2-settings-ia-integration.js +21 -6
- package/lib/vision-backend-runtime-policy.js +3 -2
- package/lib/vision-background-benchmark.js +3 -5
- package/lib/vision-capability-benchmark-client.js +7 -10
- package/lib/vision-capability-benchmark-service.js +1 -0
- package/lib/vision-diagnostics-routes.js +216 -0
- package/lib/vision-evidence-guidance.js +3 -1
- package/lib/vision-exact-check-client.js +11 -6
- package/lib/vision-model-visibility-boundary-main.js +7 -11
- package/lib/vision-model-visibility-boundary.js +7 -12
- package/lib/vision-provider-transport.js +0 -29
- package/lib/vision-routing-settings-prelude.js +7 -11
- package/lib/vision-toggle-root-hardening.js +50 -8
- package/lib/vision-tool-runtime-boundary.js +5 -1
- package/lib/vision-turn-budget-client-prelude.js +7 -10
- package/lib/web/remote-settings-client.js +9 -14
- package/package.json +23 -13
- package/lib/android-attachment-compat.js +0 -244
- package/lib/settings-client-rc8-lifecycle.js +0 -402
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
readResponseTextBounded,
|
|
16
16
|
} from './http-body-limit.js'
|
|
17
17
|
import { stripTrailingSlashes } from './string-normalization.js'
|
|
18
|
-
import { currentVisionProviderTransport } from './vision-provider-transport.js'
|
|
19
18
|
import { directSessionAffinityHeaders } from './session-affinity.js'
|
|
20
19
|
import { isOffloadedImageBlock, offloadedImagePlaceholder } from './image-offload-compat.js'
|
|
21
20
|
|
|
@@ -262,7 +261,7 @@ export async function callAnthropicCompatible(provider, messages, options = {})
|
|
|
262
261
|
const url = `${stripTrailingSlashes(provider.baseURL)}/v1/messages`
|
|
263
262
|
let response
|
|
264
263
|
try {
|
|
265
|
-
const transport =
|
|
264
|
+
const transport = options.providerTransport
|
|
266
265
|
const requestFetch = transport
|
|
267
266
|
? (input, init) => transport.fetch(input, init, { providerName: provider.name, allowProxy: true })
|
|
268
267
|
: fetch
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { injectClientCarrierHtml, installClientScriptCarriers } from './client-script-carrier.js'
|
|
2
2
|
|
|
3
3
|
const CLIENT_HOST_COMPAT_MARK = 'data-vision-router-host-compat'
|
|
4
4
|
|
|
@@ -243,19 +243,14 @@ export const CLIENT_HOST_COMPAT_PRELUDE = String.raw`(function(){
|
|
|
243
243
|
})();`
|
|
244
244
|
|
|
245
245
|
export function injectClientHostCompatibility(html) {
|
|
246
|
-
|
|
247
|
-
const safe = CLIENT_HOST_COMPAT_PRELUDE.replace(/<\/script/gi, '<\\/script')
|
|
248
|
-
const script = `<script ${CLIENT_HOST_COMPAT_MARK}>${safe}</script>`
|
|
249
|
-
const closeHead = html.indexOf('</head>')
|
|
250
|
-
if (closeHead !== -1) return `${html.slice(0, closeHead)}${script}${html.slice(closeHead)}`
|
|
251
|
-
return `${html}${script}`
|
|
246
|
+
return injectClientCarrierHtml(html, CLIENT_HOST_COMPAT_MARK, CLIENT_HOST_COMPAT_PRELUDE)
|
|
252
247
|
}
|
|
253
248
|
|
|
254
249
|
export function installClientHostCompatibility(ctx) {
|
|
255
|
-
ctx
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
250
|
+
installClientScriptCarriers(ctx, {
|
|
251
|
+
marker: CLIENT_HOST_COMPAT_MARK,
|
|
252
|
+
prelude: CLIENT_HOST_COMPAT_PRELUDE,
|
|
253
|
+
label: 'vision-router: client Host compatibility',
|
|
254
|
+
injectHtml: injectClientHostCompatibility,
|
|
260
255
|
})
|
|
261
256
|
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-side product maintenance actions, independent of React rendering.
|
|
3
|
+
*
|
|
4
|
+
* State access is supplied through getters/setters so the generated client
|
|
5
|
+
* bundle can keep one small action owner without giving it Settings, routing or
|
|
6
|
+
* Host-context authority.
|
|
7
|
+
*/
|
|
8
|
+
export function createClientMaintenanceActions({
|
|
9
|
+
fetchImpl,
|
|
10
|
+
t,
|
|
11
|
+
getTestState,
|
|
12
|
+
setTestState,
|
|
13
|
+
getUpdateState,
|
|
14
|
+
setUpdateState,
|
|
15
|
+
getSelfUpdateState,
|
|
16
|
+
setSelfUpdateState,
|
|
17
|
+
confirmImpl,
|
|
18
|
+
alertImpl,
|
|
19
|
+
} = {}) {
|
|
20
|
+
if (typeof fetchImpl !== 'function') throw new TypeError('client maintenance actions require fetch')
|
|
21
|
+
const translate = typeof t === 'function' ? t : (key) => String(key)
|
|
22
|
+
|
|
23
|
+
const diagnosticError = (value, fallback) => {
|
|
24
|
+
if (typeof value === 'string' && value.trim() !== '') return value.trim()
|
|
25
|
+
if (value && typeof value.message === 'string' && value.message.trim() !== '') {
|
|
26
|
+
return value.message.trim()
|
|
27
|
+
}
|
|
28
|
+
return fallback
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const runTestConnection = async () => {
|
|
32
|
+
if (getTestState?.()?.status === 'running') return
|
|
33
|
+
setTestState?.({ status: 'running' })
|
|
34
|
+
try {
|
|
35
|
+
const response = await fetchImpl('/_dsh/vision-router/test-connection')
|
|
36
|
+
const result = await response.json().catch(() => undefined)
|
|
37
|
+
setTestState?.({ status: 'done', result })
|
|
38
|
+
} catch (error) {
|
|
39
|
+
setTestState?.({
|
|
40
|
+
status: 'done',
|
|
41
|
+
result: { ok: false, error: error && error.message ? error.message : String(error) },
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const requestDesktopScreenshotPermission = () => {
|
|
47
|
+
// Fire-and-forget: the native permission dialog must never hold Settings
|
|
48
|
+
// save completion open. The actual screenshot tool still reports denial.
|
|
49
|
+
try {
|
|
50
|
+
void Promise.resolve(fetchImpl('/_dsh/vision-router/request-screenshot-permission', {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
cache: 'no-store',
|
|
53
|
+
})).catch(() => {})
|
|
54
|
+
} catch {
|
|
55
|
+
// Permission prompting is best-effort after an already-landed setting.
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const runUpdateCheck = async (force = false) => {
|
|
60
|
+
const current = getUpdateState?.() ?? {}
|
|
61
|
+
if (current.status === 'running') return
|
|
62
|
+
setUpdateState?.({ status: 'running', result: current.result })
|
|
63
|
+
try {
|
|
64
|
+
const response = await fetchImpl(
|
|
65
|
+
'/_dsh/vision-router/update-check' + (force ? '?force=1' : ''),
|
|
66
|
+
{ cache: 'no-store' },
|
|
67
|
+
)
|
|
68
|
+
const result = await response.json().catch(() => undefined)
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
throw new Error(diagnosticError(result && result.error, `HTTP ${response.status}`))
|
|
71
|
+
}
|
|
72
|
+
if (!result || typeof result !== 'object' || typeof result.ok !== 'boolean') {
|
|
73
|
+
throw new Error(translate('updateInvalidResponse'))
|
|
74
|
+
}
|
|
75
|
+
setUpdateState?.({ status: 'done', result })
|
|
76
|
+
} catch (error) {
|
|
77
|
+
setUpdateState?.({
|
|
78
|
+
status: 'done',
|
|
79
|
+
result: {
|
|
80
|
+
ok: false,
|
|
81
|
+
error: error && error.message ? error.message : String(error),
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const runSelfUpdate = async () => {
|
|
88
|
+
if (getSelfUpdateState?.()?.status === 'running') return
|
|
89
|
+
const checked = getUpdateState?.()?.result
|
|
90
|
+
const auto = checked && checked.autoUpdate
|
|
91
|
+
if (!checked || checked.ok !== true || checked.updateAvailable !== true || !auto || !auto.token) return
|
|
92
|
+
if (typeof confirmImpl === 'function' && confirmImpl(translate('updateConfirm')) !== true) return
|
|
93
|
+
setSelfUpdateState?.({ status: 'running', result: undefined })
|
|
94
|
+
try {
|
|
95
|
+
const response = await fetchImpl('/_dsh/vision-router/self-update', {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
cache: 'no-store',
|
|
98
|
+
headers: { 'x-dsh-vision-router-update-token': auto.token },
|
|
99
|
+
})
|
|
100
|
+
const result = await response.json().catch(() => undefined)
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
throw new Error(diagnosticError(result && result.error, `HTTP ${response.status}`))
|
|
103
|
+
}
|
|
104
|
+
if (!result || typeof result !== 'object' || typeof result.ok !== 'boolean') {
|
|
105
|
+
throw new Error(translate('updateInvalidResponse'))
|
|
106
|
+
}
|
|
107
|
+
setSelfUpdateState?.({ status: 'done', result })
|
|
108
|
+
} catch (error) {
|
|
109
|
+
setSelfUpdateState?.({
|
|
110
|
+
status: 'error',
|
|
111
|
+
result: { ok: false, error: error && error.message ? error.message : String(error) },
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const openLogFolder = async () => {
|
|
117
|
+
try {
|
|
118
|
+
const response = await fetchImpl('/_dsh/vision-router/logs', {
|
|
119
|
+
method: 'POST',
|
|
120
|
+
cache: 'no-store',
|
|
121
|
+
})
|
|
122
|
+
const result = await response.json().catch(() => undefined)
|
|
123
|
+
if (!response.ok || !result || result.ok !== true) {
|
|
124
|
+
throw new Error(result && result.error ? result.error : `HTTP ${response.status}`)
|
|
125
|
+
}
|
|
126
|
+
return result
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (typeof alertImpl === 'function') {
|
|
129
|
+
alertImpl(
|
|
130
|
+
translate('openLogFolderFailed') + ':' +
|
|
131
|
+
(error && error.message ? error.message : String(error)),
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
return undefined
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
runTestConnection,
|
|
140
|
+
requestDesktopScreenshotPermission,
|
|
141
|
+
runUpdateCheck,
|
|
142
|
+
runSelfUpdate,
|
|
143
|
+
openLogFolder,
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { injectClientCarrierHtml, installClientScriptCarriers } from './client-script-carrier.js'
|
|
1
2
|
const CLIENT_PRESENTATION_MARK = 'data-vision-router-presentation-boundary'
|
|
2
3
|
const VISION_MODE_WRAPPER_SOURCE = 'deepseek-official'
|
|
3
4
|
|
|
@@ -1057,21 +1058,52 @@ export const CLIENT_PRESENTATION_PRELUDE = String.raw`(function(){
|
|
|
1057
1058
|
var visionConfig = settingsState && settingsState.value && typeof settingsState.value === 'object'
|
|
1058
1059
|
? settingsState.value
|
|
1059
1060
|
: {};
|
|
1060
|
-
|
|
1061
|
+
// A Vision click may briefly drive this same Host directory through
|
|
1062
|
+
// selecting -> idle/loading while its projection is recomputed. Smooth
|
|
1063
|
+
// only that owned transaction: unrelated Host reloads (connection,
|
|
1064
|
+
// adapters, settings, credentials) must keep their loading authority.
|
|
1065
|
+
var usable = !!(state.current &&
|
|
1066
|
+
typeof state.current.provider === 'string' && state.current.provider !== '') &&
|
|
1067
|
+
Array.isArray(state.groups) && state.groups.length > 0;
|
|
1068
|
+
var settling = state.status === 'idle' || state.status === 'loading';
|
|
1069
|
+
var livePair = resolveVisionModePair(state.groups, state.current, visionConfig);
|
|
1070
|
+
var complete = usable && livePair.mode !== 'unavailable';
|
|
1071
|
+
var settledMode = React.useRef(null);
|
|
1072
|
+
var toggleTransition = React.useRef(null);
|
|
1073
|
+
if (settledMode.current && settledMode.current.directory !== directory) settledMode.current = null;
|
|
1074
|
+
if (toggleTransition.current && toggleTransition.current.directory !== directory) toggleTransition.current = null;
|
|
1075
|
+
if (complete) settledMode.current = { directory: directory, mode: livePair.mode };
|
|
1076
|
+
var transition = toggleTransition.current;
|
|
1077
|
+
if (transition && state.status === 'selecting') {
|
|
1078
|
+
var pending = state.pending;
|
|
1079
|
+
var matchesPending = !!(pending &&
|
|
1080
|
+
pending.provider === transition.target.provider &&
|
|
1081
|
+
pending.model === transition.target.model &&
|
|
1082
|
+
pending.reasoningEffort === transition.target.reasoningEffort);
|
|
1083
|
+
if (matchesPending) transition.seenSelecting = true;
|
|
1084
|
+
else toggleTransition.current = null;
|
|
1085
|
+
}
|
|
1086
|
+
transition = toggleTransition.current;
|
|
1087
|
+
var ownSettling = !!(transition && transition.seenSelecting && settling && settledMode.current);
|
|
1088
|
+
if (!ownSettling && transition && state.status !== 'selecting' &&
|
|
1089
|
+
(state.status === 'error' || state.status === 'ready')) {
|
|
1090
|
+
toggleTransition.current = null;
|
|
1091
|
+
}
|
|
1092
|
+
var pair = ownSettling ? { mode: settledMode.current.mode } : livePair;
|
|
1061
1093
|
var active = pair.mode === 'on';
|
|
1062
1094
|
var toastState = React.useState(null);
|
|
1063
1095
|
var toast = toastState[0];
|
|
1064
1096
|
var setToast = toastState[1];
|
|
1065
1097
|
var busy = state.status === 'selecting';
|
|
1066
|
-
var loading =
|
|
1098
|
+
var loading = settling;
|
|
1067
1099
|
var removed = props.session && props.session.removed === true;
|
|
1068
|
-
var disabled = props.available !== true || removed || busy || loading ||
|
|
1100
|
+
var disabled = props.available !== true || removed || busy || loading || livePair.mode === 'unavailable';
|
|
1069
1101
|
var t = typeof props.t === 'function' ? props.t : fallbackTranslate(active);
|
|
1070
|
-
var title = busy
|
|
1102
|
+
var title = busy || ownSettling
|
|
1071
1103
|
? t('switching')
|
|
1072
1104
|
: loading
|
|
1073
1105
|
? t('loading')
|
|
1074
|
-
:
|
|
1106
|
+
: livePair.mode === 'unavailable'
|
|
1075
1107
|
? t('unavailable')
|
|
1076
1108
|
: active ? t('disable') : t('enable');
|
|
1077
1109
|
var style = {
|
|
@@ -1093,7 +1125,7 @@ export const CLIENT_PRESENTATION_PRELUDE = String.raw`(function(){
|
|
|
1093
1125
|
lineHeight: 1.4,
|
|
1094
1126
|
fontWeight: active ? 650 : 500,
|
|
1095
1127
|
cursor: disabled ? 'default' : 'pointer',
|
|
1096
|
-
opacity: disabled && (
|
|
1128
|
+
opacity: disabled && !ownSettling && (livePair.mode === 'unavailable' || loading) ? 0.45 : 1,
|
|
1097
1129
|
whiteSpace: 'nowrap'
|
|
1098
1130
|
};
|
|
1099
1131
|
|
|
@@ -1120,25 +1152,52 @@ export const CLIENT_PRESENTATION_PRELUDE = String.raw`(function(){
|
|
|
1120
1152
|
'data-vision-router-mode-toggle': 'true',
|
|
1121
1153
|
'aria-pressed': active,
|
|
1122
1154
|
'aria-label': title,
|
|
1155
|
+
'aria-busy': busy || loading,
|
|
1123
1156
|
title: title,
|
|
1124
1157
|
disabled: disabled,
|
|
1125
1158
|
style: style,
|
|
1126
1159
|
onClick: function() {
|
|
1127
1160
|
if (disabled || !pair.target || typeof props.select !== 'function') return;
|
|
1161
|
+
var target = pair.target;
|
|
1162
|
+
toggleTransition.current = {
|
|
1163
|
+
directory: directory,
|
|
1164
|
+
target: target,
|
|
1165
|
+
seenSelecting: false
|
|
1166
|
+
};
|
|
1128
1167
|
setToast(null);
|
|
1129
|
-
void props.select(
|
|
1130
|
-
if (!accepted)
|
|
1168
|
+
void props.select(target).then(function(accepted){
|
|
1169
|
+
if (!accepted) {
|
|
1170
|
+
toggleTransition.current = null;
|
|
1171
|
+
announceRejectedSelection();
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
var latest;
|
|
1175
|
+
try { latest = store && typeof store.getSnapshot === 'function' ? store.getSnapshot() : undefined; } catch (_) {}
|
|
1176
|
+
if (!latest || (latest.status !== 'selecting' && latest.status !== 'idle' && latest.status !== 'loading')) {
|
|
1177
|
+
toggleTransition.current = null;
|
|
1178
|
+
}
|
|
1131
1179
|
});
|
|
1132
1180
|
}
|
|
1133
1181
|
},
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
active
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
:
|
|
1182
|
+
// One fixed 14px leading slot carries the state icon: the eye when the
|
|
1183
|
+
// route is ordinary, the check when Vision is on. Appending the check
|
|
1184
|
+
// only while active changed the chip width by ~18px and reflowed the
|
|
1185
|
+
// composer row; reserving trailing space kept the width but left the
|
|
1186
|
+
// inactive chip visually off-centre.
|
|
1187
|
+
React.createElement('span', {
|
|
1188
|
+
'aria-hidden': 'true',
|
|
1189
|
+
style: {
|
|
1190
|
+
display: 'inline-flex',
|
|
1191
|
+
alignItems: 'center',
|
|
1192
|
+
justifyContent: 'center',
|
|
1193
|
+
flex: '0 0 auto',
|
|
1194
|
+
width: 14,
|
|
1195
|
+
height: 14,
|
|
1196
|
+
fontSize: 13,
|
|
1197
|
+
lineHeight: 1
|
|
1198
|
+
}
|
|
1199
|
+
}, active ? '✓' : '👁'),
|
|
1200
|
+
React.createElement('span', null, t('label'))
|
|
1142
1201
|
);
|
|
1143
1202
|
|
|
1144
1203
|
var toastNode = toast
|
|
@@ -1183,7 +1242,7 @@ export const CLIENT_PRESENTATION_PRELUDE = String.raw`(function(){
|
|
|
1183
1242
|
}
|
|
1184
1243
|
try {
|
|
1185
1244
|
return Promise.resolve(directory.select(selection)).then(
|
|
1186
|
-
function(){ return
|
|
1245
|
+
function(result){ return !result || result.ok !== false; },
|
|
1187
1246
|
function(){ return false; }
|
|
1188
1247
|
);
|
|
1189
1248
|
} catch (_) {
|
|
@@ -1304,23 +1363,14 @@ export const CLIENT_PRESENTATION_PRELUDE = String.raw`(function(){
|
|
|
1304
1363
|
})();`
|
|
1305
1364
|
|
|
1306
1365
|
export function injectClientPresentationBoundary(html) {
|
|
1307
|
-
|
|
1308
|
-
const safe = CLIENT_PRESENTATION_PRELUDE.replace(/<\/script/gi, '<\\/script')
|
|
1309
|
-
const script = `<script ${CLIENT_PRESENTATION_MARK}>${safe}</script>`
|
|
1310
|
-
// DSH's client-modules index tap prepends the queue-mode loader bootstrap at
|
|
1311
|
-
// <head>. Run after that parser bootstrap so we can wrap both load() and the
|
|
1312
|
-
// later create() transition. The create wrapper re-applies the boundary when
|
|
1313
|
-
// rc.8 replaces loader.load while entering live mode.
|
|
1314
|
-
const closeHead = html.indexOf('</head>')
|
|
1315
|
-
if (closeHead !== -1) return `${html.slice(0, closeHead)}${script}${html.slice(closeHead)}`
|
|
1316
|
-
return `${html}${script}`
|
|
1366
|
+
return injectClientCarrierHtml(html, CLIENT_PRESENTATION_MARK, CLIENT_PRESENTATION_PRELUDE)
|
|
1317
1367
|
}
|
|
1318
1368
|
|
|
1319
1369
|
export function installClientPresentationBoundary(ctx) {
|
|
1320
|
-
ctx
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1370
|
+
installClientScriptCarriers(ctx, {
|
|
1371
|
+
marker: CLIENT_PRESENTATION_MARK,
|
|
1372
|
+
prelude: CLIENT_PRESENTATION_PRELUDE,
|
|
1373
|
+
label: 'vision-router: client presentation boundary',
|
|
1374
|
+
injectHtml: injectClientPresentationBoundary,
|
|
1325
1375
|
})
|
|
1326
1376
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { injectClientCarrierHtml, installClientScriptCarriers } from './client-script-carrier.js'
|
|
2
2
|
import {
|
|
3
3
|
CLIENT_PRESENTATION_PRELUDE as BASE_CLIENT_PRESENTATION_PRELUDE,
|
|
4
4
|
resolveVisionModePair,
|
|
@@ -10,39 +10,50 @@ import { installVisionModelVisibilityBoundary } from './vision-model-visibility-
|
|
|
10
10
|
|
|
11
11
|
const CLIENT_PRESENTATION_MARK = 'data-vision-router-presentation-boundary'
|
|
12
12
|
|
|
13
|
-
const VISION_TOGGLE_TEXT_GLYPH = String.raw`
|
|
14
|
-
|
|
15
|
-
active
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
:
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
13
|
+
const VISION_TOGGLE_TEXT_GLYPH = String.raw` // One fixed 14px leading slot carries the state icon: the eye when the
|
|
14
|
+
// route is ordinary, the check when Vision is on. Appending the check
|
|
15
|
+
// only while active changed the chip width by ~18px and reflowed the
|
|
16
|
+
// composer row; reserving trailing space kept the width but left the
|
|
17
|
+
// inactive chip visually off-centre.
|
|
18
|
+
React.createElement('span', {
|
|
19
|
+
'aria-hidden': 'true',
|
|
20
|
+
style: {
|
|
21
|
+
display: 'inline-flex',
|
|
22
|
+
alignItems: 'center',
|
|
23
|
+
justifyContent: 'center',
|
|
24
|
+
flex: '0 0 auto',
|
|
25
|
+
width: 14,
|
|
26
|
+
height: 14,
|
|
27
|
+
fontSize: 13,
|
|
28
|
+
lineHeight: 1
|
|
29
|
+
}
|
|
30
|
+
}, active ? '✓' : '👁'),
|
|
31
|
+
React.createElement('span', null, t('label'))`
|
|
32
|
+
|
|
33
|
+
const VISION_TOGGLE_FIXED_SVG = String.raw` // One fixed 14px leading slot carries the state icon: the eye when the
|
|
34
|
+
// route is ordinary, the check when Vision is on. Appending the check
|
|
35
|
+
// only while active changed the chip width by ~18px and reflowed the
|
|
36
|
+
// composer row; reserving trailing space kept the width but left the
|
|
37
|
+
// inactive chip visually off-centre.
|
|
38
|
+
React.createElement('span', {
|
|
27
39
|
'aria-hidden': 'true',
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
active
|
|
40
|
+
style: {
|
|
41
|
+
display: 'inline-flex',
|
|
42
|
+
alignItems: 'center',
|
|
43
|
+
justifyContent: 'center',
|
|
44
|
+
flex: '0 0 auto',
|
|
45
|
+
width: 14,
|
|
46
|
+
height: 14
|
|
47
|
+
}
|
|
48
|
+
}, active
|
|
38
49
|
? React.createElement('svg', {
|
|
39
|
-
width:
|
|
40
|
-
height:
|
|
50
|
+
width: 14,
|
|
51
|
+
height: 14,
|
|
41
52
|
viewBox: '0 0 14 14',
|
|
42
53
|
fill: 'none',
|
|
43
54
|
'aria-hidden': 'true',
|
|
44
55
|
focusable: 'false',
|
|
45
|
-
style: { display: 'block', flex: '0 0 auto'
|
|
56
|
+
style: { display: 'block', flex: '0 0 auto' }
|
|
46
57
|
}, React.createElement('path', {
|
|
47
58
|
d: 'M2.75 7.15 5.6 10 11.25 4.35',
|
|
48
59
|
stroke: 'currentColor',
|
|
@@ -50,7 +61,21 @@ const VISION_TOGGLE_FIXED_SVG = String.raw` React.createElement('svg',
|
|
|
50
61
|
strokeLinecap: 'round',
|
|
51
62
|
strokeLinejoin: 'round'
|
|
52
63
|
}))
|
|
53
|
-
:
|
|
64
|
+
: React.createElement('svg', {
|
|
65
|
+
width: 14,
|
|
66
|
+
height: 14,
|
|
67
|
+
viewBox: '0 0 14 14',
|
|
68
|
+
fill: 'none',
|
|
69
|
+
'aria-hidden': 'true',
|
|
70
|
+
focusable: 'false',
|
|
71
|
+
style: { display: 'block', flex: '0 0 auto' }
|
|
72
|
+
}, React.createElement('path', {
|
|
73
|
+
fillRule: 'evenodd',
|
|
74
|
+
clipRule: 'evenodd',
|
|
75
|
+
d: 'M7 2.25c-2.84 0-5.04 1.69-6.25 4.25a1.15 1.15 0 0 0 0 1C1.96 10.06 4.16 11.75 7 11.75s5.04-1.69 6.25-4.25a1.15 1.15 0 0 0 0-1C12.04 3.94 9.84 2.25 7 2.25Zm0 1.25c2.16 0 3.96 1.21 5.05 3.5C10.96 9.29 9.16 10.5 7 10.5S3.04 9.29 1.95 7C3.04 4.71 4.84 3.5 7 3.5Zm0 1.25A2.25 2.25 0 1 0 7 9.25a2.25 2.25 0 0 0 0-4.5Zm0 1.25a1 1 0 1 1 0 2 1 1 0 0 1 0-2Z',
|
|
76
|
+
fill: 'currentColor'
|
|
77
|
+
}))),
|
|
78
|
+
React.createElement('span', null, t('label'))`
|
|
54
79
|
|
|
55
80
|
const BITMAP_LIMITS_BASE = String.raw` var maxBitmapSourceBytes = 64 * 1024 * 1024;
|
|
56
81
|
var maxBitmapDimension = 10000;
|
|
@@ -187,20 +212,15 @@ export {
|
|
|
187
212
|
}
|
|
188
213
|
|
|
189
214
|
export function injectClientPresentationBoundary(html) {
|
|
190
|
-
|
|
191
|
-
const safe = CLIENT_PRESENTATION_PRELUDE.replace(/<\/script/gi, '<\\/script')
|
|
192
|
-
const script = `<script ${CLIENT_PRESENTATION_MARK}>${safe}</script>`
|
|
193
|
-
const closeHead = html.indexOf('</head>')
|
|
194
|
-
if (closeHead !== -1) return `${html.slice(0, closeHead)}${script}${html.slice(closeHead)}`
|
|
195
|
-
return `${html}${script}`
|
|
215
|
+
return injectClientCarrierHtml(html, CLIENT_PRESENTATION_MARK, CLIENT_PRESENTATION_PRELUDE)
|
|
196
216
|
}
|
|
197
217
|
|
|
198
218
|
export function installClientPresentationBoundary(ctx) {
|
|
199
|
-
ctx
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
219
|
+
installClientScriptCarriers(ctx, {
|
|
220
|
+
marker: CLIENT_PRESENTATION_MARK,
|
|
221
|
+
prelude: CLIENT_PRESENTATION_PRELUDE,
|
|
222
|
+
label: 'vision-router: client presentation boundary',
|
|
223
|
+
injectHtml: injectClientPresentationBoundary,
|
|
204
224
|
})
|
|
205
225
|
// Keep Host-version compatibility in its own capability-detected prelude.
|
|
206
226
|
// This lets the large legacy client factory stay byte-stable while alpha/new
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { htmlHasScriptMarker } from './html-script-marker.js'
|
|
2
|
+
|
|
3
|
+
const CARRIER_REGISTRY = '__dshVisionRouterClientCarriers'
|
|
4
|
+
|
|
5
|
+
function carrierKey(marker) {
|
|
6
|
+
const value = String(marker || 'vision-router-client-script')
|
|
7
|
+
return `vr:${value.replace(/^data-vision-router-/, '')}`
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Wrap one browser prelude with a shared one-shot fence. Modern served-Web
|
|
12
|
+
* pages can receive both structured index rows and tapIndex HTML transforms,
|
|
13
|
+
* while Desktop/static carriers receive only the structured row. The fence
|
|
14
|
+
* keeps both delivery paths safe without relying on each prelude to implement
|
|
15
|
+
* its own duplicate-execution semantics.
|
|
16
|
+
*/
|
|
17
|
+
export function clientCarrierSource(marker, prelude) {
|
|
18
|
+
const key = JSON.stringify(carrierKey(marker))
|
|
19
|
+
return `(function(){\n var root = window;\n var registry = root.${CARRIER_REGISTRY};\n if (!registry || typeof registry !== 'object') {\n registry = Object.create(null);\n try { Object.defineProperty(root, '${CARRIER_REGISTRY}', { value: registry, configurable: true }); }\n catch (_) { root.${CARRIER_REGISTRY} = registry; }\n }\n var key = ${key};\n if (registry[key]) return;\n registry[key] = true;\n try {\n${prelude}\n } catch (error) {\n try { delete registry[key]; } catch (_) { registry[key] = false; }\n throw error;\n }\n})();`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function injectClientCarrierHtml(html, marker, prelude) {
|
|
23
|
+
const structuredMark = `/* ${marker} */`
|
|
24
|
+
if (typeof html !== 'string' || htmlHasScriptMarker(html, marker) || html.includes(structuredMark)) return html
|
|
25
|
+
const safe = clientCarrierSource(marker, prelude).replace(/<\/script/gi, '<\\/script')
|
|
26
|
+
const script = `<script ${marker}>${safe}</script>`
|
|
27
|
+
const closeHead = html.indexOf('</head>')
|
|
28
|
+
return closeHead === -1 ? `${html}${script}` : `${html.slice(0, closeHead)}${script}${html.slice(closeHead)}`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
export function injectClientStyleHtml(html, marker, css) {
|
|
33
|
+
if (typeof html !== 'string' || typeof marker !== 'string' || marker === '') return html
|
|
34
|
+
if (html.includes(`<style ${marker}>`)) return html
|
|
35
|
+
const style = `<style ${marker}>${String(css || '')}</style>`
|
|
36
|
+
const closeHead = html.indexOf('</head>')
|
|
37
|
+
return closeHead === -1 ? `${html}${style}` : `${html.slice(0, closeHead)}${style}${html.slice(closeHead)}`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function appendStructuredClientStyle(table, marker, css) {
|
|
41
|
+
if (!Array.isArray(table) || typeof marker !== 'string' || marker === '') return false
|
|
42
|
+
const signature = `/* ${marker}:style */`
|
|
43
|
+
if (table.some((row) => row?.kind === 'style' && typeof row.text === 'string' && row.text.includes(signature))) return false
|
|
44
|
+
table.push({ kind: 'style', placement: 'head', text: `${signature}
|
|
45
|
+
${String(css || '')}` })
|
|
46
|
+
return true
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function appendStructuredClientCarrier(table, marker, prelude) {
|
|
50
|
+
if (!Array.isArray(table)) return false
|
|
51
|
+
const signature = `/* ${marker} */`
|
|
52
|
+
if (table.some((row) => row?.kind === 'script' && typeof row.text === 'string' && row.text.includes(signature))) {
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
table.push({
|
|
56
|
+
kind: 'script',
|
|
57
|
+
placement: 'head',
|
|
58
|
+
text: `${signature}\n${clientCarrierSource(marker, prelude)}`,
|
|
59
|
+
})
|
|
60
|
+
return true
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Register the same client prelude on both supported browser carriers:
|
|
65
|
+
* - structured `webserver/index-inject` rows for Desktop/static shells;
|
|
66
|
+
* - `tapIndex()` HTML transforms for served-Web and older Hosts.
|
|
67
|
+
*/
|
|
68
|
+
export function installClientScriptCarriers(ctx, { marker, prelude, label, injectHtml, style }) {
|
|
69
|
+
if (!ctx || typeof ctx.inject !== 'function') return
|
|
70
|
+
ctx.inject(['webServer'], (webCtx) => {
|
|
71
|
+
if (typeof webCtx.on === 'function') {
|
|
72
|
+
webCtx.on('webserver/index-inject', (table) => {
|
|
73
|
+
if (style) appendStructuredClientStyle(table, style.marker || marker, style.text)
|
|
74
|
+
appendStructuredClientCarrier(table, marker, prelude)
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
if (typeof webCtx.effect === 'function' && webCtx.webServer?.tapIndex) {
|
|
78
|
+
const baseTransform = typeof injectHtml === 'function'
|
|
79
|
+
? injectHtml
|
|
80
|
+
: (html) => injectClientCarrierHtml(html, marker, prelude)
|
|
81
|
+
const transform = style
|
|
82
|
+
? (html) => baseTransform(injectClientStyleHtml(html, style.marker || marker, style.text))
|
|
83
|
+
: baseTransform
|
|
84
|
+
webCtx.effect(
|
|
85
|
+
() => webCtx.webServer.tapIndex(transform),
|
|
86
|
+
label,
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
}
|