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.
Files changed (69) hide show
  1. package/README.md +8 -8
  2. package/README.zh.md +8 -8
  3. package/docs/architecture/2.3.0-compat-retirement.md +147 -0
  4. package/docs/architecture/2.3.0-execution-plan.md +410 -0
  5. package/docs/architecture/2.3.0-final-audit.md +279 -0
  6. package/docs/architecture/2.3.0-reopened-plan.md +720 -0
  7. package/docs/architecture/compat-inventory.md +26 -34
  8. package/docs/architecture/dsh-020-rc2-adaptation.md +23 -0
  9. package/docs/architecture/dsh-compatibility-matrix.md +65 -54
  10. package/docs/architecture/dsh-support-window.md +29 -21
  11. package/docs/architecture/final-closure-audit.md +2 -0
  12. package/docs/architecture/host-first-proxy-convergence.md +1 -1
  13. package/docs/releases/v2.2.8.md +25 -0
  14. package/docs/releases/v2.3.0.md +53 -0
  15. package/entry.js +8 -56
  16. package/index.js +161 -691
  17. package/lib/catalog-corrections.js +1 -2
  18. package/lib/client-host-compat-prelude.js +7 -12
  19. package/lib/client-maintenance-actions.js +145 -0
  20. package/lib/client-presentation-boundary-main.js +82 -32
  21. package/lib/client-presentation-boundary.js +60 -40
  22. package/lib/client-script-carrier.js +90 -0
  23. package/lib/client.js +248 -146
  24. package/lib/core-apply-lifecycle.js +47 -0
  25. package/lib/core-primitives.js +88 -135
  26. package/lib/desktop-screenshot-tool.js +262 -0
  27. package/lib/dsh-contract-compat.js +11 -26
  28. package/lib/dsh-support-window.js +4 -4
  29. package/lib/guide-vision-toggle-highlight.js +7 -10
  30. package/lib/http-compat.js +1 -2
  31. package/lib/live-model-client-prelude.js +7 -14
  32. package/lib/local-remote-settings-permission.js +41 -16
  33. package/lib/maintenance-routes.js +136 -0
  34. package/lib/public-config.js +57 -0
  35. package/lib/public-entry.js +2 -7
  36. package/lib/remote-settings-bridge.js +11 -0
  37. package/lib/remote-settings-risk-client-boundary.js +155 -0
  38. package/lib/remote-settings-risk-confirmation.js +17 -105
  39. package/lib/runtime-composition.js +105 -30
  40. package/lib/runtime-i18n-boundary.js +10 -11
  41. package/lib/session-message-source-compat.js +7 -3
  42. package/lib/session-vision-index.js +154 -9
  43. package/lib/session-vision-mode-boundary.js +43 -8
  44. package/lib/settings-client-loader-lifecycle.js +129 -0
  45. package/lib/settings-factory-lifecycle.js +25 -14
  46. package/lib/settings-ia-client-prelude.js +11 -11
  47. package/lib/settings-limit-client-prelude.js +7 -9
  48. package/lib/sharp-runtime.js +0 -7
  49. package/lib/strict-live-model-client-prelude.js +7 -10
  50. package/lib/structured-flow-hardening.js +3 -2
  51. package/lib/v2-settings-ia-integration.js +21 -6
  52. package/lib/vision-backend-runtime-policy.js +3 -2
  53. package/lib/vision-background-benchmark.js +3 -5
  54. package/lib/vision-capability-benchmark-client.js +7 -10
  55. package/lib/vision-capability-benchmark-service.js +1 -0
  56. package/lib/vision-diagnostics-routes.js +216 -0
  57. package/lib/vision-evidence-guidance.js +3 -1
  58. package/lib/vision-exact-check-client.js +11 -6
  59. package/lib/vision-model-visibility-boundary-main.js +7 -11
  60. package/lib/vision-model-visibility-boundary.js +7 -12
  61. package/lib/vision-provider-transport.js +0 -29
  62. package/lib/vision-routing-settings-prelude.js +7 -11
  63. package/lib/vision-toggle-root-hardening.js +50 -8
  64. package/lib/vision-tool-runtime-boundary.js +5 -1
  65. package/lib/vision-turn-budget-client-prelude.js +7 -10
  66. package/lib/web/remote-settings-client.js +9 -14
  67. package/package.json +23 -13
  68. package/lib/android-attachment-compat.js +0 -244
  69. 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 = currentVisionProviderTransport()
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 { htmlHasScriptMarker } from './html-script-marker.js'
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
- if (typeof html !== 'string' || htmlHasScriptMarker(html, CLIENT_HOST_COMPAT_MARK)) return html
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?.inject?.(['webServer'], (webCtx) => {
256
- webCtx.effect(
257
- () => webCtx.webServer.tapIndex(injectClientHostCompatibility),
258
- 'vision-router: client Host compatibility boundary',
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
- var pair = resolveVisionModePair(state.groups, state.current, visionConfig);
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 = state.status === 'idle' || state.status === 'loading';
1098
+ var loading = settling;
1067
1099
  var removed = props.session && props.session.removed === true;
1068
- var disabled = props.available !== true || removed || busy || loading || pair.mode === 'unavailable';
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
- : pair.mode === 'unavailable'
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 && (pair.mode === 'unavailable' || loading) ? 0.45 : 1,
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(pair.target).then(function(accepted){
1130
- if (!accepted) announceRejectedSelection();
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
- React.createElement('span', { 'aria-hidden': 'true', style: { fontSize: 13, lineHeight: 1 } }, '👁'),
1135
- React.createElement('span', null, t('label')),
1136
- active
1137
- ? React.createElement('span', {
1138
- 'aria-hidden': 'true',
1139
- style: { fontSize: 12, lineHeight: 1, fontWeight: 800, marginLeft: 1 }
1140
- }, '✓')
1141
- : null
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 true; },
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
- if (typeof html !== 'string' || html.includes(CLIENT_PRESENTATION_MARK)) return html
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?.inject?.(['webServer'], (webCtx) => {
1321
- webCtx.effect(
1322
- () => webCtx.webServer.tapIndex(injectClientPresentationBoundary),
1323
- 'vision-router: client presentation boundary',
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 { htmlHasScriptMarker } from './html-script-marker.js'
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` React.createElement('span', { 'aria-hidden': 'true', style: { fontSize: 13, lineHeight: 1 } }, '👁'),
14
- React.createElement('span', null, t('label')),
15
- active
16
- ? React.createElement('span', {
17
- 'aria-hidden': 'true',
18
- style: { fontSize: 12, lineHeight: 1, fontWeight: 800, marginLeft: 1 }
19
- }, '✓')
20
- : null`
21
-
22
- const VISION_TOGGLE_FIXED_SVG = String.raw` React.createElement('svg', {
23
- width: 14,
24
- height: 14,
25
- viewBox: '0 0 14 14',
26
- fill: 'none',
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
- focusable: 'false',
29
- style: { display: 'block', flex: '0 0 auto' }
30
- }, React.createElement('path', {
31
- fillRule: 'evenodd',
32
- clipRule: 'evenodd',
33
- 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',
34
- fill: 'currentColor'
35
- })),
36
- React.createElement('span', null, t('label')),
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: 12,
40
- height: 12,
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', marginLeft: 1 }
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
- : null`
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
- if (typeof html !== 'string' || htmlHasScriptMarker(html, CLIENT_PRESENTATION_MARK)) return html
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?.inject?.(['webServer'], (webCtx) => {
200
- webCtx.effect(
201
- () => webCtx.webServer.tapIndex(injectClientPresentationBoundary),
202
- 'vision-router: client presentation boundary',
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
+ }