dsh-vision-router 2.2.8 → 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 (49) hide show
  1. package/README.md +7 -7
  2. package/README.zh.md +7 -7
  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-compatibility-matrix.md +65 -54
  9. package/docs/architecture/dsh-support-window.md +27 -21
  10. package/docs/architecture/final-closure-audit.md +2 -0
  11. package/docs/releases/v2.3.0.md +53 -0
  12. package/entry.js +8 -56
  13. package/index.js +161 -691
  14. package/lib/catalog-corrections.js +1 -2
  15. package/lib/client-maintenance-actions.js +145 -0
  16. package/lib/client-presentation-boundary-main.js +75 -17
  17. package/lib/client-presentation-boundary.js +53 -28
  18. package/lib/client.js +246 -144
  19. package/lib/core-apply-lifecycle.js +47 -0
  20. package/lib/core-primitives.js +88 -135
  21. package/lib/desktop-screenshot-tool.js +262 -0
  22. package/lib/dsh-contract-compat.js +11 -26
  23. package/lib/dsh-support-window.js +3 -3
  24. package/lib/http-compat.js +1 -2
  25. package/lib/local-remote-settings-permission.js +33 -7
  26. package/lib/maintenance-routes.js +136 -0
  27. package/lib/public-config.js +57 -0
  28. package/lib/public-entry.js +2 -7
  29. package/lib/remote-settings-bridge.js +11 -0
  30. package/lib/remote-settings-risk-client-boundary.js +155 -0
  31. package/lib/remote-settings-risk-confirmation.js +17 -105
  32. package/lib/runtime-composition.js +105 -30
  33. package/lib/runtime-i18n-boundary.js +10 -11
  34. package/lib/session-message-source-compat.js +7 -3
  35. package/lib/session-vision-index.js +154 -9
  36. package/lib/settings-client-loader-lifecycle.js +129 -0
  37. package/lib/sharp-runtime.js +0 -7
  38. package/lib/structured-flow-hardening.js +3 -2
  39. package/lib/vision-backend-runtime-policy.js +3 -2
  40. package/lib/vision-capability-benchmark-service.js +1 -0
  41. package/lib/vision-diagnostics-routes.js +216 -0
  42. package/lib/vision-evidence-guidance.js +3 -1
  43. package/lib/vision-provider-transport.js +0 -29
  44. package/lib/vision-toggle-root-hardening.js +22 -5
  45. package/lib/vision-tool-runtime-boundary.js +5 -1
  46. package/lib/web/remote-settings-client.js +2 -2
  47. package/package.json +23 -13
  48. package/lib/android-attachment-compat.js +0 -244
  49. package/lib/settings-client-rc8-lifecycle.js +0 -397
@@ -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
@@ -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
+ }
@@ -1058,21 +1058,52 @@ export const CLIENT_PRESENTATION_PRELUDE = String.raw`(function(){
1058
1058
  var visionConfig = settingsState && settingsState.value && typeof settingsState.value === 'object'
1059
1059
  ? settingsState.value
1060
1060
  : {};
1061
- 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;
1062
1093
  var active = pair.mode === 'on';
1063
1094
  var toastState = React.useState(null);
1064
1095
  var toast = toastState[0];
1065
1096
  var setToast = toastState[1];
1066
1097
  var busy = state.status === 'selecting';
1067
- var loading = state.status === 'idle' || state.status === 'loading';
1098
+ var loading = settling;
1068
1099
  var removed = props.session && props.session.removed === true;
1069
- var disabled = props.available !== true || removed || busy || loading || pair.mode === 'unavailable';
1100
+ var disabled = props.available !== true || removed || busy || loading || livePair.mode === 'unavailable';
1070
1101
  var t = typeof props.t === 'function' ? props.t : fallbackTranslate(active);
1071
- var title = busy
1102
+ var title = busy || ownSettling
1072
1103
  ? t('switching')
1073
1104
  : loading
1074
1105
  ? t('loading')
1075
- : pair.mode === 'unavailable'
1106
+ : livePair.mode === 'unavailable'
1076
1107
  ? t('unavailable')
1077
1108
  : active ? t('disable') : t('enable');
1078
1109
  var style = {
@@ -1094,7 +1125,7 @@ export const CLIENT_PRESENTATION_PRELUDE = String.raw`(function(){
1094
1125
  lineHeight: 1.4,
1095
1126
  fontWeight: active ? 650 : 500,
1096
1127
  cursor: disabled ? 'default' : 'pointer',
1097
- opacity: disabled && (pair.mode === 'unavailable' || loading) ? 0.45 : 1,
1128
+ opacity: disabled && !ownSettling && (livePair.mode === 'unavailable' || loading) ? 0.45 : 1,
1098
1129
  whiteSpace: 'nowrap'
1099
1130
  };
1100
1131
 
@@ -1121,25 +1152,52 @@ export const CLIENT_PRESENTATION_PRELUDE = String.raw`(function(){
1121
1152
  'data-vision-router-mode-toggle': 'true',
1122
1153
  'aria-pressed': active,
1123
1154
  'aria-label': title,
1155
+ 'aria-busy': busy || loading,
1124
1156
  title: title,
1125
1157
  disabled: disabled,
1126
1158
  style: style,
1127
1159
  onClick: function() {
1128
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
+ };
1129
1167
  setToast(null);
1130
- void props.select(pair.target).then(function(accepted){
1131
- 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
+ }
1132
1179
  });
1133
1180
  }
1134
1181
  },
1135
- React.createElement('span', { 'aria-hidden': 'true', style: { fontSize: 13, lineHeight: 1 } }, '👁'),
1136
- React.createElement('span', null, t('label')),
1137
- active
1138
- ? React.createElement('span', {
1139
- 'aria-hidden': 'true',
1140
- style: { fontSize: 12, lineHeight: 1, fontWeight: 800, marginLeft: 1 }
1141
- }, '✓')
1142
- : 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'))
1143
1201
  );
1144
1202
 
1145
1203
  var toastNode = toast
@@ -1184,7 +1242,7 @@ export const CLIENT_PRESENTATION_PRELUDE = String.raw`(function(){
1184
1242
  }
1185
1243
  try {
1186
1244
  return Promise.resolve(directory.select(selection)).then(
1187
- function(){ return true; },
1245
+ function(result){ return !result || result.ok !== false; },
1188
1246
  function(){ return false; }
1189
1247
  );
1190
1248
  } catch (_) {
@@ -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;