@deepwatch/dsh-client-settings 0.1.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/LICENSE +21 -0
- package/README.md +82 -0
- package/lib/client/binding-state.d.ts +334 -0
- package/lib/client/binding-state.js +543 -0
- package/lib/client/binding-state.js.map +1 -0
- package/lib/client/chat-gate.d.ts +100 -0
- package/lib/client/chat-gate.js +141 -0
- package/lib/client/chat-gate.js.map +1 -0
- package/lib/client/components.d.ts +194 -0
- package/lib/client/components.js +273 -0
- package/lib/client/components.js.map +1 -0
- package/lib/client/index.d.ts +35 -0
- package/lib/client/index.js +273 -0
- package/lib/client/index.js.map +1 -0
- package/lib/client/onboarding.d.ts +28 -0
- package/lib/client/onboarding.js +31 -0
- package/lib/client/onboarding.js.map +1 -0
- package/lib/client/readiness.d.ts +36 -0
- package/lib/client/readiness.js +207 -0
- package/lib/client/readiness.js.map +1 -0
- package/lib/client/role-bindings.d.ts +118 -0
- package/lib/client/role-bindings.js +211 -0
- package/lib/client/role-bindings.js.map +1 -0
- package/lib/client.js +3888 -0
- package/lib/client.js.map +1 -0
- package/lib/index.d.ts +13 -0
- package/lib/index.js +13 -0
- package/lib/index.js.map +1 -0
- package/lib/ocr-measured.d.ts +27 -0
- package/lib/ocr-measured.js +66 -0
- package/lib/ocr-measured.js.map +1 -0
- package/lib/providers.d.ts +26 -0
- package/lib/providers.js +26 -0
- package/lib/providers.js.map +1 -0
- package/package.json +83 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,3888 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@deepwatch/dsh-client-settings",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
10
|
+
//#region ../brand/lib/identity.js
|
|
11
|
+
/**
|
|
12
|
+
* The Watch product identity, and the one colour rule that matters.
|
|
13
|
+
*
|
|
14
|
+
* Kept as data rather than as markup so the same strings reach the sidebar,
|
|
15
|
+
* the conversation hero, the About panel, the window title and the release
|
|
16
|
+
* notes without four copies drifting apart. Attribution in particular has to
|
|
17
|
+
* be identical everywhere it appears, because it is a legal statement rather
|
|
18
|
+
* than a design element.
|
|
19
|
+
*
|
|
20
|
+
* @module @deepwatch/dsh-client-brand/identity
|
|
21
|
+
*/
|
|
22
|
+
/** What the product is called. Never "DeepSeek" anything. */
|
|
23
|
+
const PRODUCT_NAME = "DeepWatch";
|
|
24
|
+
/**
|
|
25
|
+
* Attribution to the upstream project.
|
|
26
|
+
*
|
|
27
|
+
* Required, and required to be visible. Watch is built on DeepSeek Harness and
|
|
28
|
+
* says so; the alternative — quietly shipping someone else's foundation — is
|
|
29
|
+
* both wrong and against the MIT notice this distribution inherits.
|
|
30
|
+
*/
|
|
31
|
+
const ATTRIBUTION = "Built on DeepSeek Harness · Powered by Watch Skill";
|
|
32
|
+
/**
|
|
33
|
+
* The independence disclosure.
|
|
34
|
+
*
|
|
35
|
+
* Equally required, and for the opposite reason: attribution without it could
|
|
36
|
+
* read as endorsement, and no such endorsement exists.
|
|
37
|
+
*/
|
|
38
|
+
const INDEPENDENCE = "DeepWatch and Watch Skill are independent projects and are not affiliated with or endorsed by DeepSeek.";
|
|
39
|
+
/**
|
|
40
|
+
* The semantic token a tone maps to.
|
|
41
|
+
*
|
|
42
|
+
* Feature packages ask for a tone and get a CSS variable. They never write a
|
|
43
|
+
* hex value, which is what stops the palette from being re-invented slightly
|
|
44
|
+
* differently in every panel — and what makes a theme change one edit.
|
|
45
|
+
*/
|
|
46
|
+
function tokenFor(tone) {
|
|
47
|
+
return `var(--watch-tone-${tone})`;
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region ../brand/lib/mark.js
|
|
51
|
+
/**
|
|
52
|
+
* The Watch mark, inlined.
|
|
53
|
+
*
|
|
54
|
+
* Generated by `scripts/brand-assets.mjs` from `assets/watch-orca-master.png`.
|
|
55
|
+
* Do not edit by hand: the master is the brand source of truth and this is a
|
|
56
|
+
* mechanical derivation of it at 64px, which covers a 32px slot at 2x.
|
|
57
|
+
*
|
|
58
|
+
* @module @deepwatch/dsh-client-brand/mark
|
|
59
|
+
*/
|
|
60
|
+
/** The orca, as a transparent PNG data URI. */
|
|
61
|
+
const WATCH_MARK_PNG = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAF+UlEQVR42u1Za4hUZRhezbxladcfSkmpuO7sfJf5Y5EFupDRDcK0nTnf934nJNDKrCTCROyHv6JCEaWyP9mPjESQiH4EW4iYREkZhGDQRSq7kGl7cc/7zpz23M+M67qrM7s6ngc+dvYwwznP8z7v5ftOS0uGDBkyZMiQIUOGDBnGGtKmFqaQCcDnpcF3pcFPBeAnTFNnUxPPdeIUYXC1NPi9AHSlIbdgk1sw5AqNfQJoeVMSz2saL4BWSoPHfcIhaV+A4O+XAqitKckLoFkc8LOIbFqAgqGKBNzFNU1szsgrWiqA/pRRtKvJe9feYwrHN2mRo3XSEFURtwMhpJ//eJgBTW468kx5+Y5bBZDrLWnKbsEuJ+R9AagiDN19+VVxRdcJQ23C0CIBtEQaXCxtWpgr0XxmcEauRNO5xt0+8ZC8tMPlfTaxCIcvD8LaJwzC4PsC8JeAFLnCJH+9diY0utxfVPGvx9GnkHxyzf8+0M5L28YaZ3ON2wXQ6eTBz7N0QjoWwE4LFX3PF2vfJUm8reRcxTSt54C93sNyTS5XPrmKF2EvdyOCMrZ5DUEztFDebzgQMaDVCyyacAn1a7yJa/zci1DwoOgWN1LvF99VevvR7a9U3L6/Trr9e7rKPR1PY39EPixq1bavWenrkQihUMcGlsUtGttWmLfoVq7xKA/Je6tgk/Nfr3vGHQT/nHadjjXUH5NJRz8iHre+8pCC+P9rOsI1PjBG5PFGrvFYWMSSXAZyV71a7vvtb7e/VoBvj1W6F60ip0oAU50OMiJvD+6KSKS4Zhg/5fYwwJtHuW/jHq7Q9ZZvf11VrLyBpbxiA/U8t6XcveaNcs8jL1Jfrd1FKqqxCzwBngj7v31u8rK2Zmj8lQPdOyrk5z/u5GPiis4ITQck0DtC02tC41bh93T8WgKekUNUfVlT5KpTYBDbwzlc4Kegvxym8MlRmNWxkyt0BshsYppuGKItTuGA9wuNOwXQKZkSYNBKH8366fG3tnsEXcW71ieBugP3hQ6MPgO90Fj7A84TgCtH8huucZrUuFZoPJE88NkCpNMjiXxM+ggr0VqmcI60aVx4QDKbK9wsvXOBpBNVmMKnGr5xuSDxLLxHRFHUNbZOpUVseX9ipB+ZRRaHc7c9ZuGjItWNJGCZa3y4YQIsKNFEpujBkQrRVnTGC40/i8TKZ7W6QvL5Bwn0bLs6/+6vvYgzuMZKdGIUCIwnOdAdjasFJZqTt/B1ATRtROmgcF1s19QJjwDqKhjaKgFfEkB3SXt4g868x3rGMY17Y/JRLQn2EIfaVQMnR2moPa+wSxi6cwQtdCrXeLxWBAH0EwOaOuLNV5HuixwVnyDFxdO/x8uNrQeGbuGA+4ShN+d3OrcN0wVLhcZyusL7+wgYefESQAuFRscrflUtM5kR+oRpYCqERXFc3nJWCsBTAvBtpil33t8AbpAp2wbRwm8u5P7tRWemMLRGGjzkCRFun5M5QdMHo7U52i8g2gHiQaZxbU7h3MGKpXdNAr7iRy4pfmUGJC/mGbjCgtD4Uap9hl2B+CicCdD6pB9HbQx78pZzgClnOwOn2FrC26uE0LhCGvq3ELqAA26rSzAUGqGDabQQ1IcPcyVcLA1uHkiJXdLgNqYczRRdU896MEtAuO0NT3K4pq4w4hOkTddK++wbDqTNTAm4XXoppPB43RypscMLgEy1x0H2GCeYxmV1E4Er3BFX5eAg48CwHVRyprR34qy6Hc+VcHLewv2pFKvaa8RvmAArOQufqddO8Xqhg7PA8Ka/X+jkeNHBANoRFcTIAYWaly0S6DS38CBX+FXrivL0ermASYN/hAJU+Bi8wsorbBWAFLgxHr9Phe2yWxr6WBpaxjVNakxH0DjX2xYXDHrn+ZtGXQBNG9NTYbhJWiJtmjTcKfPiRbCcCVw7nRywY9TfNBnaHXeAoBvt9eaVlisFPJwFCoH9dzbty9QhDmTeCvN/y1gV4bF2gMWVczQPdHXLlYjW5TQpV8SHWjJkyJAhQ4YMGTJkyJAhQ4YMGTIMH/8DqnZ5/XHc+csAAAAASUVORK5CYII=";
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region ../technology/lib/ocr.js
|
|
64
|
+
/** Shared resource limits for a local OCR engine. */
|
|
65
|
+
const LOCAL_LIMITS = {
|
|
66
|
+
maxConcurrency: 2,
|
|
67
|
+
timeoutMs: 12e4,
|
|
68
|
+
maxMemoryMb: 4096
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* The lightweight local route.
|
|
72
|
+
*
|
|
73
|
+
* The default, because it runs everywhere. Not because it is the most accurate
|
|
74
|
+
* — on clean documents it is not — but because a default that needs a GPU is
|
|
75
|
+
* not a default.
|
|
76
|
+
*/
|
|
77
|
+
const RAPID_OCR = {
|
|
78
|
+
id: "ocr.rapidocr",
|
|
79
|
+
displayName: "RapidOCR (ONNX)",
|
|
80
|
+
version: "1.x",
|
|
81
|
+
kind: "engine",
|
|
82
|
+
capabilities: ["document_ocr", "visual"],
|
|
83
|
+
modalities: ["image"],
|
|
84
|
+
roles: ["ocr_layout"],
|
|
85
|
+
runtime: "local_library",
|
|
86
|
+
protocols: [],
|
|
87
|
+
endpoints: [],
|
|
88
|
+
credentialReference: null,
|
|
89
|
+
hardware: {
|
|
90
|
+
gpu: "none",
|
|
91
|
+
minVramGb: null,
|
|
92
|
+
minRamGb: 2,
|
|
93
|
+
accelerators: []
|
|
94
|
+
},
|
|
95
|
+
privacy: {
|
|
96
|
+
egress: "none",
|
|
97
|
+
worksOffline: true,
|
|
98
|
+
requiresEgressConsent: false
|
|
99
|
+
},
|
|
100
|
+
install: {
|
|
101
|
+
method: "package_manager",
|
|
102
|
+
downloadBytes: null,
|
|
103
|
+
automatic: false
|
|
104
|
+
},
|
|
105
|
+
provenance: {
|
|
106
|
+
codeLicense: "Apache-2.0",
|
|
107
|
+
weightsLicense: "Apache-2.0",
|
|
108
|
+
revision: null,
|
|
109
|
+
sourceUrl: "https://github.com/RapidAI/RapidOCR",
|
|
110
|
+
weightsLicenseReviewed: true
|
|
111
|
+
},
|
|
112
|
+
resources: LOCAL_LIMITS,
|
|
113
|
+
trust: "built_in",
|
|
114
|
+
probeMethod: "import check",
|
|
115
|
+
testMethod: "recognize a bundled fixture image"
|
|
116
|
+
};
|
|
117
|
+
/** The deterministic system fallback, for scripts the ONNX route cannot read. */
|
|
118
|
+
const TESSERACT = {
|
|
119
|
+
id: "ocr.tesseract",
|
|
120
|
+
displayName: "Tesseract",
|
|
121
|
+
version: "5.x",
|
|
122
|
+
kind: "engine",
|
|
123
|
+
capabilities: ["document_ocr"],
|
|
124
|
+
modalities: ["image"],
|
|
125
|
+
roles: ["ocr_layout"],
|
|
126
|
+
runtime: "local_process",
|
|
127
|
+
protocols: [],
|
|
128
|
+
endpoints: [],
|
|
129
|
+
credentialReference: null,
|
|
130
|
+
hardware: {
|
|
131
|
+
gpu: "none",
|
|
132
|
+
minVramGb: null,
|
|
133
|
+
minRamGb: 1,
|
|
134
|
+
accelerators: []
|
|
135
|
+
},
|
|
136
|
+
privacy: {
|
|
137
|
+
egress: "none",
|
|
138
|
+
worksOffline: true,
|
|
139
|
+
requiresEgressConsent: false
|
|
140
|
+
},
|
|
141
|
+
install: {
|
|
142
|
+
method: "package_manager",
|
|
143
|
+
downloadBytes: null,
|
|
144
|
+
automatic: false
|
|
145
|
+
},
|
|
146
|
+
provenance: {
|
|
147
|
+
codeLicense: "Apache-2.0",
|
|
148
|
+
weightsLicense: "Apache-2.0",
|
|
149
|
+
revision: null,
|
|
150
|
+
sourceUrl: "https://github.com/tesseract-ocr/tesseract",
|
|
151
|
+
weightsLicenseReviewed: true
|
|
152
|
+
},
|
|
153
|
+
resources: LOCAL_LIMITS,
|
|
154
|
+
trust: "built_in",
|
|
155
|
+
probeMethod: "tesseract --version",
|
|
156
|
+
testMethod: "recognize a bundled fixture image"
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* DeepSeek-OCR, the compatibility and comparison route.
|
|
160
|
+
*
|
|
161
|
+
* Pinned to a reviewed revision. `trust_remote_code` in its published
|
|
162
|
+
* inference path is why `trust` is `isolated` and the runtime is a separate
|
|
163
|
+
* process: a surprise in a model repository should cost a worker, not the
|
|
164
|
+
* workspace.
|
|
165
|
+
*/
|
|
166
|
+
const DEEPSEEK_OCR = {
|
|
167
|
+
id: "ocr.deepseek-ocr",
|
|
168
|
+
displayName: "DeepSeek-OCR",
|
|
169
|
+
version: "1.0",
|
|
170
|
+
kind: "engine",
|
|
171
|
+
capabilities: ["document_ocr", "visual"],
|
|
172
|
+
modalities: ["image"],
|
|
173
|
+
roles: ["ocr_layout"],
|
|
174
|
+
runtime: "local_process",
|
|
175
|
+
protocols: [],
|
|
176
|
+
endpoints: [],
|
|
177
|
+
credentialReference: null,
|
|
178
|
+
hardware: {
|
|
179
|
+
gpu: "required",
|
|
180
|
+
minVramGb: 12,
|
|
181
|
+
minRamGb: 16,
|
|
182
|
+
accelerators: ["cuda"]
|
|
183
|
+
},
|
|
184
|
+
privacy: {
|
|
185
|
+
egress: "none",
|
|
186
|
+
worksOffline: true,
|
|
187
|
+
requiresEgressConsent: false
|
|
188
|
+
},
|
|
189
|
+
install: {
|
|
190
|
+
method: "download",
|
|
191
|
+
downloadBytes: null,
|
|
192
|
+
automatic: false
|
|
193
|
+
},
|
|
194
|
+
provenance: {
|
|
195
|
+
codeLicense: "MIT",
|
|
196
|
+
weightsLicense: null,
|
|
197
|
+
revision: "09eaf526153e7a01ed16c9dea8c96282aaea29c0",
|
|
198
|
+
sourceUrl: "https://github.com/deepseek-ai/DeepSeek-OCR",
|
|
199
|
+
weightsLicenseReviewed: false
|
|
200
|
+
},
|
|
201
|
+
resources: {
|
|
202
|
+
maxConcurrency: 1,
|
|
203
|
+
timeoutMs: 3e5,
|
|
204
|
+
maxMemoryMb: 16384
|
|
205
|
+
},
|
|
206
|
+
trust: "isolated",
|
|
207
|
+
probeMethod: "worker handshake",
|
|
208
|
+
testMethod: "recognize a bundled fixture image in the worker"
|
|
209
|
+
};
|
|
210
|
+
/** The engines this build knows about. */
|
|
211
|
+
const OCR_ENGINES = [
|
|
212
|
+
RAPID_OCR,
|
|
213
|
+
TESSERACT,
|
|
214
|
+
DEEPSEEK_OCR,
|
|
215
|
+
{
|
|
216
|
+
...DEEPSEEK_OCR,
|
|
217
|
+
id: "ocr.deepseek-ocr2",
|
|
218
|
+
displayName: "DeepSeek-OCR2",
|
|
219
|
+
version: "2.0",
|
|
220
|
+
provenance: {
|
|
221
|
+
codeLicense: "Apache-2.0",
|
|
222
|
+
weightsLicense: null,
|
|
223
|
+
revision: "2f3699ebbb96fa8af32212e8c170f2cc28730fad",
|
|
224
|
+
sourceUrl: "https://github.com/deepseek-ai/DeepSeek-OCR-2",
|
|
225
|
+
weightsLicenseReviewed: false
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
id: "ocr.cloud",
|
|
230
|
+
displayName: "Cloud OCR (deployment-configured)",
|
|
231
|
+
version: "1.0",
|
|
232
|
+
kind: "engine",
|
|
233
|
+
capabilities: ["document_ocr", "visual"],
|
|
234
|
+
modalities: ["image"],
|
|
235
|
+
roles: ["ocr_layout"],
|
|
236
|
+
runtime: "remote",
|
|
237
|
+
protocols: ["https"],
|
|
238
|
+
endpoints: [],
|
|
239
|
+
credentialReference: "dsh:connection",
|
|
240
|
+
hardware: {
|
|
241
|
+
gpu: "none",
|
|
242
|
+
minVramGb: null,
|
|
243
|
+
minRamGb: 0,
|
|
244
|
+
accelerators: []
|
|
245
|
+
},
|
|
246
|
+
privacy: {
|
|
247
|
+
egress: "content",
|
|
248
|
+
worksOffline: false,
|
|
249
|
+
requiresEgressConsent: true
|
|
250
|
+
},
|
|
251
|
+
install: {
|
|
252
|
+
method: "none",
|
|
253
|
+
downloadBytes: null,
|
|
254
|
+
automatic: false
|
|
255
|
+
},
|
|
256
|
+
provenance: {
|
|
257
|
+
codeLicense: "proprietary",
|
|
258
|
+
weightsLicense: "proprietary",
|
|
259
|
+
revision: null,
|
|
260
|
+
sourceUrl: "",
|
|
261
|
+
weightsLicenseReviewed: true
|
|
262
|
+
},
|
|
263
|
+
resources: {
|
|
264
|
+
maxConcurrency: 4,
|
|
265
|
+
timeoutMs: 6e4,
|
|
266
|
+
maxMemoryMb: 256
|
|
267
|
+
},
|
|
268
|
+
trust: "untrusted",
|
|
269
|
+
probeMethod: "endpoint reachability",
|
|
270
|
+
testMethod: "recognize a bundled fixture image over the wire"
|
|
271
|
+
}
|
|
272
|
+
];
|
|
273
|
+
//#endregion
|
|
274
|
+
//#region ../contracts/lib/readiness.js
|
|
275
|
+
/**
|
|
276
|
+
* Whether a role can actually run, kept as four separate facts.
|
|
277
|
+
*
|
|
278
|
+
* This module exists because they were one. A provider row showed a green dot
|
|
279
|
+
* the moment a credential was saved, and a person reasonably read that as "the
|
|
280
|
+
* product is ready" — so they typed a prompt, and the runtime routed it to a
|
|
281
|
+
* provider they had never configured, failed on a missing environment
|
|
282
|
+
* variable, and left a failed turn in their session. Every step of that was a
|
|
283
|
+
* consequence of one indicator standing for four different questions:
|
|
284
|
+
*
|
|
285
|
+
* 1. is there a credential? {@link ProviderCredentialStatus}
|
|
286
|
+
* 2. does the provider answer? {@link ProviderReachability}
|
|
287
|
+
* 3. is a model chosen? {@link ModelSelectionStatus}
|
|
288
|
+
* 4. is a role bound and runnable? {@link RoleBindingStatus}
|
|
289
|
+
*
|
|
290
|
+
* A credential is the *first* of those and implies none of the others.
|
|
291
|
+
* {@link roleReadiness} is the only thing in this product allowed to answer
|
|
292
|
+
* "ready", and it requires all four to line up, plus a route that supports the
|
|
293
|
+
* role and a policy state that permits the request.
|
|
294
|
+
*
|
|
295
|
+
* **Nothing here ever carries secret material.** Not a value, not a prefix, a
|
|
296
|
+
* suffix, a length or a hash. A credential appears in these types only as a
|
|
297
|
+
* *reference* — an opaque handle the Host can resolve — and as a status word.
|
|
298
|
+
* `configured_unverified` is the honest state after a save: something is
|
|
299
|
+
* stored, and nobody has asked the provider whether it works.
|
|
300
|
+
*
|
|
301
|
+
* Verification is deliberately not automatic. Saving a credential must not
|
|
302
|
+
* quietly spend a request, and discovering models must be distinguishable from
|
|
303
|
+
* a billable completion — so both are user actions with their own results, and
|
|
304
|
+
* {@link ProviderReachability} stays `unknown` until one of them runs.
|
|
305
|
+
*
|
|
306
|
+
* @module @deepwatch/dsh-contracts/readiness
|
|
307
|
+
*/
|
|
308
|
+
/**
|
|
309
|
+
* The order blockers are reported in.
|
|
310
|
+
*
|
|
311
|
+
* Not alphabetical and not arbitrary: it is the order a person has to fix them
|
|
312
|
+
* in. Telling somebody their model is unavailable when they have not chosen a
|
|
313
|
+
* provider sends them to the wrong screen.
|
|
314
|
+
*/
|
|
315
|
+
const BLOCKER_ORDER = [
|
|
316
|
+
"no_binding",
|
|
317
|
+
"provider_unknown",
|
|
318
|
+
"credential_absent",
|
|
319
|
+
"credential_inaccessible",
|
|
320
|
+
"credential_rejected",
|
|
321
|
+
"model_unset",
|
|
322
|
+
"model_invalid",
|
|
323
|
+
"model_unavailable",
|
|
324
|
+
"provider_untested",
|
|
325
|
+
"provider_unreachable",
|
|
326
|
+
"provider_rate_limited",
|
|
327
|
+
"route_lacks_role",
|
|
328
|
+
"modality_unsupported",
|
|
329
|
+
"contract_mismatch",
|
|
330
|
+
"consent_required",
|
|
331
|
+
"policy_forbids"
|
|
332
|
+
];
|
|
333
|
+
/**
|
|
334
|
+
* Whether a role can run, and if not, exactly what is missing.
|
|
335
|
+
*
|
|
336
|
+
* The single gate. A caller may not assemble "ready" from parts: every surface
|
|
337
|
+
* that shows readiness, and both halves of preflight, ask this function, so
|
|
338
|
+
* there is one definition of executable and it is testable on its own.
|
|
339
|
+
*
|
|
340
|
+
* @param inputs - every fact that bears on the decision.
|
|
341
|
+
* @param role - the role being asked about.
|
|
342
|
+
* @returns the status and the blockers, in the order they must be fixed.
|
|
343
|
+
*/
|
|
344
|
+
function roleReadiness(role, inputs) {
|
|
345
|
+
const found = /* @__PURE__ */ new Set();
|
|
346
|
+
const { binding, route } = inputs;
|
|
347
|
+
if (binding === null) found.add("no_binding");
|
|
348
|
+
else {
|
|
349
|
+
if (route === null) found.add("provider_unknown");
|
|
350
|
+
if (inputs.credential === "absent") found.add("credential_absent");
|
|
351
|
+
if (inputs.credential === "inaccessible") found.add("credential_inaccessible");
|
|
352
|
+
if (inputs.credential === "rejected") found.add("credential_rejected");
|
|
353
|
+
if (inputs.credential === "configured_unverified") found.add("provider_untested");
|
|
354
|
+
if (inputs.reachability === "unauthorized") found.add("credential_rejected");
|
|
355
|
+
if (inputs.reachability === "unknown") found.add("provider_untested");
|
|
356
|
+
if (inputs.reachability === "unreachable") found.add("provider_unreachable");
|
|
357
|
+
if (inputs.reachability === "rate_limited") found.add("provider_rate_limited");
|
|
358
|
+
if (inputs.model === "none" || binding.model === "") found.add("model_unset");
|
|
359
|
+
if (inputs.model === "invalid") found.add("model_invalid");
|
|
360
|
+
if (inputs.model === "unavailable") found.add("model_unavailable");
|
|
361
|
+
if (route !== null) {
|
|
362
|
+
if (!route.roles.includes(role)) found.add("route_lacks_role");
|
|
363
|
+
const supported = new Set(route.modalities);
|
|
364
|
+
if (!binding.modalities.every((modality) => supported.has(modality))) found.add("modality_unsupported");
|
|
365
|
+
}
|
|
366
|
+
if (!inputs.contractMatches) found.add("contract_mismatch");
|
|
367
|
+
if (!inputs.consentGranted) found.add("consent_required");
|
|
368
|
+
if (!inputs.policyPermits) found.add("policy_forbids");
|
|
369
|
+
}
|
|
370
|
+
const blockers = BLOCKER_ORDER.filter((blocker) => found.has(blocker));
|
|
371
|
+
if (blockers.length === 0) return {
|
|
372
|
+
role,
|
|
373
|
+
status: "executable",
|
|
374
|
+
blockers: [],
|
|
375
|
+
primaryBlocker: null
|
|
376
|
+
};
|
|
377
|
+
return {
|
|
378
|
+
role,
|
|
379
|
+
status: binding === null ? "unbound" : blockers.every((blocker) => blocker === "provider_untested") ? "bound_unverified" : "blocked",
|
|
380
|
+
blockers,
|
|
381
|
+
primaryBlocker: blockers[0] ?? null
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Whether a role may be described to a person as ready.
|
|
386
|
+
*
|
|
387
|
+
* A helper rather than a comparison at each call site, because "ready" is
|
|
388
|
+
* exactly the word this whole module exists to stop being used loosely.
|
|
389
|
+
*/
|
|
390
|
+
function isExecutable(readiness) {
|
|
391
|
+
return readiness.status === "executable";
|
|
392
|
+
}
|
|
393
|
+
/** What a person should be told to do about one blocker, in their words. */
|
|
394
|
+
const BLOCKER_COPY = {
|
|
395
|
+
no_binding: "Choose a provider and model for this capability.",
|
|
396
|
+
provider_unknown: "The bound provider is no longer available. Choose another.",
|
|
397
|
+
credential_absent: "Add a credential for this provider.",
|
|
398
|
+
credential_inaccessible: "The saved credential could not be read. Save it again.",
|
|
399
|
+
credential_rejected: "The provider rejected the saved credential. Update it.",
|
|
400
|
+
provider_untested: "Run the provider test before using this capability.",
|
|
401
|
+
provider_unreachable: "The provider did not answer. Check the network and try again.",
|
|
402
|
+
provider_rate_limited: "The provider rate-limited the test. Wait, then try again.",
|
|
403
|
+
model_unset: "Choose a model for this capability.",
|
|
404
|
+
model_invalid: "The chosen model is not valid for this provider. Choose another.",
|
|
405
|
+
model_unavailable: "The provider no longer offers the chosen model. Choose another.",
|
|
406
|
+
route_lacks_role: "This provider cannot serve this capability. Choose another.",
|
|
407
|
+
modality_unsupported: "This model does not support what this capability needs.",
|
|
408
|
+
contract_mismatch: "This version of DeepWatch cannot talk to that provider.",
|
|
409
|
+
consent_required: "This capability needs a consent that has not been granted.",
|
|
410
|
+
policy_forbids: "Policy on this machine forbids this request."
|
|
411
|
+
};
|
|
412
|
+
/**
|
|
413
|
+
* One sentence a person can act on, for the blocker that matters most.
|
|
414
|
+
*
|
|
415
|
+
* Deliberately free of route ids, package names and environment variables:
|
|
416
|
+
* those belong in Diagnostics, and a person reading their first error should
|
|
417
|
+
* be told what to do rather than what broke internally.
|
|
418
|
+
*/
|
|
419
|
+
function blockerMessage(readiness) {
|
|
420
|
+
const blocker = readiness.primaryBlocker;
|
|
421
|
+
return blocker === null ? null : BLOCKER_COPY[blocker];
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* The accessible label for a status, so colour is never the only signal.
|
|
425
|
+
*
|
|
426
|
+
* Every surface uses these words, so a screen reader and a sighted reader are
|
|
427
|
+
* told the same thing, and a red or green dot is decoration on top of text
|
|
428
|
+
* rather than the fact itself.
|
|
429
|
+
*/
|
|
430
|
+
const BINDING_STATUS_LABEL = {
|
|
431
|
+
unbound: "Not configured",
|
|
432
|
+
bound_unverified: "Configured · not tested",
|
|
433
|
+
executable: "Ready",
|
|
434
|
+
blocked: "Blocked"
|
|
435
|
+
};
|
|
436
|
+
/** The accessible label for a credential status. */
|
|
437
|
+
const CREDENTIAL_STATUS_LABEL = {
|
|
438
|
+
absent: "No credential",
|
|
439
|
+
configured_unverified: "Credential saved · not yet assigned",
|
|
440
|
+
verified: "Credential accepted",
|
|
441
|
+
rejected: "Credential rejected",
|
|
442
|
+
inaccessible: "Credential unreadable"
|
|
443
|
+
};
|
|
444
|
+
//#endregion
|
|
445
|
+
//#region ../contracts/lib/bindings.js
|
|
446
|
+
/**
|
|
447
|
+
* What a person actually chose: which provider, which model, for which role.
|
|
448
|
+
*
|
|
449
|
+
* {@link module:@deepwatch/dsh-contracts/readiness} answers "can this run?"
|
|
450
|
+
* from four separate facts. This module is where three of those facts are
|
|
451
|
+
* *kept* — a durable document recording the decisions, so that reopening the
|
|
452
|
+
* product finds the same bindings rather than an empty screen and a composer
|
|
453
|
+
* pointed at somebody else's default.
|
|
454
|
+
*
|
|
455
|
+
* **A binding is a reference, never a credential.** The document below can be
|
|
456
|
+
* read by anything: it is written to the Harness's own settings file, it rides
|
|
457
|
+
* the settings RPC, it appears in Diagnostics, and it is included in a session
|
|
458
|
+
* export. None of that is safe unless the rule is absolute, so it is: the only
|
|
459
|
+
* credential-shaped field here is {@link RoleBindingRecord.credentialRef}, an
|
|
460
|
+
* opaque handle the Host resolves against its own store. No value, no prefix,
|
|
461
|
+
* no suffix, no length, no hash. {@link assertNoSecretMaterial} is the test
|
|
462
|
+
* this file is held to.
|
|
463
|
+
*
|
|
464
|
+
* **Nothing is bound implicitly.** There is no "default role", no inheritance
|
|
465
|
+
* from one role to another, and no provider that becomes bound because it was
|
|
466
|
+
* the only one configured. A role with no entry in {@link WatchBindings.roles}
|
|
467
|
+
* is unbound, and unbound means the composer refuses. That is the whole point
|
|
468
|
+
* of the module: the failure it exists to prevent was a product that treated a
|
|
469
|
+
* saved credential as a decision the person never made.
|
|
470
|
+
*
|
|
471
|
+
* @module @deepwatch/dsh-contracts/bindings
|
|
472
|
+
*/
|
|
473
|
+
/**
|
|
474
|
+
* The settings namespace this document lives in.
|
|
475
|
+
*
|
|
476
|
+
* A DeepWatch-owned section of the Harness's own user-settings document, which
|
|
477
|
+
* is what makes the binding durable, hot-reloaded and editable by hand without
|
|
478
|
+
* DeepWatch inventing a second configuration store beside the one the product
|
|
479
|
+
* already has.
|
|
480
|
+
*/
|
|
481
|
+
const BINDINGS_NAMESPACE = "watch-bindings";
|
|
482
|
+
/**
|
|
483
|
+
* The roles a person can bind to a provider, in the order setup presents them.
|
|
484
|
+
*
|
|
485
|
+
* These ids are `RoleId`s from `@deepwatch/dsh-technology`, spelled here rather
|
|
486
|
+
* than imported because `contracts` is the package everything else depends on
|
|
487
|
+
* and must not depend on anything. `tests/bindings-store.test.mjs` asserts the
|
|
488
|
+
* two lists agree, so the duplication is checked rather than trusted -- one
|
|
489
|
+
* role vocabulary with a gate on it, instead of two that drift.
|
|
490
|
+
*
|
|
491
|
+
* It is a *subset*. `verifier`, `ocr_layout`, `reranking` and
|
|
492
|
+
* `speaker_diarization` are served by local engines rather than chosen from a
|
|
493
|
+
* provider catalogue, so offering them here would offer a choice that is not
|
|
494
|
+
* there.
|
|
495
|
+
*
|
|
496
|
+
* `agent_model` is first and is the only one the first conversation needs. The
|
|
497
|
+
* rest are progressive: a product that demanded five bindings before the first
|
|
498
|
+
* message would be a product nobody finished configuring.
|
|
499
|
+
*/
|
|
500
|
+
const BINDABLE_ROLES = [
|
|
501
|
+
"agent_model",
|
|
502
|
+
"visual_perception",
|
|
503
|
+
"asr",
|
|
504
|
+
"audio_understanding",
|
|
505
|
+
"embeddings"
|
|
506
|
+
];
|
|
507
|
+
/** The role the first conversation needs, named once so nothing spells it twice. */
|
|
508
|
+
const PRIMARY_ROLE = "agent_model";
|
|
509
|
+
/**
|
|
510
|
+
* What each role is called on screen.
|
|
511
|
+
*
|
|
512
|
+
* `agent_model` is labelled **Chat**, and the difference is not cosmetic. A
|
|
513
|
+
* person configuring this product is not choosing an "agent model" -- they are
|
|
514
|
+
* choosing what answers them in the surface the Harness calls Chat, and every
|
|
515
|
+
* blocked-composer message and setup step has to name the thing they are
|
|
516
|
+
* looking at. The id stays `agent_model` because that is the vocabulary the
|
|
517
|
+
* descriptors and the routing rules already use.
|
|
518
|
+
*/
|
|
519
|
+
const ROLE_LABEL = {
|
|
520
|
+
agent_model: "Chat",
|
|
521
|
+
visual_perception: "Visual perception",
|
|
522
|
+
asr: "Speech to text",
|
|
523
|
+
audio_understanding: "Audio understanding",
|
|
524
|
+
embeddings: "Embeddings and retrieval"
|
|
525
|
+
};
|
|
526
|
+
/** Whether a string is a role this product binds. */
|
|
527
|
+
function isBindableRole(value) {
|
|
528
|
+
return BINDABLE_ROLES.includes(value);
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* What each role is for, in a person's words.
|
|
532
|
+
*
|
|
533
|
+
* Here rather than in a component because the setup flow, the Role Bindings
|
|
534
|
+
* screen and the blocked-composer card all name the same role, and three
|
|
535
|
+
* copies of this sentence would eventually be three different sentences.
|
|
536
|
+
*/
|
|
537
|
+
const ROLE_PURPOSE = {
|
|
538
|
+
agent_model: "Plans, reasons and writes. This is what answers you in a conversation.",
|
|
539
|
+
visual_perception: "Reads what is on screen or in a frame.",
|
|
540
|
+
asr: "Speech to text, with timings a citation can point at.",
|
|
541
|
+
audio_understanding: "Non-speech audio: events, tone, music.",
|
|
542
|
+
embeddings: "Search over the library and over memory."
|
|
543
|
+
};
|
|
544
|
+
/** The modalities each role's work actually needs a route to support. */
|
|
545
|
+
const ROLE_MODALITIES = {
|
|
546
|
+
agent_model: ["text"],
|
|
547
|
+
visual_perception: ["vision"],
|
|
548
|
+
asr: ["audio"],
|
|
549
|
+
audio_understanding: ["audio"],
|
|
550
|
+
embeddings: ["embedding"]
|
|
551
|
+
};
|
|
552
|
+
/** The actors a stored document may name. */
|
|
553
|
+
const BINDING_ACTORS = [
|
|
554
|
+
"person",
|
|
555
|
+
"setup",
|
|
556
|
+
"unknown"
|
|
557
|
+
];
|
|
558
|
+
/** Whether a stored value is an actor this build understands. */
|
|
559
|
+
function isBindingActor(value) {
|
|
560
|
+
return typeof value === "string" && BINDING_ACTORS.includes(value);
|
|
561
|
+
}
|
|
562
|
+
/** The document a profile that has never been configured has. */
|
|
563
|
+
const EMPTY_BINDINGS = {
|
|
564
|
+
version: 1,
|
|
565
|
+
roles: {}
|
|
566
|
+
};
|
|
567
|
+
/**
|
|
568
|
+
* A model id worth storing.
|
|
569
|
+
*
|
|
570
|
+
* Deliberately permissive about shape — provider model ids are provider-owned
|
|
571
|
+
* and this product does not get to decide that `openai/gpt-4o` is malformed —
|
|
572
|
+
* and deliberately strict about the things that make a stored value dangerous:
|
|
573
|
+
* control characters, newlines and absurd length, all of which arrive from a
|
|
574
|
+
* hand-edited settings file rather than from the picker.
|
|
575
|
+
*/
|
|
576
|
+
function isStorableId(value) {
|
|
577
|
+
return typeof value === "string" && value !== "" && value.length <= 200 && !/[\u0000-\u001f\u007f]/.test(value);
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Read a stored document, keeping only what is well-formed.
|
|
581
|
+
*
|
|
582
|
+
* A hand-edited settings file is a supported way to configure this product, so
|
|
583
|
+
* a malformed entry must not take the whole document with it: the bad role is
|
|
584
|
+
* dropped and the rest survive. Dropping is the safe direction — an unbound
|
|
585
|
+
* role refuses at the composer, where a person is told what to fix, whereas a
|
|
586
|
+
* half-read binding would route a prompt somewhere nobody chose.
|
|
587
|
+
*
|
|
588
|
+
* @param raw - whatever the settings document held.
|
|
589
|
+
* @returns a document this build can act on.
|
|
590
|
+
*/
|
|
591
|
+
function readBindings(raw) {
|
|
592
|
+
if (typeof raw !== "object" || raw === null) return EMPTY_BINDINGS;
|
|
593
|
+
const record = raw;
|
|
594
|
+
if (typeof record.version === "number" && record.version > 1) return EMPTY_BINDINGS;
|
|
595
|
+
if (typeof record.roles !== "object" || record.roles === null) return EMPTY_BINDINGS;
|
|
596
|
+
const roles = {};
|
|
597
|
+
for (const [role, value] of Object.entries(record.roles)) {
|
|
598
|
+
if (!isBindableRole(role)) continue;
|
|
599
|
+
const entry = readBindingRecord(value);
|
|
600
|
+
if (entry !== null) roles[role] = entry;
|
|
601
|
+
}
|
|
602
|
+
return {
|
|
603
|
+
version: 1,
|
|
604
|
+
roles
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
/** One entry, or null when it is not something this build can honour. */
|
|
608
|
+
function readBindingRecord(value) {
|
|
609
|
+
if (typeof value !== "object" || value === null) return null;
|
|
610
|
+
const entry = value;
|
|
611
|
+
if (!isStorableId(entry["provider"]) || !isStorableId(entry["model"])) return null;
|
|
612
|
+
const ref = entry["credentialRef"];
|
|
613
|
+
const credentialRef = typeof ref === "string" && ref !== "" ? ref : null;
|
|
614
|
+
const boundAt = typeof entry["boundAt"] === "string" ? entry["boundAt"] : "";
|
|
615
|
+
const boundBy = isBindingActor(entry["boundBy"]) ? entry["boundBy"] : "unknown";
|
|
616
|
+
return {
|
|
617
|
+
provider: entry["provider"],
|
|
618
|
+
model: entry["model"],
|
|
619
|
+
credentialRef,
|
|
620
|
+
boundAt,
|
|
621
|
+
boundBy
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* The document with one role bound, as a new value.
|
|
626
|
+
*
|
|
627
|
+
* Never mutates: the caller holds a snapshot it may still be rendering from,
|
|
628
|
+
* and a document edited underneath a React tree is a stale-render bug that
|
|
629
|
+
* shows somebody the binding they had a moment ago.
|
|
630
|
+
*/
|
|
631
|
+
function withBinding(current, role, record) {
|
|
632
|
+
return {
|
|
633
|
+
version: 1,
|
|
634
|
+
roles: {
|
|
635
|
+
...current.roles,
|
|
636
|
+
[role]: record
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
/** The document with one role unbound. */
|
|
641
|
+
function withoutBinding(current, role) {
|
|
642
|
+
return {
|
|
643
|
+
version: 1,
|
|
644
|
+
roles: Object.fromEntries(Object.entries(current.roles).filter(([key]) => key !== role))
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* The readiness-shaped view of one stored role, or null when it is unbound.
|
|
649
|
+
*
|
|
650
|
+
* The join between this module and `readiness`: storage keeps records, the
|
|
651
|
+
* gate takes {@link RoleBinding}s, and this is the only place that converts
|
|
652
|
+
* one into the other — so the modalities a role is checked against always come
|
|
653
|
+
* from {@link ROLE_MODALITIES} rather than from whatever a call site guessed.
|
|
654
|
+
*/
|
|
655
|
+
function bindingFor(bindings, role) {
|
|
656
|
+
const record = bindings.roles[role];
|
|
657
|
+
if (record === void 0) return null;
|
|
658
|
+
return {
|
|
659
|
+
role,
|
|
660
|
+
provider: record.provider,
|
|
661
|
+
model: record.model,
|
|
662
|
+
credentialRef: record.credentialRef,
|
|
663
|
+
modalities: ROLE_MODALITIES[role]
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Patterns that must never appear in a stored binding.
|
|
668
|
+
*
|
|
669
|
+
* Not an exhaustive secret detector — there is no such thing — but a guard on
|
|
670
|
+
* the shapes a credential takes when somebody pastes one into a field meant to
|
|
671
|
+
* hold a reference.
|
|
672
|
+
*
|
|
673
|
+
* `OPENROUTER_API_KEY` is deliberately *not* one of them, and that distinction
|
|
674
|
+
* is the design rather than an omission: it is a reference — the name of a
|
|
675
|
+
* place the Host looks — and naming a place is exactly what `credentialRef` is
|
|
676
|
+
* for. A guard that refused it would refuse every legitimate binding, be
|
|
677
|
+
* switched off within a day, and stop catching what it was written for. What
|
|
678
|
+
* is refused is a *value*: the shapes a key has when the key itself is pasted.
|
|
679
|
+
*/
|
|
680
|
+
const SECRET_SHAPES = [
|
|
681
|
+
/\bsk-[A-Za-z0-9_-]{8,}/,
|
|
682
|
+
/\bsk_live_[A-Za-z0-9]{8,}/,
|
|
683
|
+
/\bBearer\s+[A-Za-z0-9._-]{8,}/i,
|
|
684
|
+
/(?=[A-Za-z0-9+/_-]{32,})(?=[^\n]*[a-z])(?=[^\n]*[A-Z])(?=[^\n]*\d)[A-Za-z0-9+/_-]{32,}/
|
|
685
|
+
];
|
|
686
|
+
/**
|
|
687
|
+
* Throw when a document about to be stored or shown carries secret material.
|
|
688
|
+
*
|
|
689
|
+
* A programming error rather than a runtime condition: every write path builds
|
|
690
|
+
* this document from a picker, so a value matching one of these means a code
|
|
691
|
+
* path has started copying a credential into a place that is read back in
|
|
692
|
+
* plain text. Failing loudly at the write is the only point where that is
|
|
693
|
+
* still cheap to fix.
|
|
694
|
+
*
|
|
695
|
+
* @param where - the surface being guarded, for a message that can be acted on.
|
|
696
|
+
* @param bindings - the document about to leave a trusted boundary.
|
|
697
|
+
*/
|
|
698
|
+
function assertNoSecretMaterial(where, bindings) {
|
|
699
|
+
for (const [role, record] of Object.entries(bindings.roles)) for (const [field, value] of Object.entries(record)) {
|
|
700
|
+
if (typeof value !== "string") continue;
|
|
701
|
+
for (const shape of SECRET_SHAPES) {
|
|
702
|
+
if (!shape.test(value)) continue;
|
|
703
|
+
throw new Error(`${where}: the ${role} binding's ${field} looks like credential material. A binding stores a reference the Host resolves, never a value.`);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
//#endregion
|
|
708
|
+
//#region ../contracts/lib/failures.js
|
|
709
|
+
/**
|
|
710
|
+
* What a person is told when a conversation cannot run, and what they are not.
|
|
711
|
+
*
|
|
712
|
+
* A prompt failed and the Chat surface showed the reader
|
|
713
|
+
* `@deepseek-ai/dsh-system-prompt`, the route id `llm-deepseek`, the provider
|
|
714
|
+
* key `deepseek-official`, the environment variable `DEEPSEEK_API_KEY`, and a
|
|
715
|
+
* paragraph of sandbox-policy text. Every one of those is true, and not one of
|
|
716
|
+
* them told the reader what to do. Worse, two of them named a provider they had
|
|
717
|
+
* never chosen, so the message actively misled: it read as *DeepWatch is a
|
|
718
|
+
* DeepSeek product that is broken* rather than *nothing is bound yet*.
|
|
719
|
+
*
|
|
720
|
+
* This module draws the line. A raw provider or runtime failure is classified
|
|
721
|
+
* into one of a small closed set of {@link FailureKind}s, each of which becomes
|
|
722
|
+
* a card with a title, one sentence, and exactly one next action. The detailed
|
|
723
|
+
* technical facts are not destroyed — they go to Diagnostics and the Session
|
|
724
|
+
* Log, redacted — but they never render in an ordinary conversation.
|
|
725
|
+
*
|
|
726
|
+
* **Two rules hold everything here together.**
|
|
727
|
+
*
|
|
728
|
+
* The first is that a card carries no implementation identity.
|
|
729
|
+
* {@link assertNoInternalDisclosure} is the executable form of that rule, and
|
|
730
|
+
* it is applied to every card this module produces rather than trusted to
|
|
731
|
+
* review.
|
|
732
|
+
*
|
|
733
|
+
* The second is that nothing here ever carries credential material — and
|
|
734
|
+
* "material" includes the things people reach for when they are trying to be
|
|
735
|
+
* helpful about a key without printing it: a prefix, a suffix, a length, a
|
|
736
|
+
* fingerprint, a hash. A reader does not need to be told their key is 51
|
|
737
|
+
* characters long, and an attacker reading a shared screenshot does.
|
|
738
|
+
*
|
|
739
|
+
* @module @deepwatch/dsh-contracts/failures
|
|
740
|
+
*/
|
|
741
|
+
/** The card each kind renders as. */
|
|
742
|
+
const CARDS = {
|
|
743
|
+
not_configured: {
|
|
744
|
+
title: "Chat model is not configured",
|
|
745
|
+
detail: "Choose a provider and a model, then assign one to Chat. Nothing is sent until you do.",
|
|
746
|
+
action: "Choose models and roles",
|
|
747
|
+
target: "role-bindings"
|
|
748
|
+
},
|
|
749
|
+
not_tested: {
|
|
750
|
+
title: "Chat provider has not been tested",
|
|
751
|
+
detail: "The provider and model are assigned, but no provider request has succeeded yet.",
|
|
752
|
+
action: "Run provider test",
|
|
753
|
+
target: "role-bindings"
|
|
754
|
+
},
|
|
755
|
+
credential_unavailable: {
|
|
756
|
+
title: "Credential is unavailable",
|
|
757
|
+
detail: "A credential is assigned to this provider, and it could not be read on this machine.",
|
|
758
|
+
action: "Review the provider credential",
|
|
759
|
+
target: "provider-credential"
|
|
760
|
+
},
|
|
761
|
+
credential_rejected: {
|
|
762
|
+
title: "Provider rejected the credential",
|
|
763
|
+
detail: "The provider answered and would not accept the credential saved for it.",
|
|
764
|
+
action: "Update the provider credential",
|
|
765
|
+
target: "provider-credential"
|
|
766
|
+
},
|
|
767
|
+
model_unavailable: {
|
|
768
|
+
title: "Model is no longer available",
|
|
769
|
+
detail: "The provider no longer offers the model assigned to this capability.",
|
|
770
|
+
action: "Choose another model",
|
|
771
|
+
target: "model-selection"
|
|
772
|
+
},
|
|
773
|
+
provider_unreachable: {
|
|
774
|
+
title: "Provider is temporarily unreachable",
|
|
775
|
+
detail: "No usable answer came back from the provider. Nothing was charged and no turn was recorded.",
|
|
776
|
+
action: "Try again",
|
|
777
|
+
target: "retry"
|
|
778
|
+
},
|
|
779
|
+
rate_limited: {
|
|
780
|
+
title: "Request was rate-limited",
|
|
781
|
+
detail: "The provider declined this request for rate reasons. Waiting and retrying usually clears it.",
|
|
782
|
+
action: "Try again",
|
|
783
|
+
target: "retry"
|
|
784
|
+
},
|
|
785
|
+
policy_forbids: {
|
|
786
|
+
title: "This machine’s policy blocked the request",
|
|
787
|
+
detail: "A policy in force here does not permit this request. Diagnostics records which one.",
|
|
788
|
+
action: "Open Diagnostics",
|
|
789
|
+
target: "diagnostics"
|
|
790
|
+
},
|
|
791
|
+
unavailable: {
|
|
792
|
+
title: "The request could not be completed",
|
|
793
|
+
detail: "Something went wrong before a reply could be produced. Diagnostics has the detail.",
|
|
794
|
+
action: "Open Diagnostics",
|
|
795
|
+
target: "diagnostics"
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
/**
|
|
799
|
+
* The card for one kind.
|
|
800
|
+
*
|
|
801
|
+
* @param kind - the classified failure.
|
|
802
|
+
* @param hasDiagnostics - whether a redacted record was written for it.
|
|
803
|
+
* @returns a card safe to render in an ordinary conversation.
|
|
804
|
+
*/
|
|
805
|
+
function failureCard(kind, hasDiagnostics = true) {
|
|
806
|
+
return {
|
|
807
|
+
kind,
|
|
808
|
+
...CARDS[kind],
|
|
809
|
+
hasDiagnostics
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* The card a readiness blocker becomes.
|
|
814
|
+
*
|
|
815
|
+
* The two vocabularies are separate because they answer different questions —
|
|
816
|
+
* a blocker is "what is missing", a card is "what a person sees when they tried
|
|
817
|
+
* anyway" — and this is the one place they are joined, so a blocker can never
|
|
818
|
+
* reach a reader with no card defined for it.
|
|
819
|
+
*/
|
|
820
|
+
function cardForBlocker(blocker) {
|
|
821
|
+
return failureCard(blocker === "credential_inaccessible" ? "credential_unavailable" : blocker === "provider_untested" ? "not_tested" : blocker === "credential_rejected" ? "credential_rejected" : blocker === "provider_unreachable" ? "provider_unreachable" : blocker === "provider_rate_limited" ? "rate_limited" : blocker === "model_unavailable" || blocker === "model_invalid" ? "model_unavailable" : blocker === "policy_forbids" || blocker === "consent_required" ? "policy_forbids" : "not_configured");
|
|
822
|
+
}
|
|
823
|
+
//#endregion
|
|
824
|
+
//#region src/client/readiness.tsx
|
|
825
|
+
const READINESS_STATUS_LABEL = {
|
|
826
|
+
loading: "Loading…",
|
|
827
|
+
ready: "Ready",
|
|
828
|
+
degraded: "Degraded",
|
|
829
|
+
unconfigured: "Not configured",
|
|
830
|
+
unavailable: "Unavailable",
|
|
831
|
+
not_tested: "Not tested",
|
|
832
|
+
error: "Error"
|
|
833
|
+
};
|
|
834
|
+
const READINESS = [
|
|
835
|
+
{
|
|
836
|
+
name: "Watch Core",
|
|
837
|
+
detail: "The engine that mints evidence and issues verdicts. Runs as a child of this workspace.",
|
|
838
|
+
section: "watch-diagnostics"
|
|
839
|
+
},
|
|
840
|
+
{
|
|
841
|
+
name: "Memory",
|
|
842
|
+
detail: "Durable, correctable memory with provenance on every record.",
|
|
843
|
+
section: "watch-memory",
|
|
844
|
+
coreCapabilities: ["watch.memory.recall"]
|
|
845
|
+
},
|
|
846
|
+
{
|
|
847
|
+
name: "Verification",
|
|
848
|
+
detail: "Deterministic checks against the world. Needs no model.",
|
|
849
|
+
section: "watch-verification",
|
|
850
|
+
coreCapabilities: ["watch.verification.run"]
|
|
851
|
+
},
|
|
852
|
+
{
|
|
853
|
+
name: "Browser",
|
|
854
|
+
detail: "A supervised browser that acts and returns Core-owned evidence and a receipt.",
|
|
855
|
+
section: "watch-sources",
|
|
856
|
+
coreCapabilities: [
|
|
857
|
+
"watch.browser.observe",
|
|
858
|
+
"watch.browser.operate",
|
|
859
|
+
"watch.evidence.resolve"
|
|
860
|
+
]
|
|
861
|
+
},
|
|
862
|
+
{
|
|
863
|
+
name: "Agent Model",
|
|
864
|
+
role: "agent_model",
|
|
865
|
+
detail: "Plans, reasons and writes. Any provider DSH supports — DeepSeek is one of them.",
|
|
866
|
+
section: "watch-roles"
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
name: "Visual Perception",
|
|
870
|
+
role: "visual_perception",
|
|
871
|
+
detail: "Reads what is on screen or in a frame. A local model works.",
|
|
872
|
+
section: "watch-roles"
|
|
873
|
+
},
|
|
874
|
+
{
|
|
875
|
+
name: "OCR",
|
|
876
|
+
detail: "Text out of images and pages. A CPU engine is available.",
|
|
877
|
+
section: "watch-engines",
|
|
878
|
+
defaultStatus: "not_tested"
|
|
879
|
+
},
|
|
880
|
+
{
|
|
881
|
+
name: "ASR",
|
|
882
|
+
role: "asr",
|
|
883
|
+
detail: "Speech to text, with timings a citation can point at.",
|
|
884
|
+
section: "watch-roles"
|
|
885
|
+
},
|
|
886
|
+
{
|
|
887
|
+
name: "Audio Understanding",
|
|
888
|
+
role: "audio_understanding",
|
|
889
|
+
detail: "Non-speech audio: events, tone, music.",
|
|
890
|
+
section: "watch-roles"
|
|
891
|
+
},
|
|
892
|
+
{
|
|
893
|
+
name: "Speaker / Diarization",
|
|
894
|
+
detail: "Who spoke, and when.",
|
|
895
|
+
section: "watch-roles",
|
|
896
|
+
defaultStatus: "unconfigured"
|
|
897
|
+
},
|
|
898
|
+
{
|
|
899
|
+
name: "Embeddings / Retrieval",
|
|
900
|
+
role: "embeddings",
|
|
901
|
+
detail: "Search over the library and over memory. Falls back to lexical matching.",
|
|
902
|
+
section: "watch-roles"
|
|
903
|
+
},
|
|
904
|
+
{
|
|
905
|
+
name: "Capture",
|
|
906
|
+
detail: "Screen, window, camera and microphone. Permission is asked at first use.",
|
|
907
|
+
section: "watch-sources",
|
|
908
|
+
defaultStatus: "unconfigured"
|
|
909
|
+
}
|
|
910
|
+
];
|
|
911
|
+
function toneFor$1(status) {
|
|
912
|
+
if (status === "ready") return "active";
|
|
913
|
+
if (status === "degraded" || status === "unavailable" || status === "error") return "caution";
|
|
914
|
+
return "neutral";
|
|
915
|
+
}
|
|
916
|
+
function coreStatus(health, reading) {
|
|
917
|
+
if (reading) return "loading";
|
|
918
|
+
if (health === null || health === void 0) return "error";
|
|
919
|
+
if (health.isTestOnlyMock) return "degraded";
|
|
920
|
+
if (health.blocker === "connected") return health.phase === "ready" ? "ready" : "degraded";
|
|
921
|
+
if (health.blocker === "core_missing" || health.blocker === "bridge_surface_missing") return "unavailable";
|
|
922
|
+
return health.phase === "degraded" ? "degraded" : "error";
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* Core's evidence level, said in the vocabulary a surface may use.
|
|
926
|
+
*
|
|
927
|
+
* The interesting line is `probed`. Core separates three facts on purpose:
|
|
928
|
+
* the code exists, its dependencies were found (`probed`), and a real request
|
|
929
|
+
* ran here and succeeded (`machine_tested`). Only the third is *ready*.
|
|
930
|
+
*
|
|
931
|
+
* `probed` was drawn as **Degraded** for one release, and that was a false
|
|
932
|
+
* alarm in the opposite direction to the green dot: a machine with Chromium
|
|
933
|
+
* installed and a healthy memory index reported two capabilities as impaired
|
|
934
|
+
* when nothing was wrong with either — they simply had not been exercised.
|
|
935
|
+
* Degraded means working and hurt. Ignorance is `not_tested`, which is a
|
|
936
|
+
* neutral word and the accurate one.
|
|
937
|
+
*/
|
|
938
|
+
function capabilityStatus(detail) {
|
|
939
|
+
if (detail === void 0) return "not_tested";
|
|
940
|
+
if (!detail.usable) return detail.status === "unavailable" ? "unavailable" : "degraded";
|
|
941
|
+
if (detail.status === "machine_tested") return "ready";
|
|
942
|
+
if (detail.status === "unavailable") return "unavailable";
|
|
943
|
+
return "not_tested";
|
|
944
|
+
}
|
|
945
|
+
const STATUS_PRIORITY = {
|
|
946
|
+
error: 7,
|
|
947
|
+
unavailable: 6,
|
|
948
|
+
degraded: 5,
|
|
949
|
+
loading: 4,
|
|
950
|
+
not_tested: 3,
|
|
951
|
+
unconfigured: 2,
|
|
952
|
+
ready: 1
|
|
953
|
+
};
|
|
954
|
+
/** Of two states, the one that promises less. */
|
|
955
|
+
function worse(left, right) {
|
|
956
|
+
return STATUS_PRIORITY[right] > STATUS_PRIORITY[left] ? right : left;
|
|
957
|
+
}
|
|
958
|
+
/** Derive every row from runtime facts. Used by both first-run and Diagnostics. */
|
|
959
|
+
function deriveReadiness(facts) {
|
|
960
|
+
const reading = facts.reading === true;
|
|
961
|
+
const health = facts.health;
|
|
962
|
+
const details = new Map((health?.capabilityDetails ?? []).map((item) => [item.capabilityId, item]));
|
|
963
|
+
const roles = new Map((facts.roles ?? []).map((row) => [row.role, row]));
|
|
964
|
+
const core = coreStatus(health, reading);
|
|
965
|
+
return READINESS.map((definition, index) => {
|
|
966
|
+
let status;
|
|
967
|
+
let detail = definition.detail;
|
|
968
|
+
let statusLabel;
|
|
969
|
+
if (index === 0) {
|
|
970
|
+
status = core;
|
|
971
|
+
if (health !== null && health !== void 0 && health.blocker !== "connected" && health.fix !== "") detail = health.fix;
|
|
972
|
+
} else if (definition.coreCapabilities !== void 0) {
|
|
973
|
+
status = definition.coreCapabilities.map((id) => capabilityStatus(details.get(id))).reduce(worse, core);
|
|
974
|
+
const problem = definition.coreCapabilities.map((id) => details.get(id)).find((item) => item !== void 0 && (!item.usable || item.status !== "machine_tested"));
|
|
975
|
+
if (problem !== void 0) detail = problem.fixes[0] ?? (problem.missing.length === 0 ? definition.detail : `Missing: ${problem.missing.join(", ")}.`);
|
|
976
|
+
else if (status !== "ready" && health !== null && health !== void 0 && health.fix !== "") detail = health.fix;
|
|
977
|
+
} else if (definition.role !== void 0) {
|
|
978
|
+
const role = roles.get(definition.role);
|
|
979
|
+
if (role === void 0) status = "unconfigured";
|
|
980
|
+
else if (isExecutable(role.readiness)) status = "ready";
|
|
981
|
+
else if (role.readiness.status === "bound_unverified") status = "not_tested";
|
|
982
|
+
else if (role.readiness.status === "unbound") status = "unconfigured";
|
|
983
|
+
else status = "error";
|
|
984
|
+
if (role !== void 0) {
|
|
985
|
+
statusLabel = BINDING_STATUS_LABEL[role.readiness.status];
|
|
986
|
+
if (!isExecutable(role.readiness)) detail = blockerMessage(role.readiness) ?? detail;
|
|
987
|
+
}
|
|
988
|
+
} else status = definition.defaultStatus ?? "not_tested";
|
|
989
|
+
return {
|
|
990
|
+
...definition,
|
|
991
|
+
detail,
|
|
992
|
+
status,
|
|
993
|
+
statusLabel: statusLabel ?? READINESS_STATUS_LABEL[status],
|
|
994
|
+
tone: toneFor$1(status)
|
|
995
|
+
};
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
function ReadinessList({ openSection, roles, health, reading }) {
|
|
999
|
+
const rows = deriveReadiness({
|
|
1000
|
+
roles,
|
|
1001
|
+
health,
|
|
1002
|
+
reading
|
|
1003
|
+
});
|
|
1004
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1005
|
+
style: {
|
|
1006
|
+
border: "1px solid color-mix(in srgb, var(--watch-accent) 10%, var(--dsw-alias-border-l2))",
|
|
1007
|
+
borderRadius: "14px",
|
|
1008
|
+
overflow: "hidden",
|
|
1009
|
+
background: "linear-gradient(145deg, color-mix(in srgb, var(--watch-accent) 3%, var(--dsw-alias-bg-layer-2)), var(--dsw-alias-bg-base))",
|
|
1010
|
+
boxShadow: "0 10px 28px color-mix(in srgb, black 8%, transparent)"
|
|
1011
|
+
},
|
|
1012
|
+
children: rows.map((item, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1013
|
+
style: {
|
|
1014
|
+
display: "flex",
|
|
1015
|
+
alignItems: "flex-start",
|
|
1016
|
+
gap: "12px",
|
|
1017
|
+
padding: "12px 16px",
|
|
1018
|
+
borderTop: index === 0 ? "none" : "1px solid var(--dsw-alias-border-l2)"
|
|
1019
|
+
},
|
|
1020
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1021
|
+
style: {
|
|
1022
|
+
flex: 1,
|
|
1023
|
+
minWidth: 0
|
|
1024
|
+
},
|
|
1025
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1026
|
+
style: {
|
|
1027
|
+
fontSize: "13px",
|
|
1028
|
+
fontWeight: 500
|
|
1029
|
+
},
|
|
1030
|
+
children: item.role === void 0 ? item.name : ROLE_LABEL[item.role]
|
|
1031
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1032
|
+
style: {
|
|
1033
|
+
fontSize: "12px",
|
|
1034
|
+
lineHeight: 1.5,
|
|
1035
|
+
marginTop: "2px",
|
|
1036
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
1037
|
+
},
|
|
1038
|
+
children: item.detail
|
|
1039
|
+
})]
|
|
1040
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1041
|
+
style: {
|
|
1042
|
+
display: "flex",
|
|
1043
|
+
gap: "8px",
|
|
1044
|
+
alignItems: "center",
|
|
1045
|
+
flexShrink: 0
|
|
1046
|
+
},
|
|
1047
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusChip, {
|
|
1048
|
+
tone: item.tone,
|
|
1049
|
+
children: item.statusLabel
|
|
1050
|
+
}), item.section === void 0 || openSection === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1051
|
+
type: "button",
|
|
1052
|
+
onClick: () => {
|
|
1053
|
+
openSection(item.section);
|
|
1054
|
+
},
|
|
1055
|
+
style: {
|
|
1056
|
+
background: "none",
|
|
1057
|
+
border: "none",
|
|
1058
|
+
cursor: "pointer",
|
|
1059
|
+
fontSize: "11px",
|
|
1060
|
+
padding: "2px 4px",
|
|
1061
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
1062
|
+
textDecoration: "underline"
|
|
1063
|
+
},
|
|
1064
|
+
children: "Configure"
|
|
1065
|
+
})]
|
|
1066
|
+
})]
|
|
1067
|
+
}, item.name))
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
//#endregion
|
|
1071
|
+
//#region src/ocr-measured.ts
|
|
1072
|
+
/** The engine that was run, and where. */
|
|
1073
|
+
const OCR_ENGINE = "rapidocr";
|
|
1074
|
+
/** Per workload, because qualification is per workload and not an average. */
|
|
1075
|
+
const OCR_BY_WORKLOAD = [
|
|
1076
|
+
{
|
|
1077
|
+
"workload": "dark_mode",
|
|
1078
|
+
"samples": 1,
|
|
1079
|
+
"cer": 0,
|
|
1080
|
+
"wordAccuracy": 1,
|
|
1081
|
+
"hallucination": 0,
|
|
1082
|
+
"passes": true
|
|
1083
|
+
},
|
|
1084
|
+
{
|
|
1085
|
+
"workload": "document",
|
|
1086
|
+
"samples": 1,
|
|
1087
|
+
"cer": 0,
|
|
1088
|
+
"wordAccuracy": 1,
|
|
1089
|
+
"hallucination": 0,
|
|
1090
|
+
"passes": true
|
|
1091
|
+
},
|
|
1092
|
+
{
|
|
1093
|
+
"workload": "low_resolution",
|
|
1094
|
+
"samples": 4,
|
|
1095
|
+
"cer": .6111,
|
|
1096
|
+
"wordAccuracy": .25,
|
|
1097
|
+
"hallucination": .25,
|
|
1098
|
+
"passes": false
|
|
1099
|
+
},
|
|
1100
|
+
{
|
|
1101
|
+
"workload": "mixed_script",
|
|
1102
|
+
"samples": 2,
|
|
1103
|
+
"cer": .75,
|
|
1104
|
+
"wordAccuracy": .25,
|
|
1105
|
+
"hallucination": 0,
|
|
1106
|
+
"passes": false
|
|
1107
|
+
},
|
|
1108
|
+
{
|
|
1109
|
+
"workload": "ui_text",
|
|
1110
|
+
"samples": 8,
|
|
1111
|
+
"cer": .0074,
|
|
1112
|
+
"wordAccuracy": .9792,
|
|
1113
|
+
"hallucination": 0,
|
|
1114
|
+
"passes": true
|
|
1115
|
+
},
|
|
1116
|
+
{
|
|
1117
|
+
"workload": "video_overlay",
|
|
1118
|
+
"samples": 1,
|
|
1119
|
+
"cer": 0,
|
|
1120
|
+
"wordAccuracy": 1,
|
|
1121
|
+
"hallucination": 0,
|
|
1122
|
+
"passes": true
|
|
1123
|
+
}
|
|
1124
|
+
];
|
|
1125
|
+
//#endregion
|
|
1126
|
+
//#region src/client/components.tsx
|
|
1127
|
+
/**
|
|
1128
|
+
* The panel vocabulary every surface here shares.
|
|
1129
|
+
*
|
|
1130
|
+
* Exported because Role Bindings moved into its own file once it stopped being
|
|
1131
|
+
* static copy, and two settings screens with independently-invented padding is
|
|
1132
|
+
* how a panel starts looking like two products.
|
|
1133
|
+
*/
|
|
1134
|
+
const T = {
|
|
1135
|
+
page: {
|
|
1136
|
+
padding: "6px 4px 30px",
|
|
1137
|
+
maxWidth: "880px",
|
|
1138
|
+
background: "radial-gradient(circle at 0 0, color-mix(in srgb, var(--watch-accent) 5%, transparent), transparent 34%)"
|
|
1139
|
+
},
|
|
1140
|
+
lead: {
|
|
1141
|
+
fontSize: "13.5px",
|
|
1142
|
+
lineHeight: 1.65,
|
|
1143
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
1144
|
+
margin: "0 0 20px",
|
|
1145
|
+
maxWidth: "72ch"
|
|
1146
|
+
},
|
|
1147
|
+
card: {
|
|
1148
|
+
border: "1px solid color-mix(in srgb, var(--watch-accent) 9%, var(--dsw-alias-border-l2))",
|
|
1149
|
+
borderRadius: "14px",
|
|
1150
|
+
padding: "16px 18px",
|
|
1151
|
+
marginBottom: "12px",
|
|
1152
|
+
background: "linear-gradient(145deg, color-mix(in srgb, var(--watch-accent) 3%, var(--dsw-alias-bg-layer-2)), var(--dsw-alias-bg-base))",
|
|
1153
|
+
boxShadow: "0 10px 28px color-mix(in srgb, black 8%, transparent)"
|
|
1154
|
+
},
|
|
1155
|
+
cardHead: {
|
|
1156
|
+
display: "flex",
|
|
1157
|
+
alignItems: "baseline",
|
|
1158
|
+
gap: "10px",
|
|
1159
|
+
justifyContent: "space-between",
|
|
1160
|
+
flexWrap: "wrap"
|
|
1161
|
+
},
|
|
1162
|
+
title: {
|
|
1163
|
+
fontSize: "14px",
|
|
1164
|
+
fontWeight: 620,
|
|
1165
|
+
margin: 0,
|
|
1166
|
+
letterSpacing: "-0.01em"
|
|
1167
|
+
},
|
|
1168
|
+
meta: {
|
|
1169
|
+
display: "grid",
|
|
1170
|
+
gridTemplateColumns: "max-content 1fr",
|
|
1171
|
+
columnGap: "14px",
|
|
1172
|
+
rowGap: "4px",
|
|
1173
|
+
fontSize: "12px",
|
|
1174
|
+
marginTop: "10px"
|
|
1175
|
+
},
|
|
1176
|
+
key: { color: "var(--dsw-alias-label-tertiary)" },
|
|
1177
|
+
value: { color: "var(--dsw-alias-label-secondary)" },
|
|
1178
|
+
note: {
|
|
1179
|
+
fontSize: "12px",
|
|
1180
|
+
lineHeight: 1.55,
|
|
1181
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
1182
|
+
borderInlineStart: "2px solid var(--watch-accent)",
|
|
1183
|
+
paddingInlineStart: "10px",
|
|
1184
|
+
margin: "14px 0 0"
|
|
1185
|
+
},
|
|
1186
|
+
h2: {
|
|
1187
|
+
fontSize: "11px",
|
|
1188
|
+
fontWeight: 700,
|
|
1189
|
+
letterSpacing: "0.11em",
|
|
1190
|
+
textTransform: "uppercase",
|
|
1191
|
+
color: "var(--watch-accent)",
|
|
1192
|
+
margin: "24px 0 9px"
|
|
1193
|
+
}
|
|
1194
|
+
};
|
|
1195
|
+
/**
|
|
1196
|
+
* A status chip.
|
|
1197
|
+
*
|
|
1198
|
+
* The tone vocabulary is the brand's, and `success` is deliberately absent:
|
|
1199
|
+
* nothing in a settings page is a verification verdict, so nothing here is
|
|
1200
|
+
* allowed to be green. A capability that is genuinely working reads as
|
|
1201
|
+
* `active`, which is a different colour and a different claim.
|
|
1202
|
+
*/
|
|
1203
|
+
function StatusChip({ tone, children }) {
|
|
1204
|
+
const colour = tokenFor(tone);
|
|
1205
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1206
|
+
style: {
|
|
1207
|
+
display: "inline-flex",
|
|
1208
|
+
alignItems: "center",
|
|
1209
|
+
gap: "5px",
|
|
1210
|
+
fontSize: "11px",
|
|
1211
|
+
lineHeight: 1.4,
|
|
1212
|
+
padding: "2px 8px",
|
|
1213
|
+
borderRadius: "999px",
|
|
1214
|
+
whiteSpace: "nowrap",
|
|
1215
|
+
border: `1px solid color-mix(in srgb, ${colour} 60%, transparent)`,
|
|
1216
|
+
color: colour,
|
|
1217
|
+
background: `color-mix(in srgb, ${colour} 8%, transparent)`
|
|
1218
|
+
},
|
|
1219
|
+
children
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
/** A local/remote label, because where the data goes is a product fact. */
|
|
1223
|
+
function Where({ local }) {
|
|
1224
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusChip, {
|
|
1225
|
+
tone: local ? "active" : "info",
|
|
1226
|
+
children: local ? "Local" : "Remote"
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
function Row({ label, children }) {
|
|
1230
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1231
|
+
style: T.key,
|
|
1232
|
+
children: label
|
|
1233
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1234
|
+
style: T.value,
|
|
1235
|
+
children
|
|
1236
|
+
})] });
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* The empty state every surface here needs.
|
|
1240
|
+
*
|
|
1241
|
+
* A capability that is not configured must still render something a person can
|
|
1242
|
+
* act on. A dead control that fails when clicked teaches people the product is
|
|
1243
|
+
* broken; a sentence explaining what is missing and what would fix it does not.
|
|
1244
|
+
*/
|
|
1245
|
+
function NotConfigured({ what, why, fix }) {
|
|
1246
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1247
|
+
style: {
|
|
1248
|
+
...T.card,
|
|
1249
|
+
borderStyle: "dashed"
|
|
1250
|
+
},
|
|
1251
|
+
children: [
|
|
1252
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1253
|
+
style: T.cardHead,
|
|
1254
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
1255
|
+
style: T.title,
|
|
1256
|
+
children: what
|
|
1257
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusChip, {
|
|
1258
|
+
tone: "neutral",
|
|
1259
|
+
children: "Not configured"
|
|
1260
|
+
})]
|
|
1261
|
+
}),
|
|
1262
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1263
|
+
style: {
|
|
1264
|
+
...T.lead,
|
|
1265
|
+
margin: "8px 0 0"
|
|
1266
|
+
},
|
|
1267
|
+
children: why
|
|
1268
|
+
}),
|
|
1269
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1270
|
+
style: {
|
|
1271
|
+
...T.note,
|
|
1272
|
+
marginTop: "10px"
|
|
1273
|
+
},
|
|
1274
|
+
children: fix
|
|
1275
|
+
})
|
|
1276
|
+
]
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
/**
|
|
1280
|
+
* Lifecycle words a person can act on, and the tone each earns.
|
|
1281
|
+
*
|
|
1282
|
+
* `not_tested` is the state every engine is in on a machine where no capability
|
|
1283
|
+
* check has run, and it is what `untestedHealth` returns. It is deliberately
|
|
1284
|
+
* not styled as an error: nothing is broken, nobody has looked.
|
|
1285
|
+
*/
|
|
1286
|
+
function lifecycleChip(state) {
|
|
1287
|
+
const entry = {
|
|
1288
|
+
machine_tested: {
|
|
1289
|
+
tone: "active",
|
|
1290
|
+
text: "Machine tested"
|
|
1291
|
+
},
|
|
1292
|
+
ready: {
|
|
1293
|
+
tone: "active",
|
|
1294
|
+
text: "Ready"
|
|
1295
|
+
},
|
|
1296
|
+
probed: {
|
|
1297
|
+
tone: "caution",
|
|
1298
|
+
text: "Probed, not measured"
|
|
1299
|
+
},
|
|
1300
|
+
installed: {
|
|
1301
|
+
tone: "caution",
|
|
1302
|
+
text: "Installed, not tested"
|
|
1303
|
+
},
|
|
1304
|
+
discovered: {
|
|
1305
|
+
tone: "neutral",
|
|
1306
|
+
text: "Not tested"
|
|
1307
|
+
},
|
|
1308
|
+
not_tested: {
|
|
1309
|
+
tone: "neutral",
|
|
1310
|
+
text: "Not tested"
|
|
1311
|
+
},
|
|
1312
|
+
not_installed: {
|
|
1313
|
+
tone: "neutral",
|
|
1314
|
+
text: "Not installed"
|
|
1315
|
+
},
|
|
1316
|
+
installing: {
|
|
1317
|
+
tone: "caution",
|
|
1318
|
+
text: "Installing"
|
|
1319
|
+
},
|
|
1320
|
+
degraded: {
|
|
1321
|
+
tone: "error",
|
|
1322
|
+
text: "Degraded"
|
|
1323
|
+
},
|
|
1324
|
+
unavailable: {
|
|
1325
|
+
tone: "error",
|
|
1326
|
+
text: "Unavailable"
|
|
1327
|
+
},
|
|
1328
|
+
incompatible: {
|
|
1329
|
+
tone: "error",
|
|
1330
|
+
text: "Incompatible"
|
|
1331
|
+
},
|
|
1332
|
+
disabled: {
|
|
1333
|
+
tone: "neutral",
|
|
1334
|
+
text: "Disabled"
|
|
1335
|
+
}
|
|
1336
|
+
}[state] ?? {
|
|
1337
|
+
tone: "neutral",
|
|
1338
|
+
text: state
|
|
1339
|
+
};
|
|
1340
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusChip, {
|
|
1341
|
+
tone: entry.tone,
|
|
1342
|
+
children: entry.text
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
function gigabytes(bytes) {
|
|
1346
|
+
if (bytes === null) return "unknown";
|
|
1347
|
+
return `${(bytes / 1e9).toFixed(1)} GB`;
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* Perception Engines.
|
|
1351
|
+
*
|
|
1352
|
+
* An Engine Runtime is not a Provider Connection, and the distinction is the
|
|
1353
|
+
* whole point of the screen: a provider is a credential and an endpoint, an
|
|
1354
|
+
* engine is software that runs here. Presence on disk is not readiness, so the
|
|
1355
|
+
* lifecycle state is shown verbatim rather than collapsed into a tick.
|
|
1356
|
+
*
|
|
1357
|
+
* No engine shows a quality or speed number. On a machine where nothing has
|
|
1358
|
+
* been measured, every such number would be invented.
|
|
1359
|
+
*/
|
|
1360
|
+
function EnginesSection() {
|
|
1361
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1362
|
+
style: T.page,
|
|
1363
|
+
children: [
|
|
1364
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1365
|
+
style: T.lead,
|
|
1366
|
+
children: "An engine runs on this machine; a provider is something you connect to. Presence on disk is not readiness, so each engine shows the furthest state actually reached. Nothing here shows an accuracy or speed figure: none has been measured on this hardware, and a figure that was not measured would be fiction."
|
|
1367
|
+
}),
|
|
1368
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
1369
|
+
style: T.h2,
|
|
1370
|
+
children: "OCR and layout"
|
|
1371
|
+
}),
|
|
1372
|
+
OCR_ENGINES.map((engine) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1373
|
+
style: T.card,
|
|
1374
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1375
|
+
style: T.cardHead,
|
|
1376
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
1377
|
+
style: T.title,
|
|
1378
|
+
children: engine.displayName
|
|
1379
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1380
|
+
style: {
|
|
1381
|
+
display: "flex",
|
|
1382
|
+
gap: "6px"
|
|
1383
|
+
},
|
|
1384
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Where, { local: engine.runtime !== "remote" }), lifecycleChip("not_tested")]
|
|
1385
|
+
})]
|
|
1386
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1387
|
+
style: T.meta,
|
|
1388
|
+
children: [
|
|
1389
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1390
|
+
label: "Version",
|
|
1391
|
+
children: engine.version
|
|
1392
|
+
}),
|
|
1393
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1394
|
+
label: "Runtime",
|
|
1395
|
+
children: engine.runtime
|
|
1396
|
+
}),
|
|
1397
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1398
|
+
label: "Hardware",
|
|
1399
|
+
children: engine.hardware.gpu === "required" ? `GPU required${engine.hardware.minVramGb === null ? "" : `, ${String(engine.hardware.minVramGb)} GB VRAM`}` : engine.hardware.gpu === "optional" ? "GPU optional" : "CPU only"
|
|
1400
|
+
}),
|
|
1401
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1402
|
+
label: "Egress",
|
|
1403
|
+
children: engine.privacy.egress === "none" ? "Nothing leaves this machine" : engine.privacy.egress === "metadata_only" ? "Metadata only" : "Sends content off this machine"
|
|
1404
|
+
}),
|
|
1405
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1406
|
+
label: "Offline",
|
|
1407
|
+
children: engine.privacy.worksOffline ? "Works offline" : "Requires network"
|
|
1408
|
+
}),
|
|
1409
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Row, {
|
|
1410
|
+
label: "Install",
|
|
1411
|
+
children: [engine.install.method === "bundled" ? "Bundled" : engine.install.method, engine.install.downloadBytes === null ? "" : ` · ${gigabytes(engine.install.downloadBytes)} download`]
|
|
1412
|
+
}),
|
|
1413
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1414
|
+
label: "Quality",
|
|
1415
|
+
children: engine.id.includes("rapidocr") ? "Measured on this machine — see the table below." : "Not measured on this machine"
|
|
1416
|
+
}),
|
|
1417
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1418
|
+
label: "How it would be checked",
|
|
1419
|
+
children: engine.testMethod ?? engine.probeMethod ?? "No check is defined for this engine"
|
|
1420
|
+
})
|
|
1421
|
+
]
|
|
1422
|
+
})]
|
|
1423
|
+
}, engine.id)),
|
|
1424
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
1425
|
+
style: T.h2,
|
|
1426
|
+
children: `Measured accuracy — ${OCR_ENGINE} on cpu`
|
|
1427
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1428
|
+
style: T.card,
|
|
1429
|
+
children: [
|
|
1430
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1431
|
+
style: {
|
|
1432
|
+
...T.lead,
|
|
1433
|
+
margin: "0 0 10px"
|
|
1434
|
+
},
|
|
1435
|
+
children: "A real run over a versioned ground-truth corpus, scored against thresholds committed before the benchmark existed. Reported per workload rather than as one average: an engine can be entirely fit for reading a settings panel and entirely unfit for reading grey-on-grey, and a single number hides both."
|
|
1436
|
+
}),
|
|
1437
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1438
|
+
style: T.meta,
|
|
1439
|
+
children: OCR_BY_WORKLOAD.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Row, {
|
|
1440
|
+
label: row.workload,
|
|
1441
|
+
children: [
|
|
1442
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusChip, {
|
|
1443
|
+
tone: row.passes ? "active" : "error",
|
|
1444
|
+
children: row.passes ? "Qualified" : "Not qualified"
|
|
1445
|
+
}),
|
|
1446
|
+
` CER ${String(row.cer)} · word accuracy ${String(row.wordAccuracy)} `,
|
|
1447
|
+
`· invented words ${String(row.hallucination)} · ${String(row.samples)} sample(s)`
|
|
1448
|
+
]
|
|
1449
|
+
}, row.workload))
|
|
1450
|
+
}),
|
|
1451
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1452
|
+
style: T.note,
|
|
1453
|
+
children: "Accuracy is not a GPU question and is measured here. GPU throughput is not, because there is no GPU on this machine — that remains externally unvalidated."
|
|
1454
|
+
})
|
|
1455
|
+
]
|
|
1456
|
+
})] }),
|
|
1457
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1458
|
+
style: T.note,
|
|
1459
|
+
children: "An install is never automatic. Where a download is required, its size, hardware requirement and licence are shown before anything is fetched — multi-gigabyte weights are not something a product should acquire because a page was opened."
|
|
1460
|
+
})
|
|
1461
|
+
]
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
const SOURCES = [
|
|
1465
|
+
{
|
|
1466
|
+
id: "files",
|
|
1467
|
+
name: "Files",
|
|
1468
|
+
purpose: "Documents and recordings you open.",
|
|
1469
|
+
local: true,
|
|
1470
|
+
permission: "Granted per file, when you pick one"
|
|
1471
|
+
},
|
|
1472
|
+
{
|
|
1473
|
+
id: "video",
|
|
1474
|
+
name: "Video",
|
|
1475
|
+
purpose: "Recorded footage, indexed so a citation can point at a moment.",
|
|
1476
|
+
local: true,
|
|
1477
|
+
permission: "Granted per file"
|
|
1478
|
+
},
|
|
1479
|
+
{
|
|
1480
|
+
id: "browser",
|
|
1481
|
+
name: "Browser",
|
|
1482
|
+
purpose: "A supervised browser that acts and reports a receipt.",
|
|
1483
|
+
local: true,
|
|
1484
|
+
permission: "No OS permission needed"
|
|
1485
|
+
},
|
|
1486
|
+
{
|
|
1487
|
+
id: "screen",
|
|
1488
|
+
name: "Screen",
|
|
1489
|
+
purpose: "The whole display.",
|
|
1490
|
+
local: true,
|
|
1491
|
+
permission: "Requested at first use"
|
|
1492
|
+
},
|
|
1493
|
+
{
|
|
1494
|
+
id: "window",
|
|
1495
|
+
name: "Window",
|
|
1496
|
+
purpose: "One application window.",
|
|
1497
|
+
local: true,
|
|
1498
|
+
permission: "Requested at first use"
|
|
1499
|
+
},
|
|
1500
|
+
{
|
|
1501
|
+
id: "camera",
|
|
1502
|
+
name: "Camera",
|
|
1503
|
+
purpose: "Live visual input.",
|
|
1504
|
+
local: true,
|
|
1505
|
+
permission: "Requested at first use"
|
|
1506
|
+
},
|
|
1507
|
+
{
|
|
1508
|
+
id: "microphone",
|
|
1509
|
+
name: "Microphone",
|
|
1510
|
+
purpose: "Live audio input.",
|
|
1511
|
+
local: true,
|
|
1512
|
+
permission: "Requested at first use"
|
|
1513
|
+
},
|
|
1514
|
+
{
|
|
1515
|
+
id: "live",
|
|
1516
|
+
name: "Live session",
|
|
1517
|
+
purpose: "A continuous session over one or more of the above.",
|
|
1518
|
+
local: true,
|
|
1519
|
+
permission: "Inherits its sources"
|
|
1520
|
+
}
|
|
1521
|
+
];
|
|
1522
|
+
/**
|
|
1523
|
+
* Sources & Devices.
|
|
1524
|
+
*
|
|
1525
|
+
* Nothing here asks the operating system for anything. A permission prompt on
|
|
1526
|
+
* page load trains people to click Allow without reading, so a permission is
|
|
1527
|
+
* requested when a capability is first used and not before — which is also why
|
|
1528
|
+
* every row says when it would ask.
|
|
1529
|
+
*/
|
|
1530
|
+
function SourcesSection() {
|
|
1531
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1532
|
+
style: T.page,
|
|
1533
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1534
|
+
style: T.lead,
|
|
1535
|
+
children: "Opening this page requests nothing. A permission is asked for when a capability is first used, because a prompt on load teaches people to allow without reading. Every source below is local: what it captures stays on this machine unless a separate media-upload consent is given."
|
|
1536
|
+
}), SOURCES.map((source) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1537
|
+
style: T.card,
|
|
1538
|
+
children: [
|
|
1539
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1540
|
+
style: T.cardHead,
|
|
1541
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
1542
|
+
style: T.title,
|
|
1543
|
+
children: source.name
|
|
1544
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1545
|
+
style: {
|
|
1546
|
+
display: "flex",
|
|
1547
|
+
gap: "6px"
|
|
1548
|
+
},
|
|
1549
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Where, { local: source.local }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusChip, {
|
|
1550
|
+
tone: "neutral",
|
|
1551
|
+
children: "Not requested"
|
|
1552
|
+
})]
|
|
1553
|
+
})]
|
|
1554
|
+
}),
|
|
1555
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1556
|
+
style: {
|
|
1557
|
+
...T.lead,
|
|
1558
|
+
margin: "6px 0 0"
|
|
1559
|
+
},
|
|
1560
|
+
children: source.purpose
|
|
1561
|
+
}),
|
|
1562
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1563
|
+
style: T.meta,
|
|
1564
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1565
|
+
label: "Permission",
|
|
1566
|
+
children: source.permission
|
|
1567
|
+
})
|
|
1568
|
+
})
|
|
1569
|
+
]
|
|
1570
|
+
}, source.id))]
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
const MEMORY_MODES = [
|
|
1574
|
+
{
|
|
1575
|
+
id: "off",
|
|
1576
|
+
name: "Off",
|
|
1577
|
+
detail: "Nothing is written and nothing is recalled."
|
|
1578
|
+
},
|
|
1579
|
+
{
|
|
1580
|
+
id: "session_only",
|
|
1581
|
+
name: "Session only",
|
|
1582
|
+
detail: "Kept for this session and never reaches a later one."
|
|
1583
|
+
},
|
|
1584
|
+
{
|
|
1585
|
+
id: "local_personal",
|
|
1586
|
+
name: "Local Personal",
|
|
1587
|
+
detail: "A ledger on this machine, for this profile."
|
|
1588
|
+
},
|
|
1589
|
+
{
|
|
1590
|
+
id: "workspace_shared",
|
|
1591
|
+
name: "Workspace Shared",
|
|
1592
|
+
detail: "Knowledge and decisions are shared; personal taste is not."
|
|
1593
|
+
}
|
|
1594
|
+
];
|
|
1595
|
+
/**
|
|
1596
|
+
* Memory & Retrieval.
|
|
1597
|
+
*
|
|
1598
|
+
* The encryption row is the one that matters. The ledger is a plain file with
|
|
1599
|
+
* the profile's permissions, and saying so is the difference between a product
|
|
1600
|
+
* a person can calibrate their trust against and one that misleads them about
|
|
1601
|
+
* where their data sits.
|
|
1602
|
+
*/
|
|
1603
|
+
function MemorySection() {
|
|
1604
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1605
|
+
style: T.page,
|
|
1606
|
+
children: [
|
|
1607
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1608
|
+
style: T.lead,
|
|
1609
|
+
children: "Memory is a product capability, not a plugin. The ledger is the authority and every projection — taste, index, log — is rebuilt from it, which is what makes Forget remove a record rather than hide it."
|
|
1610
|
+
}),
|
|
1611
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
1612
|
+
style: T.h2,
|
|
1613
|
+
children: "Mode"
|
|
1614
|
+
}),
|
|
1615
|
+
MEMORY_MODES.map((mode) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1616
|
+
style: T.card,
|
|
1617
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1618
|
+
style: T.cardHead,
|
|
1619
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
1620
|
+
style: T.title,
|
|
1621
|
+
children: mode.name
|
|
1622
|
+
}), mode.id === "local_personal" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusChip, {
|
|
1623
|
+
tone: "active",
|
|
1624
|
+
children: "Selected in this profile"
|
|
1625
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusChip, {
|
|
1626
|
+
tone: "neutral",
|
|
1627
|
+
children: "Available"
|
|
1628
|
+
})]
|
|
1629
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1630
|
+
style: {
|
|
1631
|
+
...T.lead,
|
|
1632
|
+
margin: "6px 0 0"
|
|
1633
|
+
},
|
|
1634
|
+
children: mode.detail
|
|
1635
|
+
})]
|
|
1636
|
+
}, mode.id)),
|
|
1637
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
1638
|
+
style: T.h2,
|
|
1639
|
+
children: "Storage"
|
|
1640
|
+
}),
|
|
1641
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1642
|
+
style: T.card,
|
|
1643
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1644
|
+
style: T.meta,
|
|
1645
|
+
children: [
|
|
1646
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1647
|
+
label: "Ledger",
|
|
1648
|
+
children: "An append-only event log on this machine"
|
|
1649
|
+
}),
|
|
1650
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1651
|
+
label: "Projections",
|
|
1652
|
+
children: "Rebuilt from the ledger, never edited in place"
|
|
1653
|
+
}),
|
|
1654
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1655
|
+
label: "Embeddings",
|
|
1656
|
+
children: "Unbound — see Role Bindings. Retrieval falls back to lexical matching."
|
|
1657
|
+
}),
|
|
1658
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1659
|
+
label: "Retention",
|
|
1660
|
+
children: "Kept until forgotten. Forget writes a tombstone."
|
|
1661
|
+
}),
|
|
1662
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1663
|
+
label: "Encryption at rest",
|
|
1664
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusChip, {
|
|
1665
|
+
tone: "caution",
|
|
1666
|
+
children: "Not encrypted"
|
|
1667
|
+
})
|
|
1668
|
+
})
|
|
1669
|
+
]
|
|
1670
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1671
|
+
style: T.note,
|
|
1672
|
+
children: "The ledger is a plain file. It is created owner-only — no group, no others — on systems that enforce file permissions; Windows has no equivalent and the file inherits the folder it sits in. On Desktop it is intended to move behind the OS keychain; until that is implemented and tested, this page will keep saying it is not encrypted. Claiming otherwise would be the one thing a privacy setting must never do."
|
|
1673
|
+
})]
|
|
1674
|
+
})
|
|
1675
|
+
]
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
/**
|
|
1679
|
+
* Verification.
|
|
1680
|
+
*
|
|
1681
|
+
* The screen leads with the distinction the whole product rests on, because
|
|
1682
|
+
* this is where somebody configures how much proof they want and needs to know
|
|
1683
|
+
* what a verdict does and does not mean.
|
|
1684
|
+
*/
|
|
1685
|
+
function VerificationSection() {
|
|
1686
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1687
|
+
style: T.page,
|
|
1688
|
+
children: [
|
|
1689
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1690
|
+
style: {
|
|
1691
|
+
...T.card,
|
|
1692
|
+
borderColor: "var(--watch-accent)"
|
|
1693
|
+
},
|
|
1694
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
1695
|
+
style: T.title,
|
|
1696
|
+
children: "Agent completed ≠ Verified"
|
|
1697
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1698
|
+
style: {
|
|
1699
|
+
...T.lead,
|
|
1700
|
+
margin: "8px 0 0"
|
|
1701
|
+
},
|
|
1702
|
+
children: "A tool returning without an error means the call finished. It does not mean the thing happened. Only a verification against the world produces a verdict, and only Watch Core produces one — no plugin, no client and no model can mint a verdict, by construction rather than by convention."
|
|
1703
|
+
})]
|
|
1704
|
+
}),
|
|
1705
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
1706
|
+
style: T.h2,
|
|
1707
|
+
children: "Verifier"
|
|
1708
|
+
}),
|
|
1709
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1710
|
+
style: T.card,
|
|
1711
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1712
|
+
style: T.cardHead,
|
|
1713
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
1714
|
+
style: T.title,
|
|
1715
|
+
children: "Deterministic checks"
|
|
1716
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1717
|
+
style: {
|
|
1718
|
+
display: "flex",
|
|
1719
|
+
gap: "6px"
|
|
1720
|
+
},
|
|
1721
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Where, { local: true }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusChip, {
|
|
1722
|
+
tone: "active",
|
|
1723
|
+
children: "Available"
|
|
1724
|
+
})]
|
|
1725
|
+
})]
|
|
1726
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1727
|
+
style: {
|
|
1728
|
+
...T.lead,
|
|
1729
|
+
margin: "6px 0 0"
|
|
1730
|
+
},
|
|
1731
|
+
children: "Reads a row, an HTTP status, a file, an exit code. Needs no model, so it works offline and its result does not depend on a provider."
|
|
1732
|
+
})]
|
|
1733
|
+
}),
|
|
1734
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1735
|
+
style: T.card,
|
|
1736
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1737
|
+
style: T.cardHead,
|
|
1738
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
1739
|
+
style: T.title,
|
|
1740
|
+
children: "Model-assisted checks"
|
|
1741
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusChip, {
|
|
1742
|
+
tone: "neutral",
|
|
1743
|
+
children: "Verifier role not bound"
|
|
1744
|
+
})]
|
|
1745
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1746
|
+
style: {
|
|
1747
|
+
...T.lead,
|
|
1748
|
+
margin: "6px 0 0"
|
|
1749
|
+
},
|
|
1750
|
+
children: "For expectations a deterministic check cannot express. Bind the Verifier role to enable them; until then, expectations that need one return INCONCLUSIVE rather than a guess."
|
|
1751
|
+
})]
|
|
1752
|
+
}),
|
|
1753
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
1754
|
+
style: T.h2,
|
|
1755
|
+
children: "Verdicts"
|
|
1756
|
+
}),
|
|
1757
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1758
|
+
style: T.card,
|
|
1759
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1760
|
+
style: T.meta,
|
|
1761
|
+
children: [
|
|
1762
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1763
|
+
label: "VERIFIED",
|
|
1764
|
+
children: "Checked against the world, and it held."
|
|
1765
|
+
}),
|
|
1766
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1767
|
+
label: "FAILED",
|
|
1768
|
+
children: "Checked, and it did not hold."
|
|
1769
|
+
}),
|
|
1770
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1771
|
+
label: "UNVERIFIED",
|
|
1772
|
+
children: "Not checked. Not a failure — an absence."
|
|
1773
|
+
}),
|
|
1774
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1775
|
+
label: "INCONCLUSIVE",
|
|
1776
|
+
children: "Checked, and the evidence did not settle it."
|
|
1777
|
+
})
|
|
1778
|
+
]
|
|
1779
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1780
|
+
style: T.note,
|
|
1781
|
+
children: "Green is reserved for VERIFIED and nothing else reaches it — not a high confidence, not a completed turn, not five checks out of six."
|
|
1782
|
+
})]
|
|
1783
|
+
})
|
|
1784
|
+
]
|
|
1785
|
+
});
|
|
1786
|
+
}
|
|
1787
|
+
/**
|
|
1788
|
+
* What the engine is doing, in one chip.
|
|
1789
|
+
*
|
|
1790
|
+
* `connected` is the only state that gets the active tone. Everything else —
|
|
1791
|
+
* including the mock backend, which is *working* and is still not the product
|
|
1792
|
+
* — reads as a problem, because on this screen it is one.
|
|
1793
|
+
*/
|
|
1794
|
+
function CoreStateChip({ health, reading }) {
|
|
1795
|
+
if (health === null || health === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusChip, {
|
|
1796
|
+
tone: "neutral",
|
|
1797
|
+
children: reading ? "Reading…" : "Could not be read"
|
|
1798
|
+
});
|
|
1799
|
+
if (health.blocker === "connected") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusChip, {
|
|
1800
|
+
tone: "active",
|
|
1801
|
+
children: "Connected"
|
|
1802
|
+
});
|
|
1803
|
+
if (health.isTestOnlyMock) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusChip, {
|
|
1804
|
+
tone: "caution",
|
|
1805
|
+
children: "Test-only mock"
|
|
1806
|
+
});
|
|
1807
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusChip, {
|
|
1808
|
+
tone: "caution",
|
|
1809
|
+
children: health.phase
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
/**
|
|
1813
|
+
* Diagnostics. What is actually running, and what is not.
|
|
1814
|
+
*
|
|
1815
|
+
* The capability readiness list lives here rather than in the first-run notice.
|
|
1816
|
+
* It needs the settings panel's width; the onboarding seat is 256 pixels wide,
|
|
1817
|
+
* and putting this there once already spilled two thousand pixels out of a
|
|
1818
|
+
* clipped sidebar column.
|
|
1819
|
+
*/
|
|
1820
|
+
function DiagnosticsSection({ openSection, roles, health, reading, onRefresh } = {}) {
|
|
1821
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1822
|
+
style: T.page,
|
|
1823
|
+
children: [
|
|
1824
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1825
|
+
style: T.lead,
|
|
1826
|
+
children: "What this installation actually consists of. Where a value cannot be read from the running system it says so, rather than showing a plausible default."
|
|
1827
|
+
}),
|
|
1828
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
1829
|
+
style: T.h2,
|
|
1830
|
+
children: "Capability readiness"
|
|
1831
|
+
}),
|
|
1832
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReadinessList, {
|
|
1833
|
+
openSection,
|
|
1834
|
+
roles,
|
|
1835
|
+
health,
|
|
1836
|
+
reading
|
|
1837
|
+
}),
|
|
1838
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
1839
|
+
style: T.h2,
|
|
1840
|
+
children: "Versions"
|
|
1841
|
+
}),
|
|
1842
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1843
|
+
style: T.card,
|
|
1844
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1845
|
+
style: T.meta,
|
|
1846
|
+
children: [
|
|
1847
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1848
|
+
label: "DeepWatch",
|
|
1849
|
+
children: "0.1.0"
|
|
1850
|
+
}),
|
|
1851
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1852
|
+
label: "DeepSeek Harness",
|
|
1853
|
+
children: "0.1.1-rc.2"
|
|
1854
|
+
}),
|
|
1855
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1856
|
+
label: "Watch Core",
|
|
1857
|
+
children: health === null || health === void 0 || health.coreVersion === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusChip, {
|
|
1858
|
+
tone: "neutral",
|
|
1859
|
+
children: "Not reported"
|
|
1860
|
+
}) : health.coreVersion
|
|
1861
|
+
}),
|
|
1862
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1863
|
+
label: "Bridge protocol",
|
|
1864
|
+
children: health === null || health === void 0 || health.protocolVersion === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusChip, {
|
|
1865
|
+
tone: "neutral",
|
|
1866
|
+
children: "Not reported"
|
|
1867
|
+
}) : `${String(health.protocolVersion)} (Core supports ${health.protocolMin === null ? "?" : String(health.protocolMin)}-${health.protocolVersion === null ? "?" : String(health.protocolVersion)})`
|
|
1868
|
+
}),
|
|
1869
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1870
|
+
label: "Memory store schema",
|
|
1871
|
+
children: "1"
|
|
1872
|
+
})
|
|
1873
|
+
]
|
|
1874
|
+
})
|
|
1875
|
+
}),
|
|
1876
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
1877
|
+
style: T.h2,
|
|
1878
|
+
children: "Health"
|
|
1879
|
+
}),
|
|
1880
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1881
|
+
style: T.card,
|
|
1882
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1883
|
+
style: T.meta,
|
|
1884
|
+
children: [
|
|
1885
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1886
|
+
label: "Watch Core",
|
|
1887
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1888
|
+
style: {
|
|
1889
|
+
display: "inline-flex",
|
|
1890
|
+
alignItems: "center",
|
|
1891
|
+
gap: "8px"
|
|
1892
|
+
},
|
|
1893
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CoreStateChip, {
|
|
1894
|
+
health,
|
|
1895
|
+
reading: reading === true
|
|
1896
|
+
}), onRefresh === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1897
|
+
type: "button",
|
|
1898
|
+
onClick: onRefresh,
|
|
1899
|
+
disabled: reading === true,
|
|
1900
|
+
style: {
|
|
1901
|
+
background: "none",
|
|
1902
|
+
border: "none",
|
|
1903
|
+
padding: 0,
|
|
1904
|
+
font: "inherit",
|
|
1905
|
+
fontSize: "12px",
|
|
1906
|
+
cursor: "pointer",
|
|
1907
|
+
color: tokenFor("info"),
|
|
1908
|
+
textDecoration: "underline"
|
|
1909
|
+
},
|
|
1910
|
+
children: "Re-read"
|
|
1911
|
+
})]
|
|
1912
|
+
})
|
|
1913
|
+
}),
|
|
1914
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1915
|
+
label: "Bridge transport",
|
|
1916
|
+
children: health === null || health === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusChip, {
|
|
1917
|
+
tone: "neutral",
|
|
1918
|
+
children: "Not reported"
|
|
1919
|
+
}) : health.isTestOnlyMock ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusChip, {
|
|
1920
|
+
tone: "caution",
|
|
1921
|
+
children: "Test-only mock backend"
|
|
1922
|
+
}) : health.transport === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusChip, {
|
|
1923
|
+
tone: "neutral",
|
|
1924
|
+
children: "Not reported"
|
|
1925
|
+
}) : health.transport
|
|
1926
|
+
}),
|
|
1927
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1928
|
+
label: "Contract",
|
|
1929
|
+
children: health === null || health === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusChip, {
|
|
1930
|
+
tone: "neutral",
|
|
1931
|
+
children: "Not reported"
|
|
1932
|
+
}) : health.contractsMatch ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusChip, {
|
|
1933
|
+
tone: "active",
|
|
1934
|
+
children: "Matches this build"
|
|
1935
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusChip, {
|
|
1936
|
+
tone: "caution",
|
|
1937
|
+
children: health.contractDrift.length === 0 ? "Unverified" : `Drifted: ${health.contractDrift.join(", ")}`
|
|
1938
|
+
})
|
|
1939
|
+
}),
|
|
1940
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1941
|
+
label: "Capabilities",
|
|
1942
|
+
children: health === null || health === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusChip, {
|
|
1943
|
+
tone: "neutral",
|
|
1944
|
+
children: "Not reported"
|
|
1945
|
+
}) : `${String(health.capabilities.ready)} ready · ${String(health.capabilities.degraded)} degraded · ${String(health.capabilities.unavailable)} unavailable · ${String(health.capabilities.unknown)} unknown`
|
|
1946
|
+
}),
|
|
1947
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1948
|
+
label: "Last handshake",
|
|
1949
|
+
children: health === null || health === void 0 || health.lastHandshakeAt === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusChip, {
|
|
1950
|
+
tone: "neutral",
|
|
1951
|
+
children: "Never"
|
|
1952
|
+
}) : health.lastHandshakeAt
|
|
1953
|
+
}),
|
|
1954
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1955
|
+
label: "Engine starts",
|
|
1956
|
+
children: health === null || health === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusChip, {
|
|
1957
|
+
tone: "neutral",
|
|
1958
|
+
children: "Not reported"
|
|
1959
|
+
}) : String(health.restartCount)
|
|
1960
|
+
}),
|
|
1961
|
+
health !== null && health !== void 0 && health.blocker !== "connected" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1962
|
+
label: "Blocker",
|
|
1963
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusChip, {
|
|
1964
|
+
tone: "caution",
|
|
1965
|
+
children: health.blocker
|
|
1966
|
+
}), health.fix === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1967
|
+
style: T.note,
|
|
1968
|
+
children: [" ", health.fix]
|
|
1969
|
+
})] })
|
|
1970
|
+
}) : null,
|
|
1971
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1972
|
+
label: "Offline",
|
|
1973
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusChip, {
|
|
1974
|
+
tone: "active",
|
|
1975
|
+
children: "Offline only"
|
|
1976
|
+
})
|
|
1977
|
+
}),
|
|
1978
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1979
|
+
label: "Media upload consent",
|
|
1980
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusChip, {
|
|
1981
|
+
tone: "neutral",
|
|
1982
|
+
children: "Not given"
|
|
1983
|
+
})
|
|
1984
|
+
})
|
|
1985
|
+
]
|
|
1986
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1987
|
+
style: T.note,
|
|
1988
|
+
children: "Offline-only and media-upload consent are two separate settings on purpose. Holding a provider credential is not permission to upload a frame, a transcript or a screen capture, and no agent can change either from inside a session."
|
|
1989
|
+
})]
|
|
1990
|
+
})
|
|
1991
|
+
]
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1994
|
+
/**
|
|
1995
|
+
* About.
|
|
1996
|
+
*
|
|
1997
|
+
* This is where the foundation becomes explicit. Watch is the product and
|
|
1998
|
+
* DeepSeek Harness is what it is built on; both statements belong on the same
|
|
1999
|
+
* screen, and the independence disclosure belongs beside them so the
|
|
2000
|
+
* attribution cannot be read as an endorsement.
|
|
2001
|
+
*/
|
|
2002
|
+
function AboutSection() {
|
|
2003
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2004
|
+
style: T.page,
|
|
2005
|
+
children: [
|
|
2006
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2007
|
+
style: {
|
|
2008
|
+
display: "flex",
|
|
2009
|
+
alignItems: "center",
|
|
2010
|
+
gap: "14px",
|
|
2011
|
+
marginBottom: "4px"
|
|
2012
|
+
},
|
|
2013
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
2014
|
+
src: WATCH_MARK_PNG,
|
|
2015
|
+
width: 44,
|
|
2016
|
+
height: 44,
|
|
2017
|
+
alt: "",
|
|
2018
|
+
"aria-hidden": "true",
|
|
2019
|
+
style: {
|
|
2020
|
+
width: "44px",
|
|
2021
|
+
height: "44px",
|
|
2022
|
+
objectFit: "contain",
|
|
2023
|
+
flexShrink: 0
|
|
2024
|
+
}
|
|
2025
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
2026
|
+
style: {
|
|
2027
|
+
...T.title,
|
|
2028
|
+
fontSize: "19px"
|
|
2029
|
+
},
|
|
2030
|
+
children: PRODUCT_NAME
|
|
2031
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2032
|
+
style: {
|
|
2033
|
+
...T.lead,
|
|
2034
|
+
margin: "2px 0 0"
|
|
2035
|
+
},
|
|
2036
|
+
children: "An agent that sees, remembers, and can prove what actually happened."
|
|
2037
|
+
})] })]
|
|
2038
|
+
}),
|
|
2039
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2040
|
+
style: T.card,
|
|
2041
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2042
|
+
style: T.meta,
|
|
2043
|
+
children: [
|
|
2044
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
2045
|
+
label: "DeepWatch",
|
|
2046
|
+
children: "0.1.0"
|
|
2047
|
+
}),
|
|
2048
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
2049
|
+
label: "Watch Core",
|
|
2050
|
+
children: "Reported by the Bridge, not read from here"
|
|
2051
|
+
}),
|
|
2052
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
2053
|
+
label: "Built on",
|
|
2054
|
+
children: "DeepSeek Harness 0.1.1-rc.2"
|
|
2055
|
+
}),
|
|
2056
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
2057
|
+
label: "DSH commit",
|
|
2058
|
+
children: "b150a551b8d465e31e418e1b2eaf5e79bbb7d28e"
|
|
2059
|
+
})
|
|
2060
|
+
]
|
|
2061
|
+
})
|
|
2062
|
+
}),
|
|
2063
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2064
|
+
style: {
|
|
2065
|
+
...T.card,
|
|
2066
|
+
borderColor: "var(--watch-accent)"
|
|
2067
|
+
},
|
|
2068
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2069
|
+
style: {
|
|
2070
|
+
margin: 0,
|
|
2071
|
+
fontSize: "13px",
|
|
2072
|
+
lineHeight: 1.6
|
|
2073
|
+
},
|
|
2074
|
+
children: ATTRIBUTION
|
|
2075
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2076
|
+
style: {
|
|
2077
|
+
margin: "8px 0 0",
|
|
2078
|
+
fontSize: "13px",
|
|
2079
|
+
lineHeight: 1.6,
|
|
2080
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
2081
|
+
},
|
|
2082
|
+
children: INDEPENDENCE
|
|
2083
|
+
})]
|
|
2084
|
+
}),
|
|
2085
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
2086
|
+
style: T.h2,
|
|
2087
|
+
children: "Licences"
|
|
2088
|
+
}),
|
|
2089
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2090
|
+
style: T.card,
|
|
2091
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2092
|
+
style: T.meta,
|
|
2093
|
+
children: [
|
|
2094
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
2095
|
+
label: "DeepWatch",
|
|
2096
|
+
children: "MIT"
|
|
2097
|
+
}),
|
|
2098
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
2099
|
+
label: "DeepSeek Harness",
|
|
2100
|
+
children: "MIT, and its notice is carried unmodified"
|
|
2101
|
+
}),
|
|
2102
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
2103
|
+
label: "Third-party notices",
|
|
2104
|
+
children: "THIRD_PARTY_NOTICES.md, shipped with this distribution"
|
|
2105
|
+
}),
|
|
2106
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
2107
|
+
label: "Model weights",
|
|
2108
|
+
children: "Distributed with none. A code licence is not a weights licence."
|
|
2109
|
+
})
|
|
2110
|
+
]
|
|
2111
|
+
})
|
|
2112
|
+
})
|
|
2113
|
+
]
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
//#endregion
|
|
2117
|
+
//#region \0watch-css-module:packages/watch/client-settings/src/client/onboarding.module.css.mjs
|
|
2118
|
+
const css = ".kGnD4q_dialog{max-height:min(760px,100vh - 40px);border-color:color-mix(in srgb, var(--watch-accent,#4c8dff) 22%, var(--dsw-alias-border-inverted))!important;background:radial-gradient(circle at 8% 0%, color-mix(in srgb, var(--watch-accent,#4c8dff) 11%, transparent), transparent 38%), var(--dsw-alias-bg-layer-2)!important;width:min(720px,100vw - 40px)!important;padding:0!important}.kGnD4q_card{flex-direction:column;min-width:0;display:flex;overflow:auto}.kGnD4q_hero{border-bottom:1px solid var(--dsw-alias-separator);grid-template-columns:minmax(0,1.08fr) minmax(240px,.92fr);align-items:center;gap:34px;padding:34px 36px 28px;display:grid}.kGnD4q_identity{align-items:center;gap:18px;min-width:0;display:flex}.kGnD4q_markFrame{border:1px solid color-mix(in srgb, var(--watch-accent,#4c8dff) 35%, transparent);background:color-mix(in srgb, var(--watch-accent,#4c8dff) 12%, transparent);border-radius:22px;flex:0 0 4.875rem;place-items:center;width:4.875rem;height:4.875rem;display:grid;box-shadow:inset 0 1px #ffffff14}.kGnD4q_mark{object-fit:contain;width:3.625rem;height:3.625rem;display:block}.kGnD4q_titleBlock{min-width:0}.kGnD4q_eyebrow{color:var(--watch-accent,#4c8dff);letter-spacing:.14em;font-size:11px;font-weight:700}.kGnD4q_title{color:var(--dsw-alias-label-primary);letter-spacing:-.035em;margin:5px 0 7px;font-size:clamp(24px,3.2vw,32px);font-weight:650;line-height:1.08}.kGnD4q_tagline{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px;line-height:1.55}.kGnD4q_lead{max-width:34ch;color:var(--dsw-alias-label-secondary);margin:0;font-size:14px;line-height:1.65}.kGnD4q_statusSection{padding:25px 36px 22px}.kGnD4q_statusHeading{justify-content:space-between;align-items:center;gap:16px;margin-bottom:12px;display:flex}.kGnD4q_liveLabel{color:var(--dsw-alias-label-tertiary);align-items:center;gap:7px;font-size:12px;display:inline-flex}.kGnD4q_liveDot{background:var(--watch-tone-success,#22c55e);width:.4375rem;height:.4375rem;box-shadow:0 0 0 3px color-mix(in srgb, var(--watch-tone-success,#22c55e) 14%, transparent);border-radius:999px}.kGnD4q_checkingDot{background:var(--watch-tone-caution,#d99026);box-shadow:0 0 0 3px color-mix(in srgb, var(--watch-tone-caution,#d99026) 14%, transparent)}.kGnD4q_statusGrid{grid-template-columns:1fr 1fr;gap:12px;display:grid}.kGnD4q_metricCard{border:1px solid var(--dsw-alias-separator);background:color-mix(in srgb, var(--dsw-alias-bg-layer-2) 86%, white 2%);border-radius:14px;min-width:0;padding:16px 17px}.kGnD4q_readyCard{border-color:color-mix(in srgb, var(--watch-accent,#4c8dff) 35%, var(--dsw-alias-separator));background:color-mix(in srgb, var(--watch-accent,#4c8dff) 7%, var(--dsw-alias-bg-layer-2))}.kGnD4q_metricTop{justify-content:space-between;align-items:center;gap:12px;display:flex}.kGnD4q_metricLabel{color:var(--dsw-alias-label-primary);align-items:center;gap:7px;font-size:13px;font-weight:600;display:inline-flex}.kGnD4q_readyCard .kGnD4q_metricLabel,.kGnD4q_readyCard .kGnD4q_metricValue{color:var(--watch-accent,#4c8dff)}.kGnD4q_metricValue{color:var(--dsw-alias-label-primary);letter-spacing:-.04em;font-size:25px;font-weight:650;line-height:1}.kGnD4q_metricCopy{color:var(--dsw-alias-label-secondary);margin:11px 0 0;font-size:12.5px;line-height:1.5}.kGnD4q_consent{background:color-mix(in srgb, var(--dsw-alias-bg-layer-1) 72%, transparent);border-radius:12px;align-items:flex-start;gap:11px;margin:0 36px;padding:13px 15px;display:flex}.kGnD4q_consentIcon{background:color-mix(in srgb, var(--watch-tone-success,#22c55e) 15%, transparent);width:1.4375rem;height:1.4375rem;color:var(--watch-tone-success,#22c55e);border-radius:999px;flex:0 0 1.4375rem;justify-content:center;align-items:center;display:inline-flex}.kGnD4q_consent strong{color:var(--dsw-alias-label-primary);font-size:12.5px;font-weight:600}.kGnD4q_consent p{color:var(--dsw-alias-label-tertiary);margin:3px 0 0;font-size:12px;line-height:1.5}.kGnD4q_footer{justify-content:space-between;align-items:center;gap:14px;padding:22px 36px 28px;display:flex}.kGnD4q_primaryActions{align-items:center;gap:9px;display:flex}@media (width<=620px){.kGnD4q_dialog{max-height:calc(100vh - 24px);width:min(460px,100vw - 24px)!important}.kGnD4q_hero{grid-template-columns:1fr;gap:18px;padding:26px 24px 22px}.kGnD4q_markFrame{border-radius:18px;flex-basis:4.125rem;width:4.125rem;height:4.125rem}.kGnD4q_mark{width:3rem;height:3rem}.kGnD4q_lead{max-width:none}.kGnD4q_statusSection{padding:21px 24px 18px}.kGnD4q_statusGrid{grid-template-columns:1fr}.kGnD4q_consent{margin:0 24px}.kGnD4q_footer{flex-direction:column-reverse;align-items:stretch;padding:18px 24px 24px}.kGnD4q_footer>button,.kGnD4q_primaryActions>button{flex:1}}";
|
|
2119
|
+
const tagId = "@deepwatch/dsh-client-settings/onboarding.module.css";
|
|
2120
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
2121
|
+
const tag = document.createElement("style");
|
|
2122
|
+
tag.dataset.plugin = "@deepwatch/dsh-client-settings";
|
|
2123
|
+
tag.dataset.pluginCss = tagId;
|
|
2124
|
+
tag.textContent = css;
|
|
2125
|
+
document.head.appendChild(tag);
|
|
2126
|
+
}
|
|
2127
|
+
var onboarding_module_css_default = {
|
|
2128
|
+
"card": "kGnD4q_card",
|
|
2129
|
+
"checkingDot": "kGnD4q_checkingDot",
|
|
2130
|
+
"consent": "kGnD4q_consent",
|
|
2131
|
+
"consentIcon": "kGnD4q_consentIcon",
|
|
2132
|
+
"dialog": "kGnD4q_dialog",
|
|
2133
|
+
"eyebrow": "kGnD4q_eyebrow",
|
|
2134
|
+
"footer": "kGnD4q_footer",
|
|
2135
|
+
"hero": "kGnD4q_hero",
|
|
2136
|
+
"identity": "kGnD4q_identity",
|
|
2137
|
+
"lead": "kGnD4q_lead",
|
|
2138
|
+
"liveDot": "kGnD4q_liveDot",
|
|
2139
|
+
"liveLabel": "kGnD4q_liveLabel",
|
|
2140
|
+
"mark": "kGnD4q_mark",
|
|
2141
|
+
"markFrame": "kGnD4q_markFrame",
|
|
2142
|
+
"metricCard": "kGnD4q_metricCard",
|
|
2143
|
+
"metricCopy": "kGnD4q_metricCopy",
|
|
2144
|
+
"metricLabel": "kGnD4q_metricLabel",
|
|
2145
|
+
"metricTop": "kGnD4q_metricTop",
|
|
2146
|
+
"metricValue": "kGnD4q_metricValue",
|
|
2147
|
+
"primaryActions": "kGnD4q_primaryActions",
|
|
2148
|
+
"readyCard": "kGnD4q_readyCard",
|
|
2149
|
+
"statusGrid": "kGnD4q_statusGrid",
|
|
2150
|
+
"statusHeading": "kGnD4q_statusHeading",
|
|
2151
|
+
"statusSection": "kGnD4q_statusSection",
|
|
2152
|
+
"tagline": "kGnD4q_tagline",
|
|
2153
|
+
"title": "kGnD4q_title",
|
|
2154
|
+
"titleBlock": "kGnD4q_titleBlock"
|
|
2155
|
+
};
|
|
2156
|
+
//#endregion
|
|
2157
|
+
//#region src/client/onboarding.tsx
|
|
2158
|
+
/** A human list that stays useful at zero, one and many items. */
|
|
2159
|
+
function names(items) {
|
|
2160
|
+
if (items.length === 0) return "None yet";
|
|
2161
|
+
if (items.length <= 3) return items.map((item) => item.name).join(" · ");
|
|
2162
|
+
return items.slice(0, 2).map((item) => item.name).join(" · ") + " · +" + String(items.length - 2) + " more";
|
|
2163
|
+
}
|
|
2164
|
+
/** The truthful, intentional first impression of DeepWatch. */
|
|
2165
|
+
function WatchOnboarding({ complete, openSection, roles, health, reading }) {
|
|
2166
|
+
const readiness = deriveReadiness({
|
|
2167
|
+
roles,
|
|
2168
|
+
health,
|
|
2169
|
+
reading
|
|
2170
|
+
});
|
|
2171
|
+
const ready = readiness.filter((item) => item.status === "ready");
|
|
2172
|
+
const pending = readiness.filter((item) => item.status !== "ready");
|
|
2173
|
+
const finish = () => {
|
|
2174
|
+
complete?.();
|
|
2175
|
+
};
|
|
2176
|
+
const go = (section) => {
|
|
2177
|
+
openSection?.(section);
|
|
2178
|
+
finish();
|
|
2179
|
+
};
|
|
2180
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
2181
|
+
open: true,
|
|
2182
|
+
title: "Welcome to DeepWatch",
|
|
2183
|
+
onClose: finish,
|
|
2184
|
+
headless: true,
|
|
2185
|
+
className: onboarding_module_css_default.dialog ?? "",
|
|
2186
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2187
|
+
className: onboarding_module_css_default.card,
|
|
2188
|
+
"aria-labelledby": "watch-welcome-title",
|
|
2189
|
+
children: [
|
|
2190
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
2191
|
+
className: onboarding_module_css_default.hero,
|
|
2192
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2193
|
+
className: onboarding_module_css_default.identity,
|
|
2194
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2195
|
+
className: onboarding_module_css_default.markFrame,
|
|
2196
|
+
"aria-hidden": "true",
|
|
2197
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
2198
|
+
src: WATCH_MARK_PNG,
|
|
2199
|
+
width: 58,
|
|
2200
|
+
height: 58,
|
|
2201
|
+
alt: "",
|
|
2202
|
+
className: onboarding_module_css_default.mark
|
|
2203
|
+
})
|
|
2204
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2205
|
+
className: onboarding_module_css_default.titleBlock,
|
|
2206
|
+
children: [
|
|
2207
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2208
|
+
className: onboarding_module_css_default.eyebrow,
|
|
2209
|
+
children: "DEEPWATCH / INTO THE KNOW"
|
|
2210
|
+
}),
|
|
2211
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h2", {
|
|
2212
|
+
id: "watch-welcome-title",
|
|
2213
|
+
className: onboarding_module_css_default.title,
|
|
2214
|
+
children: [
|
|
2215
|
+
"See what happened.",
|
|
2216
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("br", {}),
|
|
2217
|
+
"Prove what worked."
|
|
2218
|
+
]
|
|
2219
|
+
}),
|
|
2220
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2221
|
+
className: onboarding_module_css_default.tagline,
|
|
2222
|
+
children: "See · Remember · Act · Verify"
|
|
2223
|
+
})
|
|
2224
|
+
]
|
|
2225
|
+
})]
|
|
2226
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2227
|
+
className: onboarding_module_css_default.lead,
|
|
2228
|
+
children: "Your local evidence workspace is ready to begin. Connect a model when you want conversation; local capabilities need no provider and no network."
|
|
2229
|
+
})]
|
|
2230
|
+
}),
|
|
2231
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2232
|
+
className: onboarding_module_css_default.statusSection,
|
|
2233
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2234
|
+
className: onboarding_module_css_default.statusHeading,
|
|
2235
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2236
|
+
className: onboarding_module_css_default.eyebrow,
|
|
2237
|
+
children: "INSTALLATION STATUS"
|
|
2238
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2239
|
+
className: onboarding_module_css_default.liveLabel,
|
|
2240
|
+
"aria-live": "polite",
|
|
2241
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2242
|
+
className: [onboarding_module_css_default.liveDot, reading === true ? onboarding_module_css_default.checkingDot : ""].filter(Boolean).join(" "),
|
|
2243
|
+
"aria-hidden": "true"
|
|
2244
|
+
}), reading === true ? "Checking runtime" : "Runtime checked"]
|
|
2245
|
+
})]
|
|
2246
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2247
|
+
className: onboarding_module_css_default.statusGrid,
|
|
2248
|
+
role: "group",
|
|
2249
|
+
"aria-label": "What is ready now",
|
|
2250
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
|
|
2251
|
+
className: [onboarding_module_css_default.metricCard, onboarding_module_css_default.readyCard].filter(Boolean).join(" "),
|
|
2252
|
+
"data-watch-readiness": "ready",
|
|
2253
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2254
|
+
className: onboarding_module_css_default.metricTop,
|
|
2255
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2256
|
+
className: onboarding_module_css_default.metricLabel,
|
|
2257
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutline16, { size: 16 }), "Ready now"]
|
|
2258
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
|
|
2259
|
+
className: onboarding_module_css_default.metricValue,
|
|
2260
|
+
"data-watch-count": true,
|
|
2261
|
+
children: reading === true ? "—" : ready.length
|
|
2262
|
+
})]
|
|
2263
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2264
|
+
className: onboarding_module_css_default.metricCopy,
|
|
2265
|
+
children: reading === true ? "Checking this installation. Nothing is assumed ready." : names(ready) + " passed " + (ready.length === 1 ? "its" : "their") + " runtime gates."
|
|
2266
|
+
})]
|
|
2267
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
|
|
2268
|
+
className: onboarding_module_css_default.metricCard,
|
|
2269
|
+
"data-watch-readiness": "pending",
|
|
2270
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2271
|
+
className: onboarding_module_css_default.metricTop,
|
|
2272
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2273
|
+
className: onboarding_module_css_default.metricLabel,
|
|
2274
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSettingsOutline16, { size: 16 }), "Needs setup"]
|
|
2275
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
|
|
2276
|
+
className: onboarding_module_css_default.metricValue,
|
|
2277
|
+
"data-watch-count": true,
|
|
2278
|
+
children: pending.length
|
|
2279
|
+
})]
|
|
2280
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2281
|
+
className: onboarding_module_css_default.metricCopy,
|
|
2282
|
+
children: "Configure, test or repair these when you need them. Saved is never presented as tested."
|
|
2283
|
+
})]
|
|
2284
|
+
})]
|
|
2285
|
+
})]
|
|
2286
|
+
}),
|
|
2287
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2288
|
+
className: onboarding_module_css_default.consent,
|
|
2289
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2290
|
+
className: onboarding_module_css_default.consentIcon,
|
|
2291
|
+
"aria-hidden": "true",
|
|
2292
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutline16, { size: 14 })
|
|
2293
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "Private by default" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: "Connecting a model does not permit uploading frames, audio, transcripts or evidence. Media access is separate and stays off." })] })]
|
|
2294
|
+
}),
|
|
2295
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
|
|
2296
|
+
className: onboarding_module_css_default.footer,
|
|
2297
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2298
|
+
variant: "ghost",
|
|
2299
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRightUpOutline16, { size: 16 }),
|
|
2300
|
+
onClick: () => {
|
|
2301
|
+
go("watch-diagnostics");
|
|
2302
|
+
},
|
|
2303
|
+
children: "View diagnostics"
|
|
2304
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2305
|
+
className: onboarding_module_css_default.primaryActions,
|
|
2306
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2307
|
+
variant: "outline",
|
|
2308
|
+
onClick: finish,
|
|
2309
|
+
children: "Explore offline"
|
|
2310
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2311
|
+
variant: "primary",
|
|
2312
|
+
onClick: () => {
|
|
2313
|
+
go("watch-roles");
|
|
2314
|
+
},
|
|
2315
|
+
children: "Finish setup"
|
|
2316
|
+
})]
|
|
2317
|
+
})]
|
|
2318
|
+
})
|
|
2319
|
+
]
|
|
2320
|
+
})
|
|
2321
|
+
});
|
|
2322
|
+
}
|
|
2323
|
+
//#endregion
|
|
2324
|
+
//#region src/client/binding-state.ts
|
|
2325
|
+
/**
|
|
2326
|
+
* The browser's view of what is configured, assembled from four Host answers.
|
|
2327
|
+
*
|
|
2328
|
+
* A person saved an OpenRouter credential, saw a green dot and the words
|
|
2329
|
+
* "Saved openrouter.", and reasonably concluded the product was ready. It was
|
|
2330
|
+
* not: no model had been chosen and nothing had been assigned to anything. The
|
|
2331
|
+
* dot was answering "is a credential stored?" while the reader was asking "can
|
|
2332
|
+
* I send a message?", and those turn out to be four separate questions.
|
|
2333
|
+
*
|
|
2334
|
+
* So this store never derives readiness itself. It gathers the four facts —
|
|
2335
|
+
* from `llm.providers`, `llm.models`, `credentials.describe` and the stored
|
|
2336
|
+
* bindings in `settings.describe` — and hands them to `roleReadiness`, which
|
|
2337
|
+
* is the only thing in this product allowed to answer "ready". A surface that
|
|
2338
|
+
* wanted to shade a dot green would have to go through the same gate.
|
|
2339
|
+
*
|
|
2340
|
+
* **Nothing here contacts a provider.** Opening a settings page must not spend
|
|
2341
|
+
* somebody's money or rate budget, so reachability stays `unknown` until a
|
|
2342
|
+
* person asks for a check. That is why a freshly saved credential reads
|
|
2343
|
+
* "Credential saved · not yet assigned" rather than a claim about whether it
|
|
2344
|
+
* works: the honest state after a save is *stored*, and the product says so.
|
|
2345
|
+
*
|
|
2346
|
+
* **No value crosses this boundary.** `credentials.describe` is structurally
|
|
2347
|
+
* value-free — it answers `configured`, `source`, `writable` and has no slot
|
|
2348
|
+
* for a value — and the binding this store writes holds a reference the Host
|
|
2349
|
+
* resolves. There is nowhere in this file for a key to be, which is the
|
|
2350
|
+
* property that makes the store safe to render, log and screenshot.
|
|
2351
|
+
*
|
|
2352
|
+
* @module @deepwatch/dsh-client-settings/binding-state
|
|
2353
|
+
*/
|
|
2354
|
+
/**
|
|
2355
|
+
* Upstream's own settings section for the selection a new session starts with.
|
|
2356
|
+
*
|
|
2357
|
+
* Spelled here rather than imported: `AGENT_DEFAULT_MODEL_SETTINGS_NAMESPACE`
|
|
2358
|
+
* lives in a Host package, and a browser bundle that imported it would carry
|
|
2359
|
+
* the Harness's server-side settings machinery to learn one string.
|
|
2360
|
+
* `tests/binding-flow.test.mjs` holds it to the value the pinned baseline
|
|
2361
|
+
* composes.
|
|
2362
|
+
*/
|
|
2363
|
+
const DEFAULT_MODEL_NAMESPACE = "agent-default-model";
|
|
2364
|
+
/**
|
|
2365
|
+
* The identity a provider test result belongs to.
|
|
2366
|
+
*
|
|
2367
|
+
* Provider and model alone are not enough. Rebinding a role to a route served
|
|
2368
|
+
* through a different credential must not inherit the previous verdict — that
|
|
2369
|
+
* is the same "saved means working" claim this whole file exists to remove,
|
|
2370
|
+
* wearing a route it was never asked about. So the credential *reference* is
|
|
2371
|
+
* part of the key.
|
|
2372
|
+
*
|
|
2373
|
+
* What it deliberately cannot see: a value rotated behind a reference that
|
|
2374
|
+
* did not change. No credential value crosses this boundary, so the browser
|
|
2375
|
+
* half has nothing to compare. A result therefore lives only as long as the
|
|
2376
|
+
* session that produced it, and is never persisted or restored — an untested
|
|
2377
|
+
* binding after a reload reads as untested, which is the truthful answer.
|
|
2378
|
+
*/
|
|
2379
|
+
function providerTestKey(provider, model, credentialRef) {
|
|
2380
|
+
return [
|
|
2381
|
+
provider,
|
|
2382
|
+
model,
|
|
2383
|
+
credentialRef ?? ""
|
|
2384
|
+
].join("\0");
|
|
2385
|
+
}
|
|
2386
|
+
const EMPTY = {
|
|
2387
|
+
status: "idle",
|
|
2388
|
+
error: null,
|
|
2389
|
+
writable: false,
|
|
2390
|
+
providers: [],
|
|
2391
|
+
roles: [],
|
|
2392
|
+
bindings: EMPTY_BINDINGS,
|
|
2393
|
+
saving: false,
|
|
2394
|
+
testingRole: null,
|
|
2395
|
+
testMessage: null
|
|
2396
|
+
};
|
|
2397
|
+
/**
|
|
2398
|
+
* The credential reference a provider's own settings section names.
|
|
2399
|
+
*
|
|
2400
|
+
* Walked out of the described value rather than asked for separately: the
|
|
2401
|
+
* settings domain already returns each namespace's resolved value, and
|
|
2402
|
+
* `apiKeyEnv` is a field in it. The walk is plain property access because the
|
|
2403
|
+
* described value is plain JSON — the secret slots have already been removed
|
|
2404
|
+
* by the seam, which is what makes reading this safe.
|
|
2405
|
+
*/
|
|
2406
|
+
function credentialRefOf(view, path) {
|
|
2407
|
+
if (view === void 0) return null;
|
|
2408
|
+
let cursor = view.value;
|
|
2409
|
+
for (const step of path) {
|
|
2410
|
+
if (typeof cursor !== "object" || cursor === null) return null;
|
|
2411
|
+
cursor = cursor[step];
|
|
2412
|
+
}
|
|
2413
|
+
if (typeof cursor !== "object" || cursor === null) return null;
|
|
2414
|
+
const ref = cursor["apiKeyEnv"];
|
|
2415
|
+
return typeof ref === "string" && ref !== "" ? ref : null;
|
|
2416
|
+
}
|
|
2417
|
+
/**
|
|
2418
|
+
* What is known about a provider's credential.
|
|
2419
|
+
*
|
|
2420
|
+
* `configured_unverified` rather than `verified` is the whole point. The Host
|
|
2421
|
+
* can say a value resolves; only a provider can say it works, and nothing here
|
|
2422
|
+
* has asked one. Calling a stored credential verified is the claim that sent a
|
|
2423
|
+
* prompt to a provider nobody had configured.
|
|
2424
|
+
*/
|
|
2425
|
+
function credentialStatusOf(ref, described, readable) {
|
|
2426
|
+
if (ref === null) return "absent";
|
|
2427
|
+
if (!readable) return "inaccessible";
|
|
2428
|
+
return described[ref]?.configured === true ? "configured_unverified" : "absent";
|
|
2429
|
+
}
|
|
2430
|
+
/** The route capability a provider row amounts to. */
|
|
2431
|
+
function routeOf(row, role) {
|
|
2432
|
+
if (row === void 0 || !row.active) return null;
|
|
2433
|
+
return {
|
|
2434
|
+
provider: row.provider,
|
|
2435
|
+
roles: [...BINDABLE_ROLES],
|
|
2436
|
+
modalities: ROLE_MODALITIES[role],
|
|
2437
|
+
models: row.models.length === 0 ? null : row.models.map((model) => model.id)
|
|
2438
|
+
};
|
|
2439
|
+
}
|
|
2440
|
+
/** Everything known about one role, folded through the single readiness gate. */
|
|
2441
|
+
function roleRowOf(role, bindings, providers, tests = /* @__PURE__ */ new Map()) {
|
|
2442
|
+
const binding = bindingFor(bindings, role);
|
|
2443
|
+
const row = binding === null ? void 0 : providers.find((entry) => entry.provider === binding.provider);
|
|
2444
|
+
const route = routeOf(row, role);
|
|
2445
|
+
const known = route?.models ?? null;
|
|
2446
|
+
const tested = binding === null ? void 0 : tests.get(providerTestKey(binding.provider, binding.model, row?.credentialRef ?? null));
|
|
2447
|
+
const readiness = roleReadiness(role, {
|
|
2448
|
+
binding,
|
|
2449
|
+
credential: tested?.credential ?? row?.credential ?? "absent",
|
|
2450
|
+
reachability: tested?.reachability ?? "unknown",
|
|
2451
|
+
model: binding === null ? "none" : known === null || known.includes(binding.model) ? "selected" : "unavailable",
|
|
2452
|
+
route,
|
|
2453
|
+
consentGranted: true,
|
|
2454
|
+
policyPermits: true,
|
|
2455
|
+
contractMatches: true
|
|
2456
|
+
});
|
|
2457
|
+
return {
|
|
2458
|
+
role,
|
|
2459
|
+
provider: binding?.provider ?? null,
|
|
2460
|
+
model: binding?.model ?? null,
|
|
2461
|
+
readiness
|
|
2462
|
+
};
|
|
2463
|
+
}
|
|
2464
|
+
/**
|
|
2465
|
+
* The binding store, shaped for `useSyncExternalStore`.
|
|
2466
|
+
*
|
|
2467
|
+
* Deliberately a plain object with `subscribe`/`getSnapshot` rather than a
|
|
2468
|
+
* framework store: this package's browser half is loaded into somebody else's
|
|
2469
|
+
* React tree, and bringing a state library into a plugin bundle to hold six
|
|
2470
|
+
* fields is how a distribution ends up shipping two of them.
|
|
2471
|
+
*/
|
|
2472
|
+
var BindingStore = class {
|
|
2473
|
+
api;
|
|
2474
|
+
providerTester;
|
|
2475
|
+
readinessReader;
|
|
2476
|
+
snapshot = EMPTY;
|
|
2477
|
+
listeners = /* @__PURE__ */ new Set();
|
|
2478
|
+
/** Guards against a slow load landing after a newer one. */
|
|
2479
|
+
generation = 0;
|
|
2480
|
+
revision;
|
|
2481
|
+
defaultRevision;
|
|
2482
|
+
providerTests = /* @__PURE__ */ new Map();
|
|
2483
|
+
providerTestAbort = null;
|
|
2484
|
+
constructor(api, providerTester, readinessReader) {
|
|
2485
|
+
this.api = api;
|
|
2486
|
+
this.providerTester = providerTester;
|
|
2487
|
+
this.readinessReader = readinessReader;
|
|
2488
|
+
}
|
|
2489
|
+
/**
|
|
2490
|
+
* Replace what this tab believes about tested routes with what the Host says.
|
|
2491
|
+
*
|
|
2492
|
+
* The browser used to be the only place a provider-test verdict lived, and a
|
|
2493
|
+
* tab cannot see a Host restart, an edit made in another tab, or a key
|
|
2494
|
+
* rotated behind a reference that did not change. It drew a tested badge over
|
|
2495
|
+
* routes the Host had already stopped being willing to serve, and the
|
|
2496
|
+
* composer that badge unlocks opened onto a refusal.
|
|
2497
|
+
*
|
|
2498
|
+
* So the Host is asked, per bound route, and its answer wins in both
|
|
2499
|
+
* directions: a route it still proves is tested even in a tab that has just
|
|
2500
|
+
* been reloaded and ran no test, and a route it no longer proves stops being
|
|
2501
|
+
* tested here the moment this is read.
|
|
2502
|
+
*/
|
|
2503
|
+
async reconcileReadiness(bindings, providers, signal) {
|
|
2504
|
+
if (this.readinessReader === void 0) return;
|
|
2505
|
+
for (const role of BINDABLE_ROLES) {
|
|
2506
|
+
const binding = bindingFor(bindings, role);
|
|
2507
|
+
if (binding === null) continue;
|
|
2508
|
+
const credentialRef = providers.find((entry) => entry.provider === binding.provider)?.credentialRef ?? null;
|
|
2509
|
+
const key = providerTestKey(binding.provider, binding.model, credentialRef);
|
|
2510
|
+
let verdict;
|
|
2511
|
+
try {
|
|
2512
|
+
verdict = await this.readinessReader(binding.provider, binding.model, signal);
|
|
2513
|
+
} catch {
|
|
2514
|
+
continue;
|
|
2515
|
+
}
|
|
2516
|
+
if (signal.aborted) return;
|
|
2517
|
+
if (verdict.proved) this.providerTests.set(key, {
|
|
2518
|
+
provider: binding.provider,
|
|
2519
|
+
model: binding.model,
|
|
2520
|
+
ok: true,
|
|
2521
|
+
credential: "verified",
|
|
2522
|
+
reachability: "reachable",
|
|
2523
|
+
message: "The Host still holds a proof for this route."
|
|
2524
|
+
});
|
|
2525
|
+
else this.providerTests.delete(key);
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
/** @returns the current snapshot; stable between changes. */
|
|
2529
|
+
getSnapshot = () => this.snapshot;
|
|
2530
|
+
/** @param listener - called after every change. @returns the unsubscriber. */
|
|
2531
|
+
subscribe = (listener) => {
|
|
2532
|
+
this.listeners.add(listener);
|
|
2533
|
+
return () => {
|
|
2534
|
+
this.listeners.delete(listener);
|
|
2535
|
+
};
|
|
2536
|
+
};
|
|
2537
|
+
publish(next) {
|
|
2538
|
+
this.snapshot = {
|
|
2539
|
+
...this.snapshot,
|
|
2540
|
+
...next
|
|
2541
|
+
};
|
|
2542
|
+
for (const listener of this.listeners) listener();
|
|
2543
|
+
}
|
|
2544
|
+
/**
|
|
2545
|
+
* Ask the Host everything, and fold it into one snapshot.
|
|
2546
|
+
*
|
|
2547
|
+
* The three reads run together because they are independent and a settings
|
|
2548
|
+
* page that took three round trips in series felt broken on a slow link.
|
|
2549
|
+
* Their failures are not equal, though: providers and settings are the page,
|
|
2550
|
+
* so losing either is an error, while a credential describe that fails
|
|
2551
|
+
* downgrades those providers to `inaccessible` and leaves the rest readable.
|
|
2552
|
+
*/
|
|
2553
|
+
async load() {
|
|
2554
|
+
const generation = ++this.generation;
|
|
2555
|
+
this.publish({
|
|
2556
|
+
status: "loading",
|
|
2557
|
+
error: null
|
|
2558
|
+
});
|
|
2559
|
+
let providers;
|
|
2560
|
+
let namespaces;
|
|
2561
|
+
let groups = [];
|
|
2562
|
+
let failures = [];
|
|
2563
|
+
let writable = false;
|
|
2564
|
+
try {
|
|
2565
|
+
const [directory, settings, catalogue] = await Promise.all([
|
|
2566
|
+
this.api.llm.providers({}),
|
|
2567
|
+
this.api.settings.describe({}),
|
|
2568
|
+
this.api.llm.models({}).catch(() => null)
|
|
2569
|
+
]);
|
|
2570
|
+
if (!directory.result.ok) throw new Error(directory.result.error.message);
|
|
2571
|
+
if (!settings.result.ok) throw new Error(settings.result.error.message);
|
|
2572
|
+
providers = directory.result.value.providers;
|
|
2573
|
+
namespaces = settings.result.value.namespaces;
|
|
2574
|
+
writable = settings.result.value.writable;
|
|
2575
|
+
if (catalogue !== null && catalogue.result.ok) {
|
|
2576
|
+
groups = catalogue.result.value.groups;
|
|
2577
|
+
failures = catalogue.result.value.failures;
|
|
2578
|
+
}
|
|
2579
|
+
} catch (error) {
|
|
2580
|
+
if (generation !== this.generation) return;
|
|
2581
|
+
this.publish({
|
|
2582
|
+
status: "error",
|
|
2583
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2584
|
+
});
|
|
2585
|
+
return;
|
|
2586
|
+
}
|
|
2587
|
+
const byNs = new Map(namespaces.map((view) => [view.ns, view]));
|
|
2588
|
+
const stored = byNs.get(BINDINGS_NAMESPACE);
|
|
2589
|
+
this.revision = stored?.revision;
|
|
2590
|
+
const bindings = readBindings(stored?.value);
|
|
2591
|
+
const refs = /* @__PURE__ */ new Map();
|
|
2592
|
+
for (const entry of providers) refs.set(entry.provider, credentialRefOf(byNs.get(entry.settingsNs), entry.settingsPath));
|
|
2593
|
+
const wanted = [...new Set([...refs.values()].filter((ref) => ref !== null))];
|
|
2594
|
+
let credentials = {};
|
|
2595
|
+
let readable = true;
|
|
2596
|
+
if (wanted.length > 0) try {
|
|
2597
|
+
const answer = await this.api.credentials.describe({ refs: wanted });
|
|
2598
|
+
if (answer.result.ok) credentials = answer.result.value.credentials;
|
|
2599
|
+
else readable = false;
|
|
2600
|
+
} catch {
|
|
2601
|
+
readable = false;
|
|
2602
|
+
}
|
|
2603
|
+
if (generation !== this.generation) return;
|
|
2604
|
+
const catalogByProvider = new Map(groups.map((group) => [group.id, group]));
|
|
2605
|
+
const failureByProvider = new Map(failures.map((entry) => [entry.id, entry.message]));
|
|
2606
|
+
const rows = providers.map((entry) => {
|
|
2607
|
+
const ref = refs.get(entry.provider) ?? null;
|
|
2608
|
+
return {
|
|
2609
|
+
provider: entry.provider,
|
|
2610
|
+
displayName: entry.displayName,
|
|
2611
|
+
active: entry.active,
|
|
2612
|
+
credentialRef: ref,
|
|
2613
|
+
credential: credentialStatusOf(ref, credentials, readable),
|
|
2614
|
+
models: catalogByProvider.get(entry.provider)?.models ?? [],
|
|
2615
|
+
catalogError: failureByProvider.get(entry.provider) ?? null
|
|
2616
|
+
};
|
|
2617
|
+
});
|
|
2618
|
+
const readiness = new AbortController();
|
|
2619
|
+
await this.reconcileReadiness(bindings, rows, readiness.signal);
|
|
2620
|
+
if (generation !== this.generation) return;
|
|
2621
|
+
this.publish({
|
|
2622
|
+
status: "ready",
|
|
2623
|
+
error: null,
|
|
2624
|
+
writable,
|
|
2625
|
+
providers: rows,
|
|
2626
|
+
bindings,
|
|
2627
|
+
roles: BINDABLE_ROLES.map((role) => roleRowOf(role, bindings, rows, this.providerTests))
|
|
2628
|
+
});
|
|
2629
|
+
if (writable) await this.reconcileDefaultSelection(bindings, byNs.get(DEFAULT_MODEL_NAMESPACE));
|
|
2630
|
+
}
|
|
2631
|
+
/**
|
|
2632
|
+
* Write the Harness selection only when it disagrees with the binding.
|
|
2633
|
+
*
|
|
2634
|
+
* Guarded on disagreement because this runs on every load: rewriting an
|
|
2635
|
+
* already-correct section would bump its revision, invalidate every other
|
|
2636
|
+
* open editor's `expectedRevision`, and turn a read into a source of write
|
|
2637
|
+
* conflicts.
|
|
2638
|
+
*/
|
|
2639
|
+
async reconcileDefaultSelection(bindings, view) {
|
|
2640
|
+
this.defaultRevision = view?.revision;
|
|
2641
|
+
const chat = bindings.roles[PRIMARY_ROLE];
|
|
2642
|
+
const wanted = chat === void 0 ? {
|
|
2643
|
+
provider: "",
|
|
2644
|
+
model: ""
|
|
2645
|
+
} : {
|
|
2646
|
+
provider: chat.provider,
|
|
2647
|
+
model: chat.model
|
|
2648
|
+
};
|
|
2649
|
+
const current = view?.value ?? {};
|
|
2650
|
+
if (current.provider === wanted.provider && current.model === wanted.model) return;
|
|
2651
|
+
await this.syncDefaultSelection(bindings);
|
|
2652
|
+
}
|
|
2653
|
+
/**
|
|
2654
|
+
* Bind one role to one provider and model, and persist the decision.
|
|
2655
|
+
*
|
|
2656
|
+
* `replace` rather than `update`, because unbinding has to be expressible:
|
|
2657
|
+
* a merge cannot remove a key, and a role that could be added but not
|
|
2658
|
+
* removed is a role somebody is stuck with. The whole document is rewritten
|
|
2659
|
+
* from the snapshot the caller is looking at, and `expectedRevision` is what
|
|
2660
|
+
* turns a concurrent edit into a refusal rather than a silent overwrite.
|
|
2661
|
+
*/
|
|
2662
|
+
async bind(role, provider, model) {
|
|
2663
|
+
const row = this.snapshot.providers.find((entry) => entry.provider === provider);
|
|
2664
|
+
await this.write(withBinding(this.snapshot.bindings, role, {
|
|
2665
|
+
provider,
|
|
2666
|
+
model,
|
|
2667
|
+
credentialRef: row?.credentialRef ?? null,
|
|
2668
|
+
boundAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2669
|
+
boundBy: "person"
|
|
2670
|
+
}));
|
|
2671
|
+
}
|
|
2672
|
+
/** Remove one role's binding. The role becomes unbound, never inherited. */
|
|
2673
|
+
async unbind(role) {
|
|
2674
|
+
await this.write(withoutBinding(this.snapshot.bindings, role));
|
|
2675
|
+
}
|
|
2676
|
+
/** Run the exact bound route once; saving a credential never calls this. */
|
|
2677
|
+
async testRole(role) {
|
|
2678
|
+
const binding = bindingFor(this.snapshot.bindings, role);
|
|
2679
|
+
if (binding === null || this.providerTester === void 0) {
|
|
2680
|
+
this.publish({ testMessage: "Assign a provider and model before running the test." });
|
|
2681
|
+
return;
|
|
2682
|
+
}
|
|
2683
|
+
const credentialRef = this.snapshot.providers.find((entry) => entry.provider === binding.provider)?.credentialRef ?? null;
|
|
2684
|
+
const controller = new AbortController();
|
|
2685
|
+
this.providerTestAbort?.abort();
|
|
2686
|
+
this.providerTestAbort = controller;
|
|
2687
|
+
this.publish({
|
|
2688
|
+
testingRole: role,
|
|
2689
|
+
testMessage: null
|
|
2690
|
+
});
|
|
2691
|
+
try {
|
|
2692
|
+
const facts = await this.providerTester(binding.provider, binding.model, controller.signal);
|
|
2693
|
+
if (controller.signal.aborted) return;
|
|
2694
|
+
this.providerTests.set(providerTestKey(binding.provider, binding.model, credentialRef), facts);
|
|
2695
|
+
this.publish({
|
|
2696
|
+
testingRole: null,
|
|
2697
|
+
testMessage: facts.message,
|
|
2698
|
+
roles: BINDABLE_ROLES.map((current) => roleRowOf(current, this.snapshot.bindings, this.snapshot.providers, this.providerTests))
|
|
2699
|
+
});
|
|
2700
|
+
} catch {
|
|
2701
|
+
if (controller.signal.aborted) return;
|
|
2702
|
+
this.publish({
|
|
2703
|
+
testingRole: null,
|
|
2704
|
+
testMessage: "The provider test could not be started. Check Diagnostics and try again."
|
|
2705
|
+
});
|
|
2706
|
+
} finally {
|
|
2707
|
+
if (this.providerTestAbort === controller) this.providerTestAbort = null;
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
/** Cancel only the explicit provider probe; no Chat turn is involved. */
|
|
2711
|
+
cancelProviderTest() {
|
|
2712
|
+
if (this.providerTestAbort === null) return;
|
|
2713
|
+
this.providerTestAbort.abort();
|
|
2714
|
+
this.providerTestAbort = null;
|
|
2715
|
+
this.publish({
|
|
2716
|
+
testingRole: null,
|
|
2717
|
+
testMessage: "Provider test cancelled. This binding remains not tested."
|
|
2718
|
+
});
|
|
2719
|
+
}
|
|
2720
|
+
/**
|
|
2721
|
+
* Point the Harness's own default selection at what Chat is bound to.
|
|
2722
|
+
*
|
|
2723
|
+
* The link this subsystem was missing, and the failure it caused is worth
|
|
2724
|
+
* stating plainly: a person added a provider, chose a model and assigned it
|
|
2725
|
+
* to Chat, and still could not send. Every DeepWatch surface agreed the
|
|
2726
|
+
* binding existed. It did — in DeepWatch's document. But the thing that
|
|
2727
|
+
* actually routes a prompt is the Harness's model selection, which this
|
|
2728
|
+
* distribution had *emptied* so that nothing would be chosen for anybody,
|
|
2729
|
+
* and binding Chat never filled it in. Two records of one decision, and the
|
|
2730
|
+
* one the runtime reads was the one nobody was writing.
|
|
2731
|
+
*
|
|
2732
|
+
* So a Chat binding writes both. `agent-default-model` is upstream's own
|
|
2733
|
+
* settings section, read live by `AgentDefaultModelConfig` and re-read by a
|
|
2734
|
+
* blank session on every look — so a session opened before the binding
|
|
2735
|
+
* picks it up without being told.
|
|
2736
|
+
*
|
|
2737
|
+
* Only Chat. The other roles are DeepWatch's own concepts and have no
|
|
2738
|
+
* upstream selection to keep in step; writing one for them would point the
|
|
2739
|
+
* conversation at a model chosen for something else.
|
|
2740
|
+
*/
|
|
2741
|
+
async syncDefaultSelection(next) {
|
|
2742
|
+
const chat = next.roles[PRIMARY_ROLE];
|
|
2743
|
+
const section = chat === void 0 ? {
|
|
2744
|
+
provider: "",
|
|
2745
|
+
model: ""
|
|
2746
|
+
} : {
|
|
2747
|
+
provider: chat.provider,
|
|
2748
|
+
model: chat.model
|
|
2749
|
+
};
|
|
2750
|
+
try {
|
|
2751
|
+
const response = await this.api.settings.replace({
|
|
2752
|
+
ns: DEFAULT_MODEL_NAMESPACE,
|
|
2753
|
+
section,
|
|
2754
|
+
...this.defaultRevision === void 0 ? {} : { expectedRevision: this.defaultRevision }
|
|
2755
|
+
});
|
|
2756
|
+
if (response.result.ok) this.defaultRevision = response.result.value.revision;
|
|
2757
|
+
else this.publish({ error: response.result.error.message });
|
|
2758
|
+
} catch (error) {
|
|
2759
|
+
this.publish({ error: error instanceof Error ? error.message : String(error) });
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
async write(next) {
|
|
2763
|
+
assertNoSecretMaterial("the binding document", next);
|
|
2764
|
+
this.publish({
|
|
2765
|
+
saving: true,
|
|
2766
|
+
error: null
|
|
2767
|
+
});
|
|
2768
|
+
try {
|
|
2769
|
+
const response = await this.api.settings.replace({
|
|
2770
|
+
ns: BINDINGS_NAMESPACE,
|
|
2771
|
+
section: {
|
|
2772
|
+
version: 1,
|
|
2773
|
+
roles: next.roles
|
|
2774
|
+
},
|
|
2775
|
+
...this.revision === void 0 ? {} : { expectedRevision: this.revision }
|
|
2776
|
+
});
|
|
2777
|
+
if (!response.result.ok) {
|
|
2778
|
+
this.publish({
|
|
2779
|
+
saving: false,
|
|
2780
|
+
error: response.result.error.message
|
|
2781
|
+
});
|
|
2782
|
+
return;
|
|
2783
|
+
}
|
|
2784
|
+
this.revision = response.result.value.revision;
|
|
2785
|
+
const bindings = readBindings(response.result.value.value);
|
|
2786
|
+
await this.syncDefaultSelection(bindings);
|
|
2787
|
+
this.publish({
|
|
2788
|
+
saving: false,
|
|
2789
|
+
bindings,
|
|
2790
|
+
roles: BINDABLE_ROLES.map((role) => roleRowOf(role, bindings, this.snapshot.providers, this.providerTests))
|
|
2791
|
+
});
|
|
2792
|
+
} catch (error) {
|
|
2793
|
+
this.publish({
|
|
2794
|
+
saving: false,
|
|
2795
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2796
|
+
});
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
};
|
|
2800
|
+
/**
|
|
2801
|
+
* Whether the capability a conversation needs can actually run.
|
|
2802
|
+
*
|
|
2803
|
+
* The one question the composer asks, given its own name so no surface has to
|
|
2804
|
+
* remember which role Chat is or re-derive readiness to find out.
|
|
2805
|
+
*/
|
|
2806
|
+
function chatReadiness(snapshot) {
|
|
2807
|
+
return snapshot.roles.find((row) => row.role === "agent_model")?.readiness ?? null;
|
|
2808
|
+
}
|
|
2809
|
+
//#endregion
|
|
2810
|
+
//#region src/providers.ts
|
|
2811
|
+
/** A few recognisable names, for a sentence that has to stay short. */
|
|
2812
|
+
const SAMPLE_PROVIDERS = [
|
|
2813
|
+
"amazon-bedrock",
|
|
2814
|
+
"anthropic",
|
|
2815
|
+
"deepseek",
|
|
2816
|
+
"google",
|
|
2817
|
+
"groq",
|
|
2818
|
+
"mistral",
|
|
2819
|
+
"openai",
|
|
2820
|
+
"openrouter"
|
|
2821
|
+
];
|
|
2822
|
+
//#endregion
|
|
2823
|
+
//#region src/client/role-bindings.tsx
|
|
2824
|
+
/**
|
|
2825
|
+
* Role Bindings, as a screen somebody can actually finish setup on.
|
|
2826
|
+
*
|
|
2827
|
+
* This surface used to be four rows of static copy saying "Not bound" and a
|
|
2828
|
+
* paragraph explaining that bindings were stored by DSH. Both true. Neither
|
|
2829
|
+
* gave anybody a way to bind anything — so a person who had saved an OpenRouter
|
|
2830
|
+
* credential arrived here, read that nothing was bound, and had no control to
|
|
2831
|
+
* change it. The screen described the product's design instead of operating it.
|
|
2832
|
+
*
|
|
2833
|
+
* What it does now is the four steps in order, because they *are* ordered and
|
|
2834
|
+
* pretending otherwise is what made a saved credential look like a finished
|
|
2835
|
+
* setup:
|
|
2836
|
+
*
|
|
2837
|
+
* 1. a provider has a credential — stored, and nothing more
|
|
2838
|
+
* 2. that provider advertises models — asked for, never assumed
|
|
2839
|
+
* 3. a model is assigned to a capability — explicitly, by a person
|
|
2840
|
+
* 4. the capability can run — derived, never stored
|
|
2841
|
+
*
|
|
2842
|
+
* **Nothing here is signalled by colour alone.** Every state carries the word
|
|
2843
|
+
* for it, and the chip is decoration on top of that word. A status conveyed by
|
|
2844
|
+
* a green dot is a status a screen reader does not have, and — as the incident
|
|
2845
|
+
* that produced this file showed — one a sighted reader misreads too.
|
|
2846
|
+
*
|
|
2847
|
+
* **Nothing is chosen implicitly.** There is no "use my only credential", no
|
|
2848
|
+
* defaulting to the first model in the list, and no provider that becomes
|
|
2849
|
+
* bound because it was configured. Every binding here is a button somebody
|
|
2850
|
+
* pressed.
|
|
2851
|
+
*
|
|
2852
|
+
* @module @deepwatch/dsh-client-settings/role-bindings
|
|
2853
|
+
*/
|
|
2854
|
+
/**
|
|
2855
|
+
* The tone a readiness status earns.
|
|
2856
|
+
*
|
|
2857
|
+
* `bound_unverified` is deliberately not `active`. That is precisely the state
|
|
2858
|
+
* a green dot used to claim, and claiming it is what sent a prompt to a
|
|
2859
|
+
* provider nobody had configured.
|
|
2860
|
+
*/
|
|
2861
|
+
function toneFor(readiness) {
|
|
2862
|
+
if (isExecutable(readiness)) return "active";
|
|
2863
|
+
return readiness.status === "blocked" ? "caution" : "neutral";
|
|
2864
|
+
}
|
|
2865
|
+
/** The tone a credential state earns. Stored is not working. */
|
|
2866
|
+
function credentialTone(status) {
|
|
2867
|
+
if (status === "verified") return "active";
|
|
2868
|
+
if (status === "rejected" || status === "inaccessible") return "caution";
|
|
2869
|
+
return "neutral";
|
|
2870
|
+
}
|
|
2871
|
+
const CONTROL = {
|
|
2872
|
+
select: {
|
|
2873
|
+
font: "inherit",
|
|
2874
|
+
fontSize: "13px",
|
|
2875
|
+
padding: "7px 10px",
|
|
2876
|
+
borderRadius: "10px",
|
|
2877
|
+
border: "1px solid color-mix(in srgb, var(--watch-accent) 12%, var(--dsw-alias-border-l2))",
|
|
2878
|
+
background: "var(--dsw-alias-bg-layer-2)",
|
|
2879
|
+
color: "var(--dsw-alias-label-primary)",
|
|
2880
|
+
minWidth: "180px",
|
|
2881
|
+
maxWidth: "100%"
|
|
2882
|
+
},
|
|
2883
|
+
primary: {
|
|
2884
|
+
font: "inherit",
|
|
2885
|
+
fontSize: "13px",
|
|
2886
|
+
fontWeight: 600,
|
|
2887
|
+
padding: "8px 15px",
|
|
2888
|
+
borderRadius: "10px",
|
|
2889
|
+
cursor: "pointer",
|
|
2890
|
+
border: "1px solid var(--watch-accent)",
|
|
2891
|
+
background: "var(--watch-accent)",
|
|
2892
|
+
color: "white",
|
|
2893
|
+
boxShadow: "0 6px 18px color-mix(in srgb, var(--watch-accent) 22%, transparent)"
|
|
2894
|
+
},
|
|
2895
|
+
quiet: {
|
|
2896
|
+
font: "inherit",
|
|
2897
|
+
fontSize: "13px",
|
|
2898
|
+
padding: "8px 13px",
|
|
2899
|
+
borderRadius: "10px",
|
|
2900
|
+
cursor: "pointer",
|
|
2901
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
2902
|
+
background: "color-mix(in srgb, var(--dsw-alias-bg-layer-2) 80%, transparent)",
|
|
2903
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
2904
|
+
},
|
|
2905
|
+
field: {
|
|
2906
|
+
display: "flex",
|
|
2907
|
+
flexDirection: "column",
|
|
2908
|
+
gap: "4px",
|
|
2909
|
+
minWidth: 0
|
|
2910
|
+
},
|
|
2911
|
+
label: {
|
|
2912
|
+
fontSize: "11px",
|
|
2913
|
+
letterSpacing: "0.03em",
|
|
2914
|
+
textTransform: "uppercase",
|
|
2915
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
2916
|
+
},
|
|
2917
|
+
row: {
|
|
2918
|
+
display: "flex",
|
|
2919
|
+
flexWrap: "wrap",
|
|
2920
|
+
gap: "10px",
|
|
2921
|
+
alignItems: "flex-end",
|
|
2922
|
+
marginTop: "12px"
|
|
2923
|
+
}
|
|
2924
|
+
};
|
|
2925
|
+
/**
|
|
2926
|
+
* The editor for one role: pick a provider, pick one of its models, assign it.
|
|
2927
|
+
*
|
|
2928
|
+
* Two selects rather than one combined list, because the two choices fail
|
|
2929
|
+
* differently. A provider with no credential is a different problem from a
|
|
2930
|
+
* provider whose catalogue is empty, and a single flattened list would report
|
|
2931
|
+
* both as "nothing to choose".
|
|
2932
|
+
*
|
|
2933
|
+
* Exported because the blocked composer offers the same choice in place. A
|
|
2934
|
+
* person who is stopped from sending should be able to fix it where they are
|
|
2935
|
+
* standing, and offering them a *second* picker written separately is how two
|
|
2936
|
+
* screens end up disagreeing about what a provider offers.
|
|
2937
|
+
*/
|
|
2938
|
+
function BindingEditor({ row, providers, saving, onBind, onCancel }) {
|
|
2939
|
+
const providerId = (0, react.useId)();
|
|
2940
|
+
const modelId = (0, react.useId)();
|
|
2941
|
+
const noticeId = (0, react.useId)();
|
|
2942
|
+
const first = (0, react.useRef)(null);
|
|
2943
|
+
const [provider, setProvider] = (0, react.useState)(row.provider ?? "");
|
|
2944
|
+
const [model, setModel] = (0, react.useState)(row.model ?? "");
|
|
2945
|
+
(0, react.useEffect)(() => {
|
|
2946
|
+
first.current?.focus();
|
|
2947
|
+
}, []);
|
|
2948
|
+
const chosen = providers.find((entry) => entry.provider === provider);
|
|
2949
|
+
const models = chosen?.models ?? [];
|
|
2950
|
+
const pickProvider = (next) => {
|
|
2951
|
+
setProvider(next);
|
|
2952
|
+
setModel("");
|
|
2953
|
+
};
|
|
2954
|
+
const notice = chosen === void 0 ? "Choose a provider to see the models it offers." : chosen.credential === "absent" ? `${chosen.displayName} has no credential yet. Add one in Models & Providers, then come back.` : chosen.catalogError !== null ? `${chosen.displayName} did not return a model list.` : models.length === 0 ? `${chosen.displayName} advertised no models.` : `${String(models.length)} model(s) offered by ${chosen.displayName}.`;
|
|
2955
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2956
|
+
style: CONTROL.row,
|
|
2957
|
+
children: [
|
|
2958
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2959
|
+
style: CONTROL.field,
|
|
2960
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
2961
|
+
htmlFor: providerId,
|
|
2962
|
+
style: CONTROL.label,
|
|
2963
|
+
children: "Provider"
|
|
2964
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
2965
|
+
id: providerId,
|
|
2966
|
+
ref: first,
|
|
2967
|
+
style: CONTROL.select,
|
|
2968
|
+
value: provider,
|
|
2969
|
+
disabled: saving,
|
|
2970
|
+
"aria-describedby": noticeId,
|
|
2971
|
+
onChange: (event) => {
|
|
2972
|
+
pickProvider(event.target.value);
|
|
2973
|
+
},
|
|
2974
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2975
|
+
value: "",
|
|
2976
|
+
children: "Choose a provider…"
|
|
2977
|
+
}), providers.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2978
|
+
value: entry.provider,
|
|
2979
|
+
children: `${entry.displayName} — ${CREDENTIAL_STATUS_LABEL[entry.credential]}`
|
|
2980
|
+
}, entry.provider))]
|
|
2981
|
+
})]
|
|
2982
|
+
}),
|
|
2983
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2984
|
+
style: CONTROL.field,
|
|
2985
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
2986
|
+
htmlFor: modelId,
|
|
2987
|
+
style: CONTROL.label,
|
|
2988
|
+
children: "Model"
|
|
2989
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
2990
|
+
id: modelId,
|
|
2991
|
+
style: CONTROL.select,
|
|
2992
|
+
value: model,
|
|
2993
|
+
disabled: saving || models.length === 0,
|
|
2994
|
+
"aria-describedby": noticeId,
|
|
2995
|
+
onChange: (event) => {
|
|
2996
|
+
setModel(event.target.value);
|
|
2997
|
+
},
|
|
2998
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2999
|
+
value: "",
|
|
3000
|
+
children: "Choose a model…"
|
|
3001
|
+
}), models.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
3002
|
+
value: entry.id,
|
|
3003
|
+
children: entry.name === "" ? entry.id : entry.name
|
|
3004
|
+
}, entry.id))]
|
|
3005
|
+
})]
|
|
3006
|
+
}),
|
|
3007
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3008
|
+
type: "button",
|
|
3009
|
+
style: {
|
|
3010
|
+
...CONTROL.primary,
|
|
3011
|
+
opacity: provider === "" || model === "" || saving ? .5 : 1
|
|
3012
|
+
},
|
|
3013
|
+
disabled: provider === "" || model === "" || saving,
|
|
3014
|
+
onClick: () => {
|
|
3015
|
+
onBind(provider, model);
|
|
3016
|
+
},
|
|
3017
|
+
children: saving ? "Assigning…" : `Assign to ${ROLE_LABEL[row.role]}`
|
|
3018
|
+
}),
|
|
3019
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3020
|
+
type: "button",
|
|
3021
|
+
style: CONTROL.quiet,
|
|
3022
|
+
onClick: onCancel,
|
|
3023
|
+
disabled: saving,
|
|
3024
|
+
children: "Cancel"
|
|
3025
|
+
}),
|
|
3026
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3027
|
+
id: noticeId,
|
|
3028
|
+
style: {
|
|
3029
|
+
flexBasis: "100%",
|
|
3030
|
+
margin: "2px 0 0",
|
|
3031
|
+
fontSize: "12px",
|
|
3032
|
+
lineHeight: 1.5,
|
|
3033
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
3034
|
+
},
|
|
3035
|
+
children: notice
|
|
3036
|
+
})
|
|
3037
|
+
]
|
|
3038
|
+
});
|
|
3039
|
+
}
|
|
3040
|
+
/** One role: what it is, what it is bound to, and whether that can run. */
|
|
3041
|
+
function RoleCard({ row, providers, snapshot, store, openEditor, editing, onEdit, onClose }) {
|
|
3042
|
+
const provider = providers.find((entry) => entry.provider === row.provider);
|
|
3043
|
+
const blocker = blockerMessage(row.readiness);
|
|
3044
|
+
const ready = isExecutable(row.readiness);
|
|
3045
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3046
|
+
style: T.card,
|
|
3047
|
+
children: [
|
|
3048
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3049
|
+
style: T.cardHead,
|
|
3050
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
3051
|
+
style: T.title,
|
|
3052
|
+
children: ROLE_LABEL[row.role]
|
|
3053
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusChip, {
|
|
3054
|
+
tone: toneFor(row.readiness),
|
|
3055
|
+
children: BINDING_STATUS_LABEL[row.readiness.status]
|
|
3056
|
+
})]
|
|
3057
|
+
}),
|
|
3058
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3059
|
+
style: {
|
|
3060
|
+
...T.lead,
|
|
3061
|
+
margin: "6px 0 0"
|
|
3062
|
+
},
|
|
3063
|
+
children: ROLE_PURPOSE[row.role]
|
|
3064
|
+
}),
|
|
3065
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3066
|
+
style: T.meta,
|
|
3067
|
+
children: [
|
|
3068
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3069
|
+
style: T.key,
|
|
3070
|
+
children: "Provider"
|
|
3071
|
+
}),
|
|
3072
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3073
|
+
style: T.value,
|
|
3074
|
+
children: provider?.displayName ?? row.provider ?? "Nothing assigned"
|
|
3075
|
+
}),
|
|
3076
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3077
|
+
style: T.key,
|
|
3078
|
+
children: "Model"
|
|
3079
|
+
}),
|
|
3080
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3081
|
+
style: T.value,
|
|
3082
|
+
children: row.model ?? "Nothing assigned"
|
|
3083
|
+
}),
|
|
3084
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3085
|
+
style: T.key,
|
|
3086
|
+
children: "Status"
|
|
3087
|
+
}),
|
|
3088
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3089
|
+
style: T.value,
|
|
3090
|
+
children: ready ? `${ROLE_LABEL[row.role]} can run.` : blocker ?? "Not configured."
|
|
3091
|
+
})
|
|
3092
|
+
]
|
|
3093
|
+
}),
|
|
3094
|
+
openEditor && editing === row.role ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BindingEditor, {
|
|
3095
|
+
row,
|
|
3096
|
+
providers,
|
|
3097
|
+
saving: snapshot.saving,
|
|
3098
|
+
onBind: (chosenProvider, chosenModel) => {
|
|
3099
|
+
store.bind(row.role, chosenProvider, chosenModel).then(onClose);
|
|
3100
|
+
},
|
|
3101
|
+
onCancel: onClose
|
|
3102
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3103
|
+
style: CONTROL.row,
|
|
3104
|
+
children: [
|
|
3105
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3106
|
+
type: "button",
|
|
3107
|
+
style: CONTROL.primary,
|
|
3108
|
+
disabled: !snapshot.writable,
|
|
3109
|
+
"aria-describedby": snapshot.writable ? void 0 : "watch-bindings-readonly",
|
|
3110
|
+
onClick: () => {
|
|
3111
|
+
onEdit(row.role);
|
|
3112
|
+
},
|
|
3113
|
+
children: row.model === null ? "Choose a model" : "Change model"
|
|
3114
|
+
}),
|
|
3115
|
+
row.model === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3116
|
+
type: "button",
|
|
3117
|
+
style: CONTROL.quiet,
|
|
3118
|
+
disabled: snapshot.testingRole !== null && snapshot.testingRole !== row.role || snapshot.saving,
|
|
3119
|
+
onClick: () => {
|
|
3120
|
+
if (snapshot.testingRole === row.role) store.cancelProviderTest();
|
|
3121
|
+
else store.testRole(row.role);
|
|
3122
|
+
},
|
|
3123
|
+
children: snapshot.testingRole === row.role ? "Cancel provider test" : "Run provider test"
|
|
3124
|
+
}),
|
|
3125
|
+
row.model === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3126
|
+
type: "button",
|
|
3127
|
+
style: CONTROL.quiet,
|
|
3128
|
+
disabled: !snapshot.writable || snapshot.saving,
|
|
3129
|
+
"aria-describedby": snapshot.writable ? void 0 : "watch-bindings-readonly",
|
|
3130
|
+
onClick: () => {
|
|
3131
|
+
store.unbind(row.role);
|
|
3132
|
+
},
|
|
3133
|
+
children: `Unassign ${ROLE_LABEL[row.role]}`
|
|
3134
|
+
})
|
|
3135
|
+
]
|
|
3136
|
+
})
|
|
3137
|
+
]
|
|
3138
|
+
});
|
|
3139
|
+
}
|
|
3140
|
+
/** The providers a person has actually configured, and what is known of each. */
|
|
3141
|
+
function ProviderList({ providers }) {
|
|
3142
|
+
const configured = providers.filter((entry) => entry.credential !== "absent");
|
|
3143
|
+
if (configured.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3144
|
+
style: {
|
|
3145
|
+
...T.card,
|
|
3146
|
+
borderStyle: "dashed"
|
|
3147
|
+
},
|
|
3148
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3149
|
+
style: T.cardHead,
|
|
3150
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
3151
|
+
style: T.title,
|
|
3152
|
+
children: "No provider is configured yet"
|
|
3153
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusChip, {
|
|
3154
|
+
tone: "neutral",
|
|
3155
|
+
children: "No credential"
|
|
3156
|
+
})]
|
|
3157
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3158
|
+
style: {
|
|
3159
|
+
...T.lead,
|
|
3160
|
+
margin: "8px 0 0"
|
|
3161
|
+
},
|
|
3162
|
+
children: "Add a credential in the Harness’s own Models & Providers screen. A saved credential is the first of four steps — it stores a key and assigns nothing."
|
|
3163
|
+
})]
|
|
3164
|
+
});
|
|
3165
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3166
|
+
style: T.card,
|
|
3167
|
+
children: [
|
|
3168
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3169
|
+
style: T.cardHead,
|
|
3170
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
3171
|
+
style: T.title,
|
|
3172
|
+
children: `${String(37)} routes are available`
|
|
3173
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusChip, {
|
|
3174
|
+
tone: "neutral",
|
|
3175
|
+
children: `${String(configured.length)} configured`
|
|
3176
|
+
})]
|
|
3177
|
+
}),
|
|
3178
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
3179
|
+
style: {
|
|
3180
|
+
...T.lead,
|
|
3181
|
+
margin: "8px 0 0"
|
|
3182
|
+
},
|
|
3183
|
+
children: [
|
|
3184
|
+
`${String(30)} hosted and ${String(7)} where you supply the endpoint — `,
|
|
3185
|
+
SAMPLE_PROVIDERS.join(", "),
|
|
3186
|
+
" and others. Watch adds none of these and removes none of them: the ",
|
|
3187
|
+
"catalogue is the Harness’s, reached through its own Models & Providers screen."
|
|
3188
|
+
]
|
|
3189
|
+
}),
|
|
3190
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3191
|
+
style: T.meta,
|
|
3192
|
+
children: [
|
|
3193
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3194
|
+
style: T.key,
|
|
3195
|
+
children: "A local model"
|
|
3196
|
+
}),
|
|
3197
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3198
|
+
style: T.value,
|
|
3199
|
+
children: "An OpenAI-compatible server you run — Ollama, vLLM, LM Studio, llama.cpp — is a base URL you supply, not a separate feature."
|
|
3200
|
+
}),
|
|
3201
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3202
|
+
style: T.key,
|
|
3203
|
+
children: "DeepSeek"
|
|
3204
|
+
}),
|
|
3205
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3206
|
+
style: T.value,
|
|
3207
|
+
children: "One route among many. Nothing here requires it, and nothing selects it for you."
|
|
3208
|
+
})
|
|
3209
|
+
]
|
|
3210
|
+
})
|
|
3211
|
+
]
|
|
3212
|
+
}), configured.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3213
|
+
style: T.card,
|
|
3214
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3215
|
+
style: T.cardHead,
|
|
3216
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
3217
|
+
style: T.title,
|
|
3218
|
+
children: entry.displayName
|
|
3219
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusChip, {
|
|
3220
|
+
tone: credentialTone(entry.credential),
|
|
3221
|
+
children: CREDENTIAL_STATUS_LABEL[entry.credential]
|
|
3222
|
+
})]
|
|
3223
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3224
|
+
style: T.meta,
|
|
3225
|
+
children: [
|
|
3226
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3227
|
+
style: T.key,
|
|
3228
|
+
children: "Route"
|
|
3229
|
+
}),
|
|
3230
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3231
|
+
style: T.value,
|
|
3232
|
+
children: entry.active ? "Served by an adapter" : "Not currently served"
|
|
3233
|
+
}),
|
|
3234
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3235
|
+
style: T.key,
|
|
3236
|
+
children: "Models offered"
|
|
3237
|
+
}),
|
|
3238
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3239
|
+
style: T.value,
|
|
3240
|
+
children: entry.catalogError !== null ? "The provider did not return a list." : entry.models.length === 0 ? "None advertised." : String(entry.models.length)
|
|
3241
|
+
}),
|
|
3242
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3243
|
+
style: T.key,
|
|
3244
|
+
children: "Credential save"
|
|
3245
|
+
}),
|
|
3246
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3247
|
+
style: T.value,
|
|
3248
|
+
children: "Does not contact the provider. Tests are explicit and apply to one exact provider/model binding."
|
|
3249
|
+
})
|
|
3250
|
+
]
|
|
3251
|
+
})]
|
|
3252
|
+
}, entry.provider))] });
|
|
3253
|
+
}
|
|
3254
|
+
/**
|
|
3255
|
+
* Role Bindings.
|
|
3256
|
+
*
|
|
3257
|
+
* @param props.store - the binding store this surface reads and writes.
|
|
3258
|
+
* @returns the section body.
|
|
3259
|
+
*/
|
|
3260
|
+
function RoleBindings({ store }) {
|
|
3261
|
+
const snapshot = (0, react.useSyncExternalStore)(store.subscribe, store.getSnapshot, store.getSnapshot);
|
|
3262
|
+
const [editing, setEditing] = (0, react.useState)(null);
|
|
3263
|
+
(0, react.useEffect)(() => {
|
|
3264
|
+
store.load();
|
|
3265
|
+
}, [store]);
|
|
3266
|
+
const onEdit = (0, react.useCallback)((role) => {
|
|
3267
|
+
setEditing(role);
|
|
3268
|
+
}, []);
|
|
3269
|
+
const onClose = (0, react.useCallback)(() => {
|
|
3270
|
+
setEditing(null);
|
|
3271
|
+
}, []);
|
|
3272
|
+
const chat = (0, react.useMemo)(() => snapshot.roles.find((row) => row.role === "agent_model") ?? null, [snapshot.roles]);
|
|
3273
|
+
if (snapshot.status === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3274
|
+
style: T.page,
|
|
3275
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3276
|
+
style: {
|
|
3277
|
+
...T.card,
|
|
3278
|
+
borderStyle: "dashed"
|
|
3279
|
+
},
|
|
3280
|
+
children: [
|
|
3281
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3282
|
+
style: T.cardHead,
|
|
3283
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
3284
|
+
style: T.title,
|
|
3285
|
+
children: "Settings could not be read"
|
|
3286
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusChip, {
|
|
3287
|
+
tone: "caution",
|
|
3288
|
+
children: "Unavailable"
|
|
3289
|
+
})]
|
|
3290
|
+
}),
|
|
3291
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3292
|
+
style: {
|
|
3293
|
+
...T.lead,
|
|
3294
|
+
margin: "8px 0 0"
|
|
3295
|
+
},
|
|
3296
|
+
children: "The Harness did not answer. Nothing has been changed, and no capability has been reconfigured."
|
|
3297
|
+
}),
|
|
3298
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3299
|
+
style: CONTROL.row,
|
|
3300
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3301
|
+
type: "button",
|
|
3302
|
+
style: CONTROL.primary,
|
|
3303
|
+
onClick: () => {
|
|
3304
|
+
store.load();
|
|
3305
|
+
},
|
|
3306
|
+
children: "Try again"
|
|
3307
|
+
})
|
|
3308
|
+
})
|
|
3309
|
+
]
|
|
3310
|
+
})
|
|
3311
|
+
});
|
|
3312
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3313
|
+
style: T.page,
|
|
3314
|
+
children: [
|
|
3315
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3316
|
+
style: T.lead,
|
|
3317
|
+
children: "A capability is assigned per role, not per provider. One provider may serve several roles, and a role with nothing assigned says so — it never falls back to another role’s model, and a stored credential never assigns itself."
|
|
3318
|
+
}),
|
|
3319
|
+
snapshot.writable ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3320
|
+
id: "watch-bindings-readonly",
|
|
3321
|
+
style: {
|
|
3322
|
+
...T.card,
|
|
3323
|
+
borderStyle: "dashed"
|
|
3324
|
+
},
|
|
3325
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3326
|
+
style: T.cardHead,
|
|
3327
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
3328
|
+
style: T.title,
|
|
3329
|
+
children: "Settings are read-only"
|
|
3330
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusChip, {
|
|
3331
|
+
tone: "caution",
|
|
3332
|
+
children: "Not writable"
|
|
3333
|
+
})]
|
|
3334
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3335
|
+
style: {
|
|
3336
|
+
...T.lead,
|
|
3337
|
+
margin: "8px 0 0"
|
|
3338
|
+
},
|
|
3339
|
+
children: "This Workspace was opened against a configuration the Harness will not let it change, so assignments cannot be edited here. Nothing is wrong with the capabilities themselves — what is shown below is accurate and read from the running system."
|
|
3340
|
+
})]
|
|
3341
|
+
}),
|
|
3342
|
+
chat !== null && !isExecutable(chat.readiness) ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3343
|
+
style: {
|
|
3344
|
+
...T.card,
|
|
3345
|
+
borderColor: "var(--watch-accent)"
|
|
3346
|
+
},
|
|
3347
|
+
children: [
|
|
3348
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3349
|
+
style: T.cardHead,
|
|
3350
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
3351
|
+
style: T.title,
|
|
3352
|
+
children: "Chat is not ready yet"
|
|
3353
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusChip, {
|
|
3354
|
+
tone: "neutral",
|
|
3355
|
+
children: BINDING_STATUS_LABEL[chat.readiness.status]
|
|
3356
|
+
})]
|
|
3357
|
+
}),
|
|
3358
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
3359
|
+
style: {
|
|
3360
|
+
...T.lead,
|
|
3361
|
+
margin: "8px 0 0"
|
|
3362
|
+
},
|
|
3363
|
+
children: [blockerMessage(chat.readiness) ?? "Choose a provider and model for Chat.", " Until then the composer stays closed and nothing is sent."]
|
|
3364
|
+
}),
|
|
3365
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3366
|
+
style: CONTROL.row,
|
|
3367
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3368
|
+
type: "button",
|
|
3369
|
+
style: CONTROL.primary,
|
|
3370
|
+
disabled: !snapshot.writable,
|
|
3371
|
+
"aria-describedby": snapshot.writable ? void 0 : "watch-bindings-readonly",
|
|
3372
|
+
onClick: () => {
|
|
3373
|
+
setEditing(PRIMARY_ROLE);
|
|
3374
|
+
},
|
|
3375
|
+
children: "Choose models and roles"
|
|
3376
|
+
})
|
|
3377
|
+
})
|
|
3378
|
+
]
|
|
3379
|
+
}) : null,
|
|
3380
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3381
|
+
role: "status",
|
|
3382
|
+
"aria-live": "polite",
|
|
3383
|
+
style: {
|
|
3384
|
+
margin: "0 0 10px",
|
|
3385
|
+
fontSize: "12px",
|
|
3386
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
3387
|
+
minHeight: "1em"
|
|
3388
|
+
},
|
|
3389
|
+
children: snapshot.saving ? "Saving the assignment…" : snapshot.testingRole !== null ? `Testing ${ROLE_LABEL[snapshot.testingRole]} with one bounded provider request…` : snapshot.testMessage !== null ? snapshot.testMessage : snapshot.error !== null ? snapshot.error : ""
|
|
3390
|
+
}),
|
|
3391
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
3392
|
+
style: T.h2,
|
|
3393
|
+
children: "Capabilities"
|
|
3394
|
+
}),
|
|
3395
|
+
BINDABLE_ROLES.map((role) => {
|
|
3396
|
+
const row = snapshot.roles.find((entry) => entry.role === role);
|
|
3397
|
+
return row === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RoleCard, {
|
|
3398
|
+
row,
|
|
3399
|
+
providers: snapshot.providers,
|
|
3400
|
+
snapshot,
|
|
3401
|
+
store,
|
|
3402
|
+
openEditor: editing === role,
|
|
3403
|
+
editing,
|
|
3404
|
+
onEdit,
|
|
3405
|
+
onClose
|
|
3406
|
+
}, role);
|
|
3407
|
+
}),
|
|
3408
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
3409
|
+
style: T.h2,
|
|
3410
|
+
children: "Providers"
|
|
3411
|
+
}),
|
|
3412
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProviderList, { providers: snapshot.providers }),
|
|
3413
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3414
|
+
style: T.note,
|
|
3415
|
+
children: "Assignments are stored beside the Harness’s own settings, and reference a credential rather than holding one. Watch keeps no second credential store and never sees a key."
|
|
3416
|
+
})
|
|
3417
|
+
]
|
|
3418
|
+
});
|
|
3419
|
+
}
|
|
3420
|
+
//#endregion
|
|
3421
|
+
//#region src/client/chat-gate.tsx
|
|
3422
|
+
/**
|
|
3423
|
+
* The composer, closed until something is actually bound to Chat — and the
|
|
3424
|
+
* fastest way to bind it, offered where the person is standing.
|
|
3425
|
+
*
|
|
3426
|
+
* A person typed a message into a composer that looked ready, pressed send,
|
|
3427
|
+
* and the prompt was routed to a provider they had never configured. Nothing
|
|
3428
|
+
* about the composer had told them otherwise: the model chip named a DeepSeek
|
|
3429
|
+
* model they had not chosen, the send button was live, and the first thing
|
|
3430
|
+
* that disagreed was a failed turn.
|
|
3431
|
+
*
|
|
3432
|
+
* This is the client half of preflight. It raises the block upstream provides
|
|
3433
|
+
* for exactly this — *"Composer blocks: the one way another plugin stops a
|
|
3434
|
+
* session's input"* — so the textarea goes inert, the send button stops
|
|
3435
|
+
* accepting, and the placeholder says why in the words of whoever knows.
|
|
3436
|
+
*
|
|
3437
|
+
* **This is an affordance, and it is not the enforcement.** It lives in a
|
|
3438
|
+
* browser tab. The Host refuses an unbound route regardless of what any client
|
|
3439
|
+
* disables (`@deepwatch/dsh-technology/routing`), and the composed default
|
|
3440
|
+
* names no route so the Harness's own admission boundary refuses a turn before
|
|
3441
|
+
* one exists. The value of this layer is not that it makes refusal certain — it
|
|
3442
|
+
* is that a person finds out *before* typing rather than after sending.
|
|
3443
|
+
*
|
|
3444
|
+
* **The draft survives.** Blocking is one inert textarea, never a second tree:
|
|
3445
|
+
* the composer keeps its DOM, so whatever was typed is still there when the
|
|
3446
|
+
* binding is fixed. That is upstream's design and this file's reason for using
|
|
3447
|
+
* it rather than rendering a replacement.
|
|
3448
|
+
*
|
|
3449
|
+
* **The way out is here, not somewhere else.** The settings panel's open state
|
|
3450
|
+
* is component-local to `SettingsRoot`, so no plugin can navigate to a section
|
|
3451
|
+
* — and a button that names a screen it cannot open is worse than no button.
|
|
3452
|
+
* So the fix is offered inline: the same provider-and-model picker the Role
|
|
3453
|
+
* Bindings screen uses, writing through the same store, so the two surfaces
|
|
3454
|
+
* cannot disagree about what a provider offers.
|
|
3455
|
+
*
|
|
3456
|
+
* @module @deepwatch/dsh-client-settings/chat-gate
|
|
3457
|
+
*/
|
|
3458
|
+
/**
|
|
3459
|
+
* The placeholder an inert composer carries.
|
|
3460
|
+
*
|
|
3461
|
+
* The blocker's own sentence, prefixed with the capability it is about. A
|
|
3462
|
+
* placeholder reading only "Not configured" leaves a person guessing which of
|
|
3463
|
+
* six things is missing, which is the failure the ordered blocker list exists
|
|
3464
|
+
* to prevent.
|
|
3465
|
+
*/
|
|
3466
|
+
function blockReason(detail) {
|
|
3467
|
+
return `${ROLE_LABEL[PRIMARY_ROLE]} is not ready — ${detail}`;
|
|
3468
|
+
}
|
|
3469
|
+
/**
|
|
3470
|
+
* The block this snapshot calls for, or undefined when the composer may open.
|
|
3471
|
+
*
|
|
3472
|
+
* A pure function rather than a branch inside the effect, so the decision can
|
|
3473
|
+
* be tested as the decision. A test that re-derived the same condition beside
|
|
3474
|
+
* the component would agree with itself and prove nothing about what a person
|
|
3475
|
+
* gets.
|
|
3476
|
+
*
|
|
3477
|
+
* `idle` and `loading` deliberately produce nothing. Blocking a composer
|
|
3478
|
+
* because an answer has not arrived yet would make every reload look like a
|
|
3479
|
+
* misconfiguration — and the Host refuses an unbound route regardless, so the
|
|
3480
|
+
* safe direction here is to say nothing until something is known.
|
|
3481
|
+
*
|
|
3482
|
+
* @param snapshot - what the store currently knows.
|
|
3483
|
+
* @returns the block to raise, or undefined to lift any standing one.
|
|
3484
|
+
*/
|
|
3485
|
+
function blockFor(snapshot) {
|
|
3486
|
+
if (snapshot.status !== "ready") return void 0;
|
|
3487
|
+
const chat = chatReadiness(snapshot);
|
|
3488
|
+
if (chat === null || isExecutable(chat) || chat.primaryBlocker === null) return void 0;
|
|
3489
|
+
return { reason: blockReason(cardForBlocker(chat.primaryBlocker).detail) };
|
|
3490
|
+
}
|
|
3491
|
+
/**
|
|
3492
|
+
* Raise or clear this session's composer block, and offer the way out.
|
|
3493
|
+
*
|
|
3494
|
+
* @param props - see {@link ChatGateProps}.
|
|
3495
|
+
* @returns the setup card while Chat cannot run, and nothing once it can.
|
|
3496
|
+
*/
|
|
3497
|
+
function ChatGate({ sessionId, store, blocks }) {
|
|
3498
|
+
const snapshot = (0, react.useSyncExternalStore)(store.subscribe, store.getSnapshot, store.getSnapshot);
|
|
3499
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
3500
|
+
(0, react.useEffect)(() => {
|
|
3501
|
+
store.load();
|
|
3502
|
+
}, [store]);
|
|
3503
|
+
const chat = chatReadiness(snapshot);
|
|
3504
|
+
const ready = chat !== null && isExecutable(chat);
|
|
3505
|
+
const card = chat === null || chat.primaryBlocker === null ? null : cardForBlocker(chat.primaryBlocker);
|
|
3506
|
+
const block = blockFor(snapshot);
|
|
3507
|
+
(0, react.useEffect)(() => {
|
|
3508
|
+
const registry = blocks();
|
|
3509
|
+
if (registry === void 0) return;
|
|
3510
|
+
registry.set(sessionId, block);
|
|
3511
|
+
return () => {
|
|
3512
|
+
registry.set(sessionId, void 0);
|
|
3513
|
+
};
|
|
3514
|
+
}, [
|
|
3515
|
+
blocks,
|
|
3516
|
+
sessionId,
|
|
3517
|
+
block?.reason
|
|
3518
|
+
]);
|
|
3519
|
+
if (ready || card === null || chat === null) return null;
|
|
3520
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
3521
|
+
"aria-label": `${ROLE_LABEL[PRIMARY_ROLE]} setup`,
|
|
3522
|
+
style: {
|
|
3523
|
+
border: "1px solid color-mix(in srgb, var(--watch-accent) 68%, var(--dsw-alias-border-l2))",
|
|
3524
|
+
borderRadius: "14px",
|
|
3525
|
+
padding: "15px 17px",
|
|
3526
|
+
margin: "0 0 8px",
|
|
3527
|
+
background: "linear-gradient(145deg, color-mix(in srgb, var(--watch-accent) 7%, var(--dsw-alias-bg-layer-2)), var(--dsw-alias-bg-base))",
|
|
3528
|
+
boxShadow: "0 10px 30px color-mix(in srgb, var(--watch-accent) 9%, transparent)"
|
|
3529
|
+
},
|
|
3530
|
+
children: [
|
|
3531
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3532
|
+
style: {
|
|
3533
|
+
display: "flex",
|
|
3534
|
+
alignItems: "baseline",
|
|
3535
|
+
gap: "10px",
|
|
3536
|
+
justifyContent: "space-between",
|
|
3537
|
+
flexWrap: "wrap"
|
|
3538
|
+
},
|
|
3539
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
3540
|
+
style: {
|
|
3541
|
+
fontSize: "13px",
|
|
3542
|
+
fontWeight: 600,
|
|
3543
|
+
margin: 0
|
|
3544
|
+
},
|
|
3545
|
+
children: card.title
|
|
3546
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusChip, {
|
|
3547
|
+
tone: "neutral",
|
|
3548
|
+
children: chat.status === "bound_unverified" ? "Configured · not tested" : "Not ready"
|
|
3549
|
+
})]
|
|
3550
|
+
}),
|
|
3551
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3552
|
+
style: {
|
|
3553
|
+
fontSize: "12px",
|
|
3554
|
+
lineHeight: 1.55,
|
|
3555
|
+
margin: "6px 0 0",
|
|
3556
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
3557
|
+
},
|
|
3558
|
+
children: card.detail
|
|
3559
|
+
}),
|
|
3560
|
+
open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BindingEditor, {
|
|
3561
|
+
row: {
|
|
3562
|
+
role: PRIMARY_ROLE,
|
|
3563
|
+
provider: null,
|
|
3564
|
+
model: null,
|
|
3565
|
+
readiness: chat
|
|
3566
|
+
},
|
|
3567
|
+
providers: snapshot.providers,
|
|
3568
|
+
saving: snapshot.saving,
|
|
3569
|
+
onBind: (provider, model) => {
|
|
3570
|
+
store.bind(PRIMARY_ROLE, provider, model).then(() => {
|
|
3571
|
+
setOpen(false);
|
|
3572
|
+
});
|
|
3573
|
+
},
|
|
3574
|
+
onCancel: () => {
|
|
3575
|
+
setOpen(false);
|
|
3576
|
+
}
|
|
3577
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3578
|
+
style: CONTROL.row,
|
|
3579
|
+
children: [
|
|
3580
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3581
|
+
type: "button",
|
|
3582
|
+
style: CONTROL.primary,
|
|
3583
|
+
disabled: !snapshot.writable || snapshot.testingRole !== null,
|
|
3584
|
+
"aria-describedby": snapshot.writable ? void 0 : "watch-chat-gate-readonly",
|
|
3585
|
+
onClick: () => {
|
|
3586
|
+
if (chat.primaryBlocker === "provider_untested") store.testRole(PRIMARY_ROLE);
|
|
3587
|
+
else setOpen(true);
|
|
3588
|
+
},
|
|
3589
|
+
children: snapshot.testingRole === "agent_model" ? "Testing provider…" : card.action
|
|
3590
|
+
}),
|
|
3591
|
+
snapshot.writable ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3592
|
+
id: "watch-chat-gate-readonly",
|
|
3593
|
+
style: {
|
|
3594
|
+
fontSize: "12px",
|
|
3595
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
3596
|
+
},
|
|
3597
|
+
children: "This Workspace cannot change settings here, so Chat cannot be bound from this screen. What is shown is still read from the running system."
|
|
3598
|
+
}),
|
|
3599
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3600
|
+
style: {
|
|
3601
|
+
fontSize: "12px",
|
|
3602
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
3603
|
+
},
|
|
3604
|
+
children: "Settings → Role Bindings has the full view."
|
|
3605
|
+
})
|
|
3606
|
+
]
|
|
3607
|
+
})
|
|
3608
|
+
]
|
|
3609
|
+
});
|
|
3610
|
+
}
|
|
3611
|
+
//#endregion
|
|
3612
|
+
//#region src/client/index.tsx
|
|
3613
|
+
/**
|
|
3614
|
+
* The Watch Technology & Capability Center, registered into DSH's settings.
|
|
3615
|
+
*
|
|
3616
|
+
* `settings.section` is a list, so these seven sit alongside DSH's General,
|
|
3617
|
+
* Models and Plugins rather than replacing any of them. That is the whole
|
|
3618
|
+
* arrangement: the foundation keeps its settings, and Watch adds the surfaces
|
|
3619
|
+
* that make its own capabilities inspectable.
|
|
3620
|
+
*
|
|
3621
|
+
* The `order` values start at 20 so upstream's sections keep the top of the
|
|
3622
|
+
* list, and they are grouped so the reading order tells a story — what the
|
|
3623
|
+
* agent is (Intelligence), what it can perceive (Perception), what it keeps
|
|
3624
|
+
* (Memory), what it can prove (Truth), and what it actually is (System).
|
|
3625
|
+
*
|
|
3626
|
+
* @module @deepwatch/dsh-client-settings/client
|
|
3627
|
+
*/
|
|
3628
|
+
/**
|
|
3629
|
+
* Services this half needs before it can register anything.
|
|
3630
|
+
*
|
|
3631
|
+
* `connection` is what makes Role Bindings a working screen rather than a
|
|
3632
|
+
* description of one: it carries the RPC face this package asks for the
|
|
3633
|
+
* provider directory, the model catalogue, credential state and the stored
|
|
3634
|
+
* assignments. Registering the section without it would put a control on
|
|
3635
|
+
* screen that fails when pressed.
|
|
3636
|
+
*/
|
|
3637
|
+
const inject = [
|
|
3638
|
+
"slots",
|
|
3639
|
+
"connection",
|
|
3640
|
+
"remote",
|
|
3641
|
+
"remote.watchQuery"
|
|
3642
|
+
];
|
|
3643
|
+
/** One live Core reading shared by onboarding and Diagnostics. */
|
|
3644
|
+
var CoreHealthStore = class {
|
|
3645
|
+
reader;
|
|
3646
|
+
snapshot = {
|
|
3647
|
+
health: null,
|
|
3648
|
+
reading: true,
|
|
3649
|
+
loaded: false
|
|
3650
|
+
};
|
|
3651
|
+
listeners = /* @__PURE__ */ new Set();
|
|
3652
|
+
generation = 0;
|
|
3653
|
+
inFlight = null;
|
|
3654
|
+
constructor(reader) {
|
|
3655
|
+
this.reader = reader;
|
|
3656
|
+
}
|
|
3657
|
+
getSnapshot = () => this.snapshot;
|
|
3658
|
+
subscribe = (listener) => {
|
|
3659
|
+
this.listeners.add(listener);
|
|
3660
|
+
return () => {
|
|
3661
|
+
this.listeners.delete(listener);
|
|
3662
|
+
};
|
|
3663
|
+
};
|
|
3664
|
+
publish(snapshot) {
|
|
3665
|
+
this.snapshot = snapshot;
|
|
3666
|
+
for (const listener of this.listeners) listener();
|
|
3667
|
+
}
|
|
3668
|
+
/**
|
|
3669
|
+
* Read once, however many surfaces ask.
|
|
3670
|
+
*
|
|
3671
|
+
* Onboarding and Diagnostics mount independently and both need the same
|
|
3672
|
+
* reading. Firing a second Core handshake because a second surface appeared
|
|
3673
|
+
* is two readings that can disagree, and the one a person is looking at
|
|
3674
|
+
* would be whichever landed last.
|
|
3675
|
+
*/
|
|
3676
|
+
ensureLoaded() {
|
|
3677
|
+
if (this.snapshot.loaded || this.inFlight !== null) return;
|
|
3678
|
+
this.inFlight = this.refresh().finally(() => {
|
|
3679
|
+
this.inFlight = null;
|
|
3680
|
+
});
|
|
3681
|
+
}
|
|
3682
|
+
async refresh() {
|
|
3683
|
+
const generation = ++this.generation;
|
|
3684
|
+
this.publish({
|
|
3685
|
+
...this.snapshot,
|
|
3686
|
+
reading: true
|
|
3687
|
+
});
|
|
3688
|
+
const reader = this.reader();
|
|
3689
|
+
if (reader === null) {
|
|
3690
|
+
this.publish({
|
|
3691
|
+
health: null,
|
|
3692
|
+
reading: false,
|
|
3693
|
+
loaded: true
|
|
3694
|
+
});
|
|
3695
|
+
return;
|
|
3696
|
+
}
|
|
3697
|
+
try {
|
|
3698
|
+
const result = await reader({
|
|
3699
|
+
protocol: 1,
|
|
3700
|
+
requestId: `req_${Date.now().toString(36)}`,
|
|
3701
|
+
deadlineMs: 15e3
|
|
3702
|
+
}, new AbortController().signal);
|
|
3703
|
+
if (generation !== this.generation) return;
|
|
3704
|
+
const report = result.ok ? result.value : null;
|
|
3705
|
+
this.publish({
|
|
3706
|
+
health: report?.outcome === "core_health" ? report : null,
|
|
3707
|
+
reading: false,
|
|
3708
|
+
loaded: true
|
|
3709
|
+
});
|
|
3710
|
+
} catch {
|
|
3711
|
+
if (generation === this.generation) this.publish({
|
|
3712
|
+
health: null,
|
|
3713
|
+
reading: false,
|
|
3714
|
+
loaded: true
|
|
3715
|
+
});
|
|
3716
|
+
}
|
|
3717
|
+
}
|
|
3718
|
+
};
|
|
3719
|
+
/** Add the Watch sections to DSH's settings panel. */
|
|
3720
|
+
function apply(ctx) {
|
|
3721
|
+
const slots = ctx.slots;
|
|
3722
|
+
/**
|
|
3723
|
+
* The channel to the engine, looked up when Diagnostics renders.
|
|
3724
|
+
*
|
|
3725
|
+
* Deliberately *not* an `inject` entry. Naming `remote.watchQuery` there
|
|
3726
|
+
* parks this whole plugin until the service exists, and the cost of being
|
|
3727
|
+
* wrong about that is not a missing Health card — it is every Watch settings
|
|
3728
|
+
* section disappearing, which is what happened: "View diagnostics" became a
|
|
3729
|
+
* button that did nothing. The same mistake is recorded twenty lines below
|
|
3730
|
+
* about `conversation`, and it is the same mistake.
|
|
3731
|
+
*
|
|
3732
|
+
* So this is read at the point of use, and its absence is a state the panel
|
|
3733
|
+
* already renders honestly: "could not be read" rather than a plausible
|
|
3734
|
+
* default. A screen that says it does not know beats a screen that is not
|
|
3735
|
+
* there.
|
|
3736
|
+
*/
|
|
3737
|
+
const readHealth = () => {
|
|
3738
|
+
return ctx.remote?.watchQuery?.coreHealth ?? null;
|
|
3739
|
+
};
|
|
3740
|
+
const runProviderTest = async (provider, model, signal) => {
|
|
3741
|
+
const method = ctx.remote?.watchQuery?.providerTest;
|
|
3742
|
+
if (method === void 0) throw new Error("provider test remote unavailable");
|
|
3743
|
+
const result = await method({
|
|
3744
|
+
protocol: 1,
|
|
3745
|
+
requestId: `req_provider_${Date.now().toString(36)}`,
|
|
3746
|
+
deadlineMs: 15e3,
|
|
3747
|
+
provider,
|
|
3748
|
+
model
|
|
3749
|
+
}, signal);
|
|
3750
|
+
if (!result.ok) throw new Error("provider test remote refused the request");
|
|
3751
|
+
const value = result.value;
|
|
3752
|
+
if (value.outcome !== "provider_test" || value.provider !== provider || value.model !== model) throw new Error("provider test returned a mismatched response");
|
|
3753
|
+
return value;
|
|
3754
|
+
};
|
|
3755
|
+
/**
|
|
3756
|
+
* The Host's verdict on a route, read rather than remembered.
|
|
3757
|
+
*
|
|
3758
|
+
* Shaped like `runProviderTest` and deliberately not like it: this spends
|
|
3759
|
+
* nothing, reaches no provider, and is asked on every load. It is what stops
|
|
3760
|
+
* a tab that has been open across a Host restart, or across an edit made
|
|
3761
|
+
* somewhere else, from drawing a tested badge over a route the Host would
|
|
3762
|
+
* refuse.
|
|
3763
|
+
*/
|
|
3764
|
+
const readRouteReadiness = async (provider, model, signal) => {
|
|
3765
|
+
const method = ctx.remote?.watchQuery?.routeReadiness;
|
|
3766
|
+
if (method === void 0) throw new Error("route readiness remote unavailable");
|
|
3767
|
+
const result = await method({
|
|
3768
|
+
protocol: 1,
|
|
3769
|
+
requestId: `req_readiness_${Date.now().toString(36)}`,
|
|
3770
|
+
deadlineMs: 5e3,
|
|
3771
|
+
provider,
|
|
3772
|
+
model
|
|
3773
|
+
}, signal);
|
|
3774
|
+
if (!result.ok) throw new Error("route readiness remote refused the request");
|
|
3775
|
+
const value = result.value;
|
|
3776
|
+
if (value.outcome !== "route_readiness" || value.provider !== provider || value.model !== model) throw new Error("route readiness returned a mismatched response");
|
|
3777
|
+
return {
|
|
3778
|
+
proved: value.proved,
|
|
3779
|
+
reason: value.reason
|
|
3780
|
+
};
|
|
3781
|
+
};
|
|
3782
|
+
const store = new BindingStore(ctx.get("connection").api, runProviderTest, readRouteReadiness);
|
|
3783
|
+
const RoleBindingsSection = () => RoleBindings({ store });
|
|
3784
|
+
const healthStore = new CoreHealthStore(readHealth);
|
|
3785
|
+
const DiagnosticsWithRoles = () => {
|
|
3786
|
+
const snapshot = (0, react.useSyncExternalStore)(store.subscribe, store.getSnapshot, store.getSnapshot);
|
|
3787
|
+
(0, react.useEffect)(() => {
|
|
3788
|
+
store.load();
|
|
3789
|
+
}, []);
|
|
3790
|
+
const core = (0, react.useSyncExternalStore)(healthStore.subscribe, healthStore.getSnapshot, healthStore.getSnapshot);
|
|
3791
|
+
(0, react.useEffect)(() => {
|
|
3792
|
+
healthStore.ensureLoaded();
|
|
3793
|
+
}, []);
|
|
3794
|
+
return DiagnosticsSection({
|
|
3795
|
+
roles: snapshot.roles,
|
|
3796
|
+
health: core.health,
|
|
3797
|
+
reading: core.reading,
|
|
3798
|
+
onRefresh: () => {
|
|
3799
|
+
healthStore.refresh();
|
|
3800
|
+
}
|
|
3801
|
+
});
|
|
3802
|
+
};
|
|
3803
|
+
const OnboardingWithReadiness = (props) => {
|
|
3804
|
+
const snapshot = (0, react.useSyncExternalStore)(store.subscribe, store.getSnapshot, store.getSnapshot);
|
|
3805
|
+
const core = (0, react.useSyncExternalStore)(healthStore.subscribe, healthStore.getSnapshot, healthStore.getSnapshot);
|
|
3806
|
+
(0, react.useEffect)(() => {
|
|
3807
|
+
store.load();
|
|
3808
|
+
healthStore.ensureLoaded();
|
|
3809
|
+
}, []);
|
|
3810
|
+
return WatchOnboarding({
|
|
3811
|
+
...props,
|
|
3812
|
+
roles: snapshot.roles,
|
|
3813
|
+
health: core.health,
|
|
3814
|
+
reading: core.reading
|
|
3815
|
+
});
|
|
3816
|
+
};
|
|
3817
|
+
const section = (id, label, order, component) => {
|
|
3818
|
+
slots.inject("settings.section", () => {
|
|
3819
|
+
slots.register({
|
|
3820
|
+
name: "settings.section",
|
|
3821
|
+
id,
|
|
3822
|
+
label,
|
|
3823
|
+
order
|
|
3824
|
+
}, component);
|
|
3825
|
+
});
|
|
3826
|
+
};
|
|
3827
|
+
section("watch-roles", "Role Bindings", 20, RoleBindingsSection);
|
|
3828
|
+
section("watch-engines", "Perception", 30, EnginesSection);
|
|
3829
|
+
section("watch-sources", "Sources", 40, SourcesSection);
|
|
3830
|
+
section("watch-memory", "Memory", 50, MemorySection);
|
|
3831
|
+
section("watch-verification", "Verification", 60, VerificationSection);
|
|
3832
|
+
section("watch-diagnostics", "Diagnostics", 70, DiagnosticsWithRoles);
|
|
3833
|
+
section("watch-about", "About", 80, AboutSection);
|
|
3834
|
+
const blocks = () => ctx.get("conversation")?.blocks;
|
|
3835
|
+
slots.inject("conversation.input.dock", () => {
|
|
3836
|
+
slots.register({
|
|
3837
|
+
name: "conversation.input.dock",
|
|
3838
|
+
id: "watch-chat-gate",
|
|
3839
|
+
order: 5
|
|
3840
|
+
}, ({ sessionId }) => ChatGate({
|
|
3841
|
+
sessionId,
|
|
3842
|
+
store,
|
|
3843
|
+
blocks
|
|
3844
|
+
}));
|
|
3845
|
+
});
|
|
3846
|
+
slots.inject("settings.onboarding", () => {
|
|
3847
|
+
slots.register({
|
|
3848
|
+
name: "settings.onboarding",
|
|
3849
|
+
id: "watch-welcome",
|
|
3850
|
+
order: -50
|
|
3851
|
+
}, OnboardingWithReadiness);
|
|
3852
|
+
});
|
|
3853
|
+
}
|
|
3854
|
+
//#endregion
|
|
3855
|
+
exports.AboutSection = AboutSection;
|
|
3856
|
+
exports.BindingEditor = BindingEditor;
|
|
3857
|
+
exports.BindingStore = BindingStore;
|
|
3858
|
+
exports.CONTROL = CONTROL;
|
|
3859
|
+
exports.ChatGate = ChatGate;
|
|
3860
|
+
exports.DEFAULT_MODEL_NAMESPACE = DEFAULT_MODEL_NAMESPACE;
|
|
3861
|
+
exports.DiagnosticsSection = DiagnosticsSection;
|
|
3862
|
+
exports.EnginesSection = EnginesSection;
|
|
3863
|
+
exports.MemorySection = MemorySection;
|
|
3864
|
+
exports.NotConfigured = NotConfigured;
|
|
3865
|
+
exports.READINESS = READINESS;
|
|
3866
|
+
exports.READINESS_STATUS_LABEL = READINESS_STATUS_LABEL;
|
|
3867
|
+
exports.ReadinessList = ReadinessList;
|
|
3868
|
+
exports.RoleBindings = RoleBindings;
|
|
3869
|
+
exports.SourcesSection = SourcesSection;
|
|
3870
|
+
exports.StatusChip = StatusChip;
|
|
3871
|
+
exports.T = T;
|
|
3872
|
+
exports.VerificationSection = VerificationSection;
|
|
3873
|
+
exports.WatchOnboarding = WatchOnboarding;
|
|
3874
|
+
exports.apply = apply;
|
|
3875
|
+
exports.blockFor = blockFor;
|
|
3876
|
+
exports.blockReason = blockReason;
|
|
3877
|
+
exports.chatReadiness = chatReadiness;
|
|
3878
|
+
exports.credentialRefOf = credentialRefOf;
|
|
3879
|
+
exports.credentialStatusOf = credentialStatusOf;
|
|
3880
|
+
exports.deriveReadiness = deriveReadiness;
|
|
3881
|
+
exports.inject = inject;
|
|
3882
|
+
exports.providerTestKey = providerTestKey;
|
|
3883
|
+
exports.roleRowOf = roleRowOf;
|
|
3884
|
+
return module.exports;
|
|
3885
|
+
}
|
|
3886
|
+
});
|
|
3887
|
+
|
|
3888
|
+
//# sourceMappingURL=client.js.map
|