@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/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