@yadsh/dsh-qa-surface 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.
Files changed (61) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/LICENSE +21 -0
  3. package/README.md +135 -0
  4. package/SPEC-dsh-qa-surface.md +1921 -0
  5. package/compatibility.json +15 -0
  6. package/cordis.patch.yml +9 -0
  7. package/docs/ARCHITECTURE.md +24 -0
  8. package/docs/COMPATIBILITY.md +13 -0
  9. package/docs/CONFIGURATION.md +18 -0
  10. package/docs/EMBEDDING.md +13 -0
  11. package/lib/client/QaConfigController.js +65 -0
  12. package/lib/client/QaConfigController.js.map +1 -0
  13. package/lib/client/QaRouteController.js +109 -0
  14. package/lib/client/QaRouteController.js.map +1 -0
  15. package/lib/client/QaSessionController.js +366 -0
  16. package/lib/client/QaSessionController.js.map +1 -0
  17. package/lib/client/QaSurface.js +109 -0
  18. package/lib/client/QaSurface.js.map +1 -0
  19. package/lib/client/QaTranscriptAdapter.js +92 -0
  20. package/lib/client/QaTranscriptAdapter.js.map +1 -0
  21. package/lib/client/components/Markdown.js +102 -0
  22. package/lib/client/components/Markdown.js.map +1 -0
  23. package/lib/client/components/QaComposer.js +33 -0
  24. package/lib/client/components/QaComposer.js.map +1 -0
  25. package/lib/client/components/QaMessage.js +14 -0
  26. package/lib/client/components/QaMessage.js.map +1 -0
  27. package/lib/client/index.js +46 -0
  28. package/lib/client/index.js.map +1 -0
  29. package/lib/client/styles.js +58 -0
  30. package/lib/client/styles.js.map +1 -0
  31. package/lib/client.js +1131 -0
  32. package/lib/client.js.map +1 -0
  33. package/lib/config.js +83 -0
  34. package/lib/config.js.map +1 -0
  35. package/lib/host-route.js +27 -0
  36. package/lib/host-route.js.map +1 -0
  37. package/lib/index.js +83 -0
  38. package/lib/index.js.map +1 -0
  39. package/lib/navigation-marker.js +3 -0
  40. package/lib/navigation-marker.js.map +1 -0
  41. package/lib/resolve-config.js +143 -0
  42. package/lib/resolve-config.js.map +1 -0
  43. package/lib/types/client/QaConfigController.d.ts +21 -0
  44. package/lib/types/client/QaRouteController.d.ts +43 -0
  45. package/lib/types/client/QaSessionController.d.ts +59 -0
  46. package/lib/types/client/QaSurface.d.ts +16 -0
  47. package/lib/types/client/QaTranscriptAdapter.d.ts +7 -0
  48. package/lib/types/client/components/Markdown.d.ts +5 -0
  49. package/lib/types/client/components/QaComposer.d.ts +11 -0
  50. package/lib/types/client/components/QaMessage.d.ts +8 -0
  51. package/lib/types/client/index.d.ts +19 -0
  52. package/lib/types/client/styles.d.ts +2 -0
  53. package/lib/types/config.d.ts +5 -0
  54. package/lib/types/host-route.d.ts +9 -0
  55. package/lib/types/index.d.ts +29 -0
  56. package/lib/types/navigation-marker.d.ts +3 -0
  57. package/lib/types/resolve-config.d.ts +6 -0
  58. package/lib/types/types.d.ts +114 -0
  59. package/lib/types.js +2 -0
  60. package/lib/types.js.map +1 -0
  61. package/package.json +137 -0
package/lib/client.js ADDED
@@ -0,0 +1,1131 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@yadsh/dsh-qa-surface",
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
+ //#region src/resolve-config.ts
10
+ const DEFAULT_QA_SURFACE_CONFIG = Object.freeze({
11
+ enabled: true,
12
+ route: Object.freeze({
13
+ path: "/qa",
14
+ matchChildren: true
15
+ }),
16
+ branding: Object.freeze({
17
+ title: "Assistant",
18
+ subtitle: "",
19
+ welcomeMessage: "How can I help?",
20
+ placeholder: "Ask a question...",
21
+ logoUrl: null
22
+ }),
23
+ session: Object.freeze({
24
+ policy: "browser-persistent",
25
+ storageKey: "dsh-qa-surface",
26
+ workspaceId: null,
27
+ fixedSessionId: null,
28
+ agentPreset: null,
29
+ provider: null,
30
+ model: null,
31
+ reasoningEffort: null
32
+ }),
33
+ ui: Object.freeze({
34
+ showHeader: true,
35
+ showReset: true,
36
+ showStop: true,
37
+ showTimestamps: false,
38
+ showToolActivity: false,
39
+ showReasoning: false,
40
+ renderMarkdown: true,
41
+ maxContentWidth: 900
42
+ }),
43
+ suggestedQuestions: Object.freeze([]),
44
+ interaction: Object.freeze({
45
+ approvals: "blocked",
46
+ questions: "unsupported"
47
+ }),
48
+ embedding: Object.freeze({ frameAncestors: null })
49
+ });
50
+ function optionalText(value) {
51
+ const trimmed = value?.trim() ?? "";
52
+ return trimmed === "" ? null : trimmed;
53
+ }
54
+ function normalizeRoutePath(value) {
55
+ const trimmed = value.trim();
56
+ if (trimmed === "" || !trimmed.startsWith("/")) throw new TypeError("dsh-qa-surface: route.path must start with /");
57
+ const path = trimmed.length > 1 ? trimmed.replace(/\/+$/u, "") : trimmed;
58
+ if (path === "/") throw new TypeError("dsh-qa-surface: route.path cannot replace the operator root");
59
+ if (path === "/api" || path.startsWith("/api/")) throw new TypeError("dsh-qa-surface: route.path cannot claim /api");
60
+ if (path === "/plugins" || path.startsWith("/plugins/")) throw new TypeError("dsh-qa-surface: route.path cannot claim /plugins");
61
+ return path;
62
+ }
63
+ function uniqueQuestions(values) {
64
+ const questions = [...new Set(values.map((value) => value.trim()).filter(Boolean))];
65
+ if (questions.some((value) => value.length > 500)) throw new TypeError("dsh-qa-surface: suggested questions must be at most 500 characters");
66
+ return Object.freeze(questions);
67
+ }
68
+ /** Materialize defaults and enforce route/session/model safety constraints. */
69
+ function resolveConfig(input = {}) {
70
+ const routePath = normalizeRoutePath(input.route?.path ?? DEFAULT_QA_SURFACE_CONFIG.route.path);
71
+ const policy = input.session?.policy ?? DEFAULT_QA_SURFACE_CONFIG.session.policy;
72
+ const fixedSessionId = optionalText(input.session?.fixedSessionId);
73
+ if (policy === "fixed" && fixedSessionId === null) throw new TypeError("dsh-qa-surface: session.fixedSessionId is required for fixed policy");
74
+ const provider = optionalText(input.session?.provider);
75
+ const model = optionalText(input.session?.model);
76
+ if (provider === null !== (model === null)) throw new TypeError("dsh-qa-surface: session.provider and session.model must be set together");
77
+ const storageKey = input.session?.storageKey?.trim() ?? DEFAULT_QA_SURFACE_CONFIG.session.storageKey;
78
+ if (storageKey === "") throw new TypeError("dsh-qa-surface: session.storageKey cannot be empty");
79
+ const maxContentWidth = input.ui?.maxContentWidth ?? DEFAULT_QA_SURFACE_CONFIG.ui.maxContentWidth;
80
+ if (!Number.isSafeInteger(maxContentWidth) || maxContentWidth < 480 || maxContentWidth > 1600) throw new TypeError("dsh-qa-surface: ui.maxContentWidth must be an integer from 480 to 1600");
81
+ if (input.ui?.showReasoning === true) throw new TypeError("dsh-qa-surface: ui.showReasoning is not supported by the safe MVP surface");
82
+ return Object.freeze({
83
+ enabled: input.enabled ?? DEFAULT_QA_SURFACE_CONFIG.enabled,
84
+ route: Object.freeze({
85
+ path: routePath,
86
+ matchChildren: input.route?.matchChildren ?? DEFAULT_QA_SURFACE_CONFIG.route.matchChildren
87
+ }),
88
+ branding: Object.freeze({
89
+ title: input.branding?.title?.trim() || DEFAULT_QA_SURFACE_CONFIG.branding.title,
90
+ subtitle: input.branding?.subtitle?.trim() ?? "",
91
+ welcomeMessage: input.branding?.welcomeMessage?.trim() ?? DEFAULT_QA_SURFACE_CONFIG.branding.welcomeMessage,
92
+ placeholder: input.branding?.placeholder?.trim() || DEFAULT_QA_SURFACE_CONFIG.branding.placeholder,
93
+ logoUrl: optionalText(input.branding?.logoUrl)
94
+ }),
95
+ session: Object.freeze({
96
+ policy,
97
+ storageKey,
98
+ workspaceId: optionalText(input.session?.workspaceId),
99
+ fixedSessionId,
100
+ agentPreset: optionalText(input.session?.agentPreset),
101
+ provider,
102
+ model,
103
+ reasoningEffort: optionalText(input.session?.reasoningEffort)
104
+ }),
105
+ ui: Object.freeze({
106
+ showHeader: input.ui?.showHeader ?? DEFAULT_QA_SURFACE_CONFIG.ui.showHeader,
107
+ showReset: input.ui?.showReset ?? DEFAULT_QA_SURFACE_CONFIG.ui.showReset,
108
+ showStop: input.ui?.showStop ?? DEFAULT_QA_SURFACE_CONFIG.ui.showStop,
109
+ showTimestamps: input.ui?.showTimestamps ?? DEFAULT_QA_SURFACE_CONFIG.ui.showTimestamps,
110
+ showToolActivity: input.ui?.showToolActivity ?? DEFAULT_QA_SURFACE_CONFIG.ui.showToolActivity,
111
+ showReasoning: false,
112
+ renderMarkdown: input.ui?.renderMarkdown ?? DEFAULT_QA_SURFACE_CONFIG.ui.renderMarkdown,
113
+ maxContentWidth
114
+ }),
115
+ suggestedQuestions: uniqueQuestions(input.suggestedQuestions ?? []),
116
+ interaction: Object.freeze({
117
+ approvals: "blocked",
118
+ questions: "unsupported"
119
+ }),
120
+ embedding: Object.freeze({ frameAncestors: optionalText(input.embedding?.frameAncestors) })
121
+ });
122
+ }
123
+ //#endregion
124
+ //#region src/client/QaConfigController.ts
125
+ /** Browser projection of the Host-owned qa-surface settings namespace. */
126
+ var QaConfigController = class {
127
+ scope;
128
+ listeners = /* @__PURE__ */ new Set();
129
+ snapshot;
130
+ unsubscribe;
131
+ constructor(scope) {
132
+ this.scope = scope;
133
+ this.snapshot = this.project();
134
+ this.unsubscribe = scope.subscribe(() => this.refresh());
135
+ }
136
+ getSnapshot = () => this.snapshot;
137
+ subscribe = (listener) => {
138
+ this.listeners.add(listener);
139
+ return () => this.listeners.delete(listener);
140
+ };
141
+ dispose() {
142
+ this.unsubscribe();
143
+ this.listeners.clear();
144
+ }
145
+ project() {
146
+ const settings = this.scope.getSnapshot();
147
+ if (settings.status === "loading") return {
148
+ status: "loading",
149
+ config: DEFAULT_QA_SURFACE_CONFIG,
150
+ error: null
151
+ };
152
+ if (settings.status === "unavailable") return {
153
+ status: "unavailable",
154
+ config: DEFAULT_QA_SURFACE_CONFIG,
155
+ error: null
156
+ };
157
+ try {
158
+ return {
159
+ status: "ready",
160
+ config: resolveConfig(settings.value ?? {}),
161
+ error: null
162
+ };
163
+ } catch {
164
+ return {
165
+ status: "error",
166
+ config: DEFAULT_QA_SURFACE_CONFIG,
167
+ error: "The assistant configuration is unavailable."
168
+ };
169
+ }
170
+ }
171
+ refresh() {
172
+ const next = this.project();
173
+ if (next.status === this.snapshot.status && next.config === this.snapshot.config && next.error === this.snapshot.error) return;
174
+ this.snapshot = next;
175
+ for (const listener of this.listeners) listener();
176
+ }
177
+ };
178
+ //#endregion
179
+ //#region src/navigation-marker.ts
180
+ /** Query hand-off used only by the Host navigation compatibility redirect. */
181
+ const QA_NAVIGATION_MARKER = "__dsh_qa_route";
182
+ //#endregion
183
+ //#region src/client/QaRouteController.ts
184
+ function matchesQaRoute(pathname, route) {
185
+ if (pathname === route.path || pathname === `${route.path}/`) return true;
186
+ if (route.path === "/") return route.matchChildren;
187
+ if (!route.matchChildren) return false;
188
+ return pathname.startsWith(`${route.path}/`);
189
+ }
190
+ /** Observable route matcher with reversible History API instrumentation. */
191
+ var QaRouteController = class {
192
+ target;
193
+ listeners = /* @__PURE__ */ new Set();
194
+ route = {
195
+ path: "/qa",
196
+ matchChildren: true
197
+ };
198
+ enabled = false;
199
+ snapshot;
200
+ originalPushState;
201
+ originalReplaceState;
202
+ wrappedPushState;
203
+ wrappedReplaceState;
204
+ disposed = false;
205
+ constructor(target = window) {
206
+ this.target = target;
207
+ this.originalPushState = target.history.pushState;
208
+ this.originalReplaceState = target.history.replaceState;
209
+ this.restoreRedirectedPath();
210
+ this.snapshot = this.project();
211
+ const refresh = () => this.refresh();
212
+ const history = target.history;
213
+ const originalPushState = this.originalPushState;
214
+ const originalReplaceState = this.originalReplaceState;
215
+ this.wrappedPushState = function(...args) {
216
+ Reflect.apply(originalPushState, history, args);
217
+ refresh();
218
+ };
219
+ this.wrappedReplaceState = function(...args) {
220
+ Reflect.apply(originalReplaceState, history, args);
221
+ refresh();
222
+ };
223
+ target.history.pushState = this.wrappedPushState;
224
+ target.history.replaceState = this.wrappedReplaceState;
225
+ target.addEventListener("popstate", refresh);
226
+ this.removePopstate = () => target.removeEventListener("popstate", refresh);
227
+ }
228
+ removePopstate;
229
+ getSnapshot = () => this.snapshot;
230
+ subscribe = (listener) => {
231
+ this.listeners.add(listener);
232
+ return () => this.listeners.delete(listener);
233
+ };
234
+ configure(config, ready = true) {
235
+ this.route = config.route;
236
+ this.enabled = ready && config.enabled;
237
+ this.refresh();
238
+ }
239
+ dispose() {
240
+ if (this.disposed) return;
241
+ this.disposed = true;
242
+ this.removePopstate();
243
+ if (this.target.history.pushState === this.wrappedPushState) this.target.history.pushState = this.originalPushState;
244
+ if (this.target.history.replaceState === this.wrappedReplaceState) this.target.history.replaceState = this.originalReplaceState;
245
+ this.listeners.clear();
246
+ }
247
+ project() {
248
+ const pathname = this.target.location.pathname;
249
+ return {
250
+ pathname,
251
+ active: this.enabled && matchesQaRoute(pathname, this.route)
252
+ };
253
+ }
254
+ restoreRedirectedPath() {
255
+ const search = this.target.location.search;
256
+ if (search === void 0 || search === "") return;
257
+ const requested = new URLSearchParams(search).get(QA_NAVIGATION_MARKER);
258
+ if (requested === null || !requested.startsWith("/") || requested.startsWith("//")) return;
259
+ Reflect.apply(this.originalReplaceState, this.target.history, [
260
+ null,
261
+ "",
262
+ requested
263
+ ]);
264
+ }
265
+ refresh() {
266
+ if (this.disposed) return;
267
+ const next = this.project();
268
+ if (next.pathname === this.snapshot.pathname && next.active === this.snapshot.active) return;
269
+ this.snapshot = next;
270
+ for (const listener of this.listeners) listener();
271
+ }
272
+ };
273
+ //#endregion
274
+ //#region src/client/QaTranscriptAdapter.ts
275
+ function visibleContentText(content) {
276
+ return content.flatMap((block) => {
277
+ if (typeof block !== "object" || block === null) return [];
278
+ return Reflect.get(block, "type") === "text" && typeof Reflect.get(block, "text") === "string" ? [Reflect.get(block, "text")] : [];
279
+ }).join("");
280
+ }
281
+ function visibleAssistantText(blocks) {
282
+ return blocks.filter((block) => block.kind === "text" && typeof block.text === "string").map((block) => block.text).join("");
283
+ }
284
+ /** Project only end-user-safe text from the public DSH conversation snapshot. */
285
+ function projectTranscript(snapshot, options = {}) {
286
+ const messages = [];
287
+ for (const node of snapshot.nodes) {
288
+ if (node.kind === "user" || node.kind === "steering") {
289
+ const text = visibleContentText(node.content);
290
+ if (text !== "") messages.push({
291
+ id: `${node.kind}:${node.seq}`,
292
+ role: "user",
293
+ text,
294
+ status: "committed",
295
+ timestamp: node.time
296
+ });
297
+ continue;
298
+ }
299
+ if (node.kind === "assistant") {
300
+ const text = visibleAssistantText(node.blocks);
301
+ if (text !== "") messages.push({
302
+ id: `assistant:${node.messageId ?? node.seq}`,
303
+ role: "assistant",
304
+ text,
305
+ status: "committed",
306
+ timestamp: node.time
307
+ });
308
+ continue;
309
+ }
310
+ if (node.kind === "turn-error") {
311
+ messages.push({
312
+ id: `turn-error:${node.seq}`,
313
+ role: "system",
314
+ text: "The assistant could not complete this response.",
315
+ status: "error",
316
+ timestamp: node.time
317
+ });
318
+ continue;
319
+ }
320
+ if (node.kind === "turn-max-tokens") messages.push({
321
+ id: `turn-max-tokens:${node.seq}`,
322
+ role: "system",
323
+ text: "The response reached its length limit.",
324
+ status: "info",
325
+ timestamp: node.time
326
+ });
327
+ }
328
+ if (snapshot.partial !== null) {
329
+ const text = visibleAssistantText(snapshot.partial.blocks);
330
+ if (text !== "") messages.push({
331
+ id: `assistant:partial:${snapshot.partial.turn}:${snapshot.partial.step}`,
332
+ role: "assistant",
333
+ text,
334
+ status: "streaming"
335
+ });
336
+ }
337
+ if (options.showToolActivity === true && snapshot.runningCalls.length > 0 && snapshot.partial === null) messages.push({
338
+ id: "tool-activity",
339
+ role: "system",
340
+ text: "Working…",
341
+ status: "info"
342
+ });
343
+ return messages;
344
+ }
345
+ //#endregion
346
+ //#region src/client/QaSessionController.ts
347
+ const EMPTY_STATE = Object.freeze({
348
+ phase: "idle",
349
+ sessionId: null,
350
+ messages: Object.freeze([]),
351
+ error: null,
352
+ canSend: false,
353
+ canStop: false
354
+ });
355
+ function waitFor(source, predicate, timeoutMs) {
356
+ const current = source.getSnapshot();
357
+ if (predicate(current)) return Promise.resolve(current);
358
+ return new Promise((resolve, reject) => {
359
+ let settled = false;
360
+ const timeout = {};
361
+ let unsubscribe = () => void 0;
362
+ const finish = (value) => {
363
+ if (settled) return;
364
+ settled = true;
365
+ if (timeout.id !== void 0) clearTimeout(timeout.id);
366
+ unsubscribe();
367
+ resolve(value);
368
+ };
369
+ unsubscribe = source.subscribe(() => {
370
+ const next = source.getSnapshot();
371
+ if (predicate(next)) finish(next);
372
+ });
373
+ if (settled) {
374
+ unsubscribe();
375
+ return;
376
+ }
377
+ const afterSubscribe = source.getSnapshot();
378
+ if (predicate(afterSubscribe)) {
379
+ finish(afterSubscribe);
380
+ return;
381
+ }
382
+ timeout.id = setTimeout(() => {
383
+ if (settled) return;
384
+ settled = true;
385
+ unsubscribe();
386
+ reject(/* @__PURE__ */ new Error("Timed out waiting for the DSH runtime."));
387
+ }, timeoutMs);
388
+ });
389
+ }
390
+ /** The only module that couples QA behavior to DSH Session/client APIs. */
391
+ var QaSessionController = class {
392
+ listeners = /* @__PURE__ */ new Set();
393
+ sessions;
394
+ api;
395
+ connection;
396
+ config;
397
+ storage;
398
+ timeoutMs;
399
+ state = EMPTY_STATE;
400
+ session;
401
+ unsubscribeSession;
402
+ unsubscribeConnection;
403
+ ensuring;
404
+ operationError = null;
405
+ admissionPending = false;
406
+ connectedOnce;
407
+ disposed = false;
408
+ generation = 0;
409
+ constructor(options) {
410
+ this.sessions = options.sessions;
411
+ this.api = options.api;
412
+ this.connection = options.connection;
413
+ this.config = options.config;
414
+ this.storage = options.storage;
415
+ this.timeoutMs = options.timeoutMs ?? 15e3;
416
+ this.connectedOnce = this.connection.getSnapshot() !== void 0;
417
+ this.unsubscribeConnection = this.connection.subscribe(() => {
418
+ if (this.connection.getSnapshot() !== void 0) this.connectedOnce = true;
419
+ this.publish();
420
+ });
421
+ }
422
+ getSnapshot = () => this.state;
423
+ subscribe = (listener) => {
424
+ this.listeners.add(listener);
425
+ return () => this.listeners.delete(listener);
426
+ };
427
+ ensureSession() {
428
+ if (this.ensuring !== void 0) return this.ensuring;
429
+ const operation = this.ensureSessionNow().finally(() => {
430
+ if (this.ensuring === operation) this.ensuring = void 0;
431
+ });
432
+ this.ensuring = operation;
433
+ return operation;
434
+ }
435
+ async send(text) {
436
+ const prompt = text.trim();
437
+ if (prompt === "" || this.session === void 0 || !this.state.canSend) return false;
438
+ if (prompt.startsWith("/")) {
439
+ this.operationError = "Slash commands are not available in this assistant view.";
440
+ this.publish();
441
+ return false;
442
+ }
443
+ this.operationError = null;
444
+ this.admissionPending = true;
445
+ this.publish();
446
+ try {
447
+ if (!(await this.session.prompt([{
448
+ type: "text",
449
+ text: prompt
450
+ }], "queue")).ok) {
451
+ this.admissionPending = false;
452
+ this.operationError = "Your message could not be sent.";
453
+ this.publish();
454
+ return false;
455
+ }
456
+ this.publish();
457
+ return true;
458
+ } catch (error) {
459
+ this.admissionPending = false;
460
+ console.error("dsh-qa-surface: prompt failed", error);
461
+ this.operationError = "Your message could not be sent.";
462
+ this.publish();
463
+ return false;
464
+ }
465
+ }
466
+ async stop() {
467
+ if (this.session === void 0 || !this.state.canStop) return;
468
+ try {
469
+ if (!(await this.session.cancel()).ok) this.operationError = "The response could not be stopped.";
470
+ } catch (error) {
471
+ console.error("dsh-qa-surface: stop failed", error);
472
+ this.operationError = "The response could not be stopped.";
473
+ }
474
+ this.publish();
475
+ }
476
+ async reset() {
477
+ if (this.config.session.policy === "fixed") return;
478
+ const previous = this.session;
479
+ if (previous?.getSnapshot().running === true) try {
480
+ await previous.cancel();
481
+ } catch (error) {
482
+ console.error("dsh-qa-surface: stop before reset failed", error);
483
+ }
484
+ const operation = ++this.generation;
485
+ this.unbind();
486
+ this.operationError = null;
487
+ this.admissionPending = false;
488
+ this.state = {
489
+ ...EMPTY_STATE,
490
+ phase: "creating"
491
+ };
492
+ this.emit();
493
+ try {
494
+ await this.waitForConnection();
495
+ const id = await this.createSession();
496
+ if (this.disposed || operation !== this.generation) return;
497
+ this.persist(id);
498
+ await this.bind(id);
499
+ } catch (error) {
500
+ this.fail("Unable to start a new chat.", error);
501
+ }
502
+ }
503
+ dispose() {
504
+ if (this.disposed) return;
505
+ this.disposed = true;
506
+ this.generation += 1;
507
+ this.unsubscribeConnection();
508
+ this.unbind();
509
+ this.listeners.clear();
510
+ }
511
+ async ensureSessionNow() {
512
+ const operation = ++this.generation;
513
+ this.state = {
514
+ ...EMPTY_STATE,
515
+ phase: "creating"
516
+ };
517
+ this.emit();
518
+ try {
519
+ await this.waitForConnection();
520
+ const list = await waitFor(this.sessions.list, (snapshot) => snapshot.phase === "ready", this.timeoutMs);
521
+ let id = null;
522
+ if (this.config.session.policy === "fixed") {
523
+ id = this.config.session.fixedSessionId;
524
+ if (id === null || !Object.hasOwn(list.byId, id)) throw new Error("Configured fixed session is unavailable.");
525
+ } else if (this.config.session.policy === "browser-persistent") {
526
+ const stored = this.readPersisted();
527
+ if (stored !== null && Object.hasOwn(list.byId, stored)) id = stored;
528
+ else if (stored !== null) this.clearPersisted();
529
+ }
530
+ if (id === null) {
531
+ id = await this.createSession();
532
+ if (this.config.session.policy === "browser-persistent") this.persist(id);
533
+ }
534
+ if (this.disposed || operation !== this.generation) return;
535
+ await this.bind(id);
536
+ } catch (error) {
537
+ this.fail("Unable to start a chat.", error);
538
+ }
539
+ }
540
+ async createSession() {
541
+ const response = await this.api.create({
542
+ ...this.config.session.workspaceId === null ? {} : { workspaceId: this.config.session.workspaceId },
543
+ ...this.config.session.agentPreset === null ? {} : { agentPreset: this.config.session.agentPreset }
544
+ });
545
+ if (!response.result.ok) throw new Error(response.result.error.code);
546
+ const id = String(response.result.value.sessionId);
547
+ await waitFor(this.sessions.list, (snapshot) => Object.hasOwn(snapshot.byId, id), this.timeoutMs);
548
+ if (this.config.session.provider !== null && this.config.session.model !== null) {
549
+ const selected = await this.api.selectModel({
550
+ sessionId: id,
551
+ provider: this.config.session.provider,
552
+ model: this.config.session.model,
553
+ ...this.config.session.reasoningEffort === null ? {} : { reasoningEffort: this.config.session.reasoningEffort }
554
+ });
555
+ if (!selected.result.ok) throw new Error(selected.result.error.code);
556
+ }
557
+ return id;
558
+ }
559
+ async bind(id) {
560
+ this.sessions.open(id);
561
+ let binding = this.sessions.binding(id);
562
+ if (binding === void 0) {
563
+ await waitFor(this.sessions.list, (snapshot) => Object.hasOwn(snapshot.byId, id), this.timeoutMs);
564
+ binding = this.sessions.binding(id);
565
+ }
566
+ if (binding === void 0) throw new Error("Session binding is unavailable.");
567
+ this.unbind();
568
+ this.session = binding.session;
569
+ this.unsubscribeSession = binding.session.subscribe(() => {
570
+ this.admissionPending = false;
571
+ this.operationError = null;
572
+ this.publish();
573
+ });
574
+ this.publish();
575
+ }
576
+ unbind() {
577
+ this.unsubscribeSession?.();
578
+ this.unsubscribeSession = void 0;
579
+ this.session = void 0;
580
+ this.admissionPending = false;
581
+ }
582
+ waitForConnection() {
583
+ return waitFor(this.connection, (description) => description !== void 0, this.timeoutMs);
584
+ }
585
+ publish() {
586
+ if (this.disposed) return;
587
+ const connected = this.connection.getSnapshot() !== void 0;
588
+ if (this.session === void 0) {
589
+ this.state = {
590
+ ...this.state,
591
+ phase: !connected && this.connectedOnce ? "reconnecting" : this.state.phase,
592
+ error: this.operationError ?? this.state.error
593
+ };
594
+ this.emit();
595
+ return;
596
+ }
597
+ const snapshot = this.session.getSnapshot();
598
+ const blocked = snapshot.pending.length > 0;
599
+ const error = this.operationError ?? (blocked ? "This request requires an interaction that is not available in this assistant view." : snapshot.removed || snapshot.openState === "error" ? "This chat is no longer available." : null);
600
+ const phase = !connected ? "reconnecting" : blocked ? "blocked" : snapshot.removed || snapshot.openState === "error" ? "error" : snapshot.openState !== "open" ? "creating" : snapshot.running || this.admissionPending ? "running" : "ready";
601
+ this.state = {
602
+ phase,
603
+ sessionId: String(this.session.sessionId),
604
+ messages: projectTranscript(snapshot, { showToolActivity: this.config.ui.showToolActivity }),
605
+ error,
606
+ canSend: connected && phase === "ready",
607
+ canStop: connected && snapshot.running && this.config.ui.showStop && !blocked
608
+ };
609
+ this.emit();
610
+ }
611
+ fail(message, error) {
612
+ console.error("dsh-qa-surface: session operation failed", error);
613
+ if (this.disposed) return;
614
+ this.operationError = message;
615
+ this.state = {
616
+ ...EMPTY_STATE,
617
+ phase: "error",
618
+ error: message
619
+ };
620
+ this.emit();
621
+ }
622
+ storageKey() {
623
+ return `${this.config.session.storageKey}:v1:${this.config.route.path}:session`;
624
+ }
625
+ readPersisted() {
626
+ try {
627
+ const value = this.storage?.getItem(this.storageKey())?.trim() ?? "";
628
+ return value === "" ? null : value;
629
+ } catch {
630
+ return null;
631
+ }
632
+ }
633
+ persist(id) {
634
+ try {
635
+ this.storage?.setItem(this.storageKey(), id);
636
+ } catch {}
637
+ }
638
+ clearPersisted() {
639
+ try {
640
+ this.storage?.removeItem(this.storageKey());
641
+ } catch {}
642
+ }
643
+ emit() {
644
+ for (const listener of this.listeners) listener();
645
+ }
646
+ };
647
+ //#endregion
648
+ //#region src/client/components/QaComposer.tsx
649
+ function QaComposer(props) {
650
+ const [draft, setDraft] = (0, react.useState)("");
651
+ const [submitting, setSubmitting] = (0, react.useState)(false);
652
+ const textarea = (0, react.useRef)(null);
653
+ const send = async () => {
654
+ if (submitting || draft.trim() === "" || !props.canSend) return;
655
+ setSubmitting(true);
656
+ try {
657
+ if (await props.onSend(draft)) setDraft("");
658
+ } finally {
659
+ setSubmitting(false);
660
+ textarea.current?.focus();
661
+ }
662
+ };
663
+ (0, react.useEffect)(() => {
664
+ if (!props.running) textarea.current?.focus();
665
+ }, [props.running]);
666
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
667
+ className: "dsh-qa-composer",
668
+ children: [
669
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
670
+ className: "dsh-qa-sr-only",
671
+ htmlFor: "dsh-qa-prompt",
672
+ children: "Ask a question"
673
+ }),
674
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
675
+ ref: textarea,
676
+ id: "dsh-qa-prompt",
677
+ rows: 1,
678
+ value: draft,
679
+ placeholder: props.placeholder,
680
+ disabled: !props.canSend && !props.running,
681
+ onChange: (event) => setDraft(event.currentTarget.value),
682
+ onKeyDown: (event) => {
683
+ if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
684
+ event.preventDefault();
685
+ send();
686
+ }
687
+ }
688
+ }),
689
+ props.running && props.showStop ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
690
+ type: "button",
691
+ className: "dsh-qa-button dsh-qa-button--stop",
692
+ disabled: !props.canStop,
693
+ onClick: () => void props.onStop(),
694
+ children: "Stop"
695
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
696
+ type: "button",
697
+ className: "dsh-qa-button dsh-qa-button--send",
698
+ disabled: !props.canSend || submitting || draft.trim() === "",
699
+ onClick: () => void send(),
700
+ children: "Send"
701
+ })
702
+ ]
703
+ });
704
+ }
705
+ //#endregion
706
+ //#region src/client/components/Markdown.tsx
707
+ const INLINE = /(\[[^\]]+\]\([^\s)]+\)|`[^`]+`|\*\*[^*]+\*\*|\*[^*]+\*)/gu;
708
+ function safeHref(value) {
709
+ try {
710
+ const url = new URL(value, window.location.origin);
711
+ return [
712
+ "http:",
713
+ "https:",
714
+ "mailto:"
715
+ ].includes(url.protocol) ? value : void 0;
716
+ } catch {
717
+ return;
718
+ }
719
+ }
720
+ function inline(text, keyPrefix) {
721
+ const nodes = [];
722
+ let offset = 0;
723
+ for (const match of text.matchAll(INLINE)) {
724
+ const index = match.index;
725
+ if (index > offset) nodes.push(text.slice(offset, index));
726
+ const token = match[0];
727
+ const key = `${keyPrefix}:${index}`;
728
+ if (token.startsWith("[")) {
729
+ const parts = /^\[([^\]]+)\]\(([^\s)]+)\)$/u.exec(token);
730
+ const href = parts?.[2] === void 0 ? void 0 : safeHref(parts[2]);
731
+ nodes.push(href === void 0 ? token : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
732
+ href,
733
+ target: "_blank",
734
+ rel: "noreferrer",
735
+ children: parts?.[1]
736
+ }, key));
737
+ } else if (token.startsWith("`")) nodes.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: token.slice(1, -1) }, key));
738
+ else if (token.startsWith("**")) nodes.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: token.slice(2, -2) }, key));
739
+ else nodes.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("em", { children: token.slice(1, -1) }, key));
740
+ offset = index + token.length;
741
+ }
742
+ if (offset < text.length) nodes.push(text.slice(offset));
743
+ return nodes;
744
+ }
745
+ /** Deliberately small, HTML-free Markdown renderer for assistant-visible text. */
746
+ function Markdown({ text }) {
747
+ const lines = text.replace(/\r\n?/gu, "\n").split("\n");
748
+ const blocks = [];
749
+ for (let index = 0; index < lines.length;) {
750
+ const line = lines[index] ?? "";
751
+ if (line.startsWith("```")) {
752
+ const language = line.slice(3).trim();
753
+ const code = [];
754
+ index += 1;
755
+ while (index < lines.length && !(lines[index] ?? "").startsWith("```")) {
756
+ code.push(lines[index] ?? "");
757
+ index += 1;
758
+ }
759
+ if (index < lines.length) index += 1;
760
+ blocks.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
761
+ "data-language": language || void 0,
762
+ children: code.join("\n")
763
+ }) }, `code:${index}`));
764
+ continue;
765
+ }
766
+ const heading = /^(#{1,3})\s+(.+)$/u.exec(line);
767
+ if (heading !== null) {
768
+ const content = inline(heading[2] ?? "", `heading:${index}`);
769
+ const key = `heading:${index}`;
770
+ blocks.push(heading[1]?.length === 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", { children: content }, key) : heading[1]?.length === 2 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: content }, key) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: content }, key));
771
+ index += 1;
772
+ continue;
773
+ }
774
+ if (/^[-*]\s+/u.test(line)) {
775
+ const items = [];
776
+ while (index < lines.length && /^[-*]\s+/u.test(lines[index] ?? "")) {
777
+ const content = (lines[index] ?? "").replace(/^[-*]\s+/u, "");
778
+ items.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: inline(content, `item:${index}`) }, `item:${index}`));
779
+ index += 1;
780
+ }
781
+ blocks.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", { children: items }, `list:${index}`));
782
+ continue;
783
+ }
784
+ if (line.startsWith("> ")) {
785
+ blocks.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("blockquote", { children: inline(line.slice(2), `quote:${index}`) }, `quote:${index}`));
786
+ index += 1;
787
+ continue;
788
+ }
789
+ if (line.trim() === "") {
790
+ index += 1;
791
+ continue;
792
+ }
793
+ const paragraph = [line];
794
+ index += 1;
795
+ while (index < lines.length && (lines[index] ?? "").trim() !== "" && !/^(#{1,3})\s+|^```|^[-*]\s+|^>\s/u.test(lines[index] ?? "")) {
796
+ paragraph.push(lines[index] ?? "");
797
+ index += 1;
798
+ }
799
+ blocks.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: paragraph.map((part, partIndex) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [partIndex === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("br", {}), inline(part, `paragraph:${index}:${partIndex}`)] }, `line:${partIndex}`)) }, `paragraph:${index}`));
800
+ }
801
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: blocks });
802
+ }
803
+ //#endregion
804
+ //#region src/client/components/QaMessage.tsx
805
+ function QaMessage({ message, renderMarkdown, showTimestamp }) {
806
+ const label = message.role === "assistant" ? "Assistant" : message.role === "user" ? "You" : "Status";
807
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
808
+ className: `dsh-qa-message dsh-qa-message--${message.role}`,
809
+ "data-status": message.status,
810
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
811
+ className: "dsh-qa-message__meta",
812
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }), showTimestamp && message.timestamp !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("time", {
813
+ dateTime: new Date(message.timestamp).toISOString(),
814
+ children: new Date(message.timestamp).toLocaleTimeString([], {
815
+ hour: "2-digit",
816
+ minute: "2-digit"
817
+ })
818
+ }) : null]
819
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
820
+ className: "dsh-qa-message__content",
821
+ children: [message.role === "assistant" && renderMarkdown ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Markdown, { text: message.text }) : message.text, message.status === "streaming" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
822
+ className: "dsh-qa-message__cursor",
823
+ "aria-hidden": "true"
824
+ }) : null]
825
+ })]
826
+ });
827
+ }
828
+ //#endregion
829
+ //#region src/client/QaSurface.tsx
830
+ const INACTIVE_STATE = Object.freeze({
831
+ phase: "idle",
832
+ sessionId: null,
833
+ messages: Object.freeze([]),
834
+ error: null,
835
+ canSend: false,
836
+ canStop: false
837
+ });
838
+ const noopSubscribe = () => () => void 0;
839
+ function focusable(root) {
840
+ return [...root.querySelectorAll("button:not([disabled]), textarea:not([disabled]), a[href], [tabindex]:not([tabindex='-1'])")].filter((element) => !element.hidden);
841
+ }
842
+ function trapKeys(event) {
843
+ event.stopPropagation();
844
+ if (event.key !== "Tab") return;
845
+ const items = focusable(event.currentTarget);
846
+ if (items.length === 0) {
847
+ event.preventDefault();
848
+ event.currentTarget.focus();
849
+ return;
850
+ }
851
+ const first = items[0];
852
+ const last = items.at(-1);
853
+ if (event.shiftKey && document.activeElement === first) {
854
+ event.preventDefault();
855
+ last?.focus();
856
+ } else if (!event.shiftKey && document.activeElement === last) {
857
+ event.preventDefault();
858
+ first?.focus();
859
+ }
860
+ }
861
+ function statusText(state) {
862
+ if (state.phase === "creating") return "Connecting…";
863
+ if (state.phase === "reconnecting") return "Connection lost. Reconnecting…";
864
+ if (state.phase === "running") return "Assistant is responding…";
865
+ return null;
866
+ }
867
+ function QaSurface(props) {
868
+ const route = (0, react.useSyncExternalStore)(props.route.subscribe, props.route.getSnapshot, props.route.getSnapshot);
869
+ const config = (0, react.useSyncExternalStore)(props.config.subscribe, props.config.getSnapshot, props.config.getSnapshot).config;
870
+ const [controller, setController] = (0, react.useState)();
871
+ const transcript = (0, react.useRef)(null);
872
+ const nearBottom = (0, react.useRef)(true);
873
+ (0, react.useEffect)(() => {
874
+ if (!route.active) {
875
+ setController(void 0);
876
+ return;
877
+ }
878
+ const next = new QaSessionController({
879
+ sessions: props.sessions,
880
+ api: props.api,
881
+ connection: props.connection,
882
+ config,
883
+ storage: window.localStorage
884
+ });
885
+ setController(next);
886
+ next.ensureSession();
887
+ return () => next.dispose();
888
+ }, [
889
+ config,
890
+ props.api,
891
+ props.connection,
892
+ props.sessions,
893
+ route.active
894
+ ]);
895
+ const state = (0, react.useSyncExternalStore)(controller?.subscribe ?? noopSubscribe, controller?.getSnapshot ?? (() => INACTIVE_STATE), controller?.getSnapshot ?? (() => INACTIVE_STATE));
896
+ (0, react.useEffect)(() => {
897
+ if (!route.active) return;
898
+ const previousBodyOverflow = document.body.style.overflow;
899
+ const previousHtmlOverflow = document.documentElement.style.overflow;
900
+ document.body.dataset.dshQaSurface = "active";
901
+ document.body.style.overflow = "hidden";
902
+ document.documentElement.style.overflow = "hidden";
903
+ const frame = requestAnimationFrame(() => {
904
+ document.querySelector("#dsh-qa-prompt")?.focus();
905
+ });
906
+ return () => {
907
+ cancelAnimationFrame(frame);
908
+ delete document.body.dataset.dshQaSurface;
909
+ document.body.style.overflow = previousBodyOverflow;
910
+ document.documentElement.style.overflow = previousHtmlOverflow;
911
+ };
912
+ }, [route.active]);
913
+ (0, react.useLayoutEffect)(() => {
914
+ const element = transcript.current;
915
+ if (element !== null && nearBottom.current) element.scrollTop = element.scrollHeight;
916
+ }, [state.messages]);
917
+ if (!route.active) return null;
918
+ const status = statusText(state);
919
+ const empty = state.messages.length === 0;
920
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("main", {
921
+ className: "dsh-qa-surface",
922
+ "aria-label": config.branding.title,
923
+ tabIndex: -1,
924
+ onKeyDown: trapKeys,
925
+ children: [
926
+ config.ui.showHeader ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("header", {
927
+ className: "dsh-qa-header",
928
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
929
+ className: "dsh-qa-header__inner",
930
+ style: { maxWidth: config.ui.maxContentWidth },
931
+ children: [
932
+ config.branding.logoUrl === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
933
+ className: "dsh-qa-header__logo",
934
+ src: config.branding.logoUrl,
935
+ alt: ""
936
+ }),
937
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
938
+ className: "dsh-qa-header__text",
939
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", { children: config.branding.title }), config.branding.subtitle === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: config.branding.subtitle })]
940
+ }),
941
+ config.ui.showReset && config.session.policy !== "fixed" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
942
+ type: "button",
943
+ className: "dsh-qa-button dsh-qa-button--secondary",
944
+ disabled: controller === void 0 || state.phase === "creating",
945
+ onClick: () => void controller?.reset(),
946
+ children: "New chat"
947
+ }) : null
948
+ ]
949
+ })
950
+ }) : null,
951
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
952
+ ref: transcript,
953
+ className: "dsh-qa-transcript",
954
+ onScroll: (event) => {
955
+ const element = event.currentTarget;
956
+ nearBottom.current = element.scrollHeight - element.scrollTop - element.clientHeight < 96;
957
+ },
958
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
959
+ className: "dsh-qa-transcript__inner",
960
+ style: { maxWidth: config.ui.maxContentWidth },
961
+ children: [empty ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
962
+ className: "dsh-qa-welcome",
963
+ "aria-labelledby": "dsh-qa-welcome-title",
964
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
965
+ id: "dsh-qa-welcome-title",
966
+ children: config.branding.welcomeMessage
967
+ }), config.suggestedQuestions.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
968
+ className: "dsh-qa-suggestions",
969
+ "aria-label": "Suggested questions",
970
+ children: config.suggestedQuestions.map((question) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
971
+ type: "button",
972
+ disabled: !state.canSend,
973
+ onClick: () => void controller?.send(question),
974
+ children: question
975
+ }, question))
976
+ }) : null]
977
+ }) : state.messages.map((message) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QaMessage, {
978
+ message,
979
+ renderMarkdown: config.ui.renderMarkdown,
980
+ showTimestamp: config.ui.showTimestamps
981
+ }, message.id)), state.error === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
982
+ className: "dsh-qa-error",
983
+ role: "alert",
984
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: state.error }), state.phase === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
985
+ type: "button",
986
+ onClick: () => void controller?.ensureSession(),
987
+ children: "Retry"
988
+ }) : null]
989
+ })]
990
+ })
991
+ }),
992
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("footer", {
993
+ className: "dsh-qa-footer",
994
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
995
+ className: "dsh-qa-footer__inner",
996
+ style: { maxWidth: config.ui.maxContentWidth },
997
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
998
+ className: "dsh-qa-status",
999
+ "aria-live": "polite",
1000
+ "aria-atomic": "true",
1001
+ children: status
1002
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QaComposer, {
1003
+ placeholder: config.branding.placeholder,
1004
+ canSend: state.canSend,
1005
+ canStop: state.canStop,
1006
+ running: state.phase === "running",
1007
+ showStop: config.ui.showStop,
1008
+ onSend: (text) => controller?.send(text) ?? Promise.resolve(false),
1009
+ onStop: () => controller?.stop() ?? Promise.resolve()
1010
+ })]
1011
+ })
1012
+ })
1013
+ ]
1014
+ });
1015
+ }
1016
+ //#endregion
1017
+ //#region src/client/styles.ts
1018
+ const QA_SURFACE_STYLES = String.raw`
1019
+ .dsh-qa-surface{position:fixed;inset:0;z-index:2147483000;pointer-events:auto;display:flex;flex-direction:column;min-width:0;height:100vh;height:100dvh;overflow:hidden;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font-family:inherit}
1020
+ .dsh-qa-surface *{box-sizing:border-box}
1021
+ .dsh-qa-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
1022
+ .dsh-qa-header{flex:none;border-bottom:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2)}
1023
+ .dsh-qa-header__inner,.dsh-qa-footer__inner,.dsh-qa-transcript__inner{width:100%;margin:0 auto}
1024
+ .dsh-qa-header__inner{min-height:72px;display:flex;align-items:center;gap:12px;padding:12px 24px}
1025
+ .dsh-qa-header__logo{width:36px;height:36px;object-fit:contain;border-radius:9px;flex:none}
1026
+ .dsh-qa-header__text{flex:1;min-width:0}
1027
+ .dsh-qa-header h1{margin:0;color:var(--dsw-alias-label-primary);font-size:17px;font-weight:650;line-height:1.35}
1028
+ .dsh-qa-header p{margin:2px 0 0;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.4}
1029
+ .dsh-qa-transcript{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;scrollbar-gutter:stable}
1030
+ .dsh-qa-transcript__inner{min-height:100%;padding:32px 24px 24px}
1031
+ .dsh-qa-welcome{min-height:100%;display:flex;flex-direction:column;justify-content:center;align-items:center;text-align:center;padding:40px 0}
1032
+ .dsh-qa-welcome h2{max-width:620px;margin:0;color:var(--dsw-alias-label-primary);font-size:clamp(24px,4vw,34px);font-weight:600;line-height:1.25}
1033
+ .dsh-qa-suggestions{display:flex;flex-wrap:wrap;justify-content:center;gap:8px;margin-top:24px}
1034
+ .dsh-qa-suggestions button{max-width:360px;padding:9px 13px;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-secondary);font:inherit;font-size:13px;line-height:1.35;cursor:pointer}
1035
+ .dsh-qa-suggestions button:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed);color:var(--dsw-alias-label-primary)}
1036
+ .dsh-qa-suggestions button:disabled{opacity:.5;cursor:default}
1037
+ .dsh-qa-message{display:flex;flex-direction:column;max-width:min(82%,720px);margin:0 0 24px}
1038
+ .dsh-qa-message--user{align-items:flex-end;margin-left:auto}
1039
+ .dsh-qa-message--assistant{align-items:flex-start;margin-right:auto}
1040
+ .dsh-qa-message--system{align-items:center;max-width:100%;margin-left:auto;margin-right:auto}
1041
+ .dsh-qa-message__meta{display:flex;gap:8px;margin:0 4px 6px;color:var(--dsw-alias-label-tertiary);font-size:11px;font-weight:600;line-height:1.3}
1042
+ .dsh-qa-message__content{max-width:100%;white-space:pre-wrap;overflow-wrap:anywhere;border:1px solid var(--dsw-alias-border-l2);border-radius:14px;padding:11px 14px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);font-size:15px;line-height:1.62}
1043
+ .dsh-qa-message--user .dsh-qa-message__content{background:var(--dsw-alias-bg-module-platform)}
1044
+ .dsh-qa-message--system .dsh-qa-message__content{padding:7px 11px;border-radius:9px;color:var(--dsw-alias-label-secondary);font-size:13px}
1045
+ .dsh-qa-message[data-status="error"] .dsh-qa-message__content{border-color:var(--dsw-alias-status-error)}
1046
+ .dsh-qa-message__content p,.dsh-qa-message__content ul,.dsh-qa-message__content blockquote,.dsh-qa-message__content pre{margin:0 0 10px}
1047
+ .dsh-qa-message__content :last-child{margin-bottom:0}
1048
+ .dsh-qa-message__content h2,.dsh-qa-message__content h3,.dsh-qa-message__content h4{margin:14px 0 7px;line-height:1.35}
1049
+ .dsh-qa-message__content h2{font-size:18px}.dsh-qa-message__content h3{font-size:16px}.dsh-qa-message__content h4{font-size:15px}
1050
+ .dsh-qa-message__content ul{padding-left:22px}.dsh-qa-message__content a{color:var(--dsw-alias-brand-primary)}
1051
+ .dsh-qa-message__content code{border-radius:5px;padding:1px 4px;background:var(--dsw-alias-bg-layer-2);font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:.9em}
1052
+ .dsh-qa-message__content pre{overflow-x:auto;border-radius:9px;padding:12px;background:var(--dsw-alias-bg-layer-2)}
1053
+ .dsh-qa-message__content pre code{padding:0;background:transparent}
1054
+ .dsh-qa-message__content blockquote{border-left:3px solid var(--dsw-alias-border-l2);padding-left:12px;color:var(--dsw-alias-label-secondary)}
1055
+ .dsh-qa-message__cursor{display:inline-block;width:7px;height:1em;margin-left:3px;vertical-align:-2px;background:var(--dsw-alias-label-secondary);animation:dsh-qa-blink 1s steps(2,start) infinite}
1056
+ @keyframes dsh-qa-blink{50%{opacity:0}}
1057
+ .dsh-qa-error{display:flex;align-items:center;justify-content:center;gap:10px;margin:16px auto;padding:10px 12px;border:1px solid var(--dsw-alias-status-error);border-radius:10px;color:var(--dsw-alias-label-primary);font-size:13px}
1058
+ .dsh-qa-error button{border:0;background:transparent;color:var(--dsw-alias-brand-primary);font:inherit;font-weight:600;cursor:pointer}
1059
+ .dsh-qa-footer{flex:none;border-top:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);padding:0 24px max(14px,env(safe-area-inset-bottom))}
1060
+ .dsh-qa-footer__inner{position:relative;padding-top:14px}
1061
+ .dsh-qa-status{min-height:18px;margin-bottom:5px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}
1062
+ .dsh-qa-composer{display:flex;align-items:flex-end;gap:10px;border:1px solid var(--dsw-alias-border-l2);border-radius:14px;padding:7px;background:var(--dsw-alias-bg-layer-3);transition:border-color .16s}
1063
+ .dsh-qa-composer:focus-within{border-color:var(--dsw-alias-brand-primary)}
1064
+ .dsh-qa-composer textarea{flex:1;min-width:0;max-height:160px;resize:none;overflow-y:auto;border:0;outline:0;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:15px;line-height:22px;padding:7px 9px}
1065
+ .dsh-qa-composer textarea::placeholder{color:var(--dsw-alias-label-tertiary)}
1066
+ .dsh-qa-button{appearance:none;flex:none;border:1px solid transparent;border-radius:9px;padding:8px 13px;font:inherit;font-size:13px;font-weight:600;line-height:18px;cursor:pointer}
1067
+ .dsh-qa-button:focus-visible,.dsh-qa-suggestions button:focus-visible,.dsh-qa-error button:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
1068
+ .dsh-qa-button:disabled{opacity:.45;cursor:default}
1069
+ .dsh-qa-button--send{background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-on-primary)}
1070
+ .dsh-qa-button--stop,.dsh-qa-button--secondary{border-color:var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-secondary)}
1071
+ @media (max-width:600px){.dsh-qa-header__inner{min-height:60px;padding:9px 14px}.dsh-qa-header__logo{width:32px;height:32px}.dsh-qa-header p{display:none}.dsh-qa-transcript__inner{padding:22px 14px 16px}.dsh-qa-message{max-width:90%;margin-bottom:18px}.dsh-qa-message__content{font-size:14px;padding:10px 12px}.dsh-qa-footer{padding-left:10px;padding-right:10px}.dsh-qa-button{padding-left:11px;padding-right:11px}}
1072
+ @media (max-height:480px) and (orientation:landscape){.dsh-qa-header__inner{min-height:48px;padding-top:6px;padding-bottom:6px}.dsh-qa-header p{display:none}.dsh-qa-transcript__inner{padding-top:14px}.dsh-qa-footer__inner{padding-top:7px}.dsh-qa-status{position:absolute;right:0;top:-18px}}
1073
+ @media (prefers-reduced-motion:reduce){.dsh-qa-message__cursor{animation:none}}
1074
+ `;
1075
+ //#endregion
1076
+ //#region src/client/index.tsx
1077
+ const SETTINGS_NAMESPACE = "qa-surface";
1078
+ const inject = [
1079
+ "slots",
1080
+ "sessions",
1081
+ "connection",
1082
+ "settingsScope"
1083
+ ];
1084
+ /** Register the route-aware, full-frame QA entry in the additive overlay slot. */
1085
+ function apply(ctx) {
1086
+ const route = new QaRouteController();
1087
+ const config = new QaConfigController(ctx.settingsScope.bind({ namespace: SETTINGS_NAMESPACE }));
1088
+ const syncRoute = () => {
1089
+ const snapshot = config.getSnapshot();
1090
+ route.configure(snapshot.config, snapshot.status === "ready" || snapshot.status === "unavailable");
1091
+ };
1092
+ syncRoute();
1093
+ const unsubscribeConfig = config.subscribe(syncRoute);
1094
+ ctx.effect(() => {
1095
+ const style = document.createElement("style");
1096
+ style.dataset.dshQaSurface = "styles";
1097
+ style.textContent = QA_SURFACE_STYLES;
1098
+ document.head.append(style);
1099
+ return () => style.remove();
1100
+ }, "dsh-qa-surface: styles");
1101
+ ctx.effect(() => () => {
1102
+ unsubscribeConfig();
1103
+ config.dispose();
1104
+ route.dispose();
1105
+ }, "dsh-qa-surface: controllers");
1106
+ ctx.slots.inject("shell.overlay", () => ctx.slots.register({
1107
+ name: "shell.overlay",
1108
+ id: "dsh-qa-surface",
1109
+ order: -1e4,
1110
+ inject: () => ({
1111
+ route,
1112
+ config,
1113
+ sessions: ctx.sessions,
1114
+ api: ctx.connection.api.sessions,
1115
+ connection: ctx.connection.hostDescription
1116
+ })
1117
+ }, QaSurface));
1118
+ }
1119
+ //#endregion
1120
+ exports.QaConfigController = QaConfigController;
1121
+ exports.QaRouteController = QaRouteController;
1122
+ exports.QaSessionController = QaSessionController;
1123
+ exports.apply = apply;
1124
+ exports.inject = inject;
1125
+ exports.matchesQaRoute = matchesQaRoute;
1126
+ exports.projectTranscript = projectTranscript;
1127
+ return module.exports;
1128
+ }
1129
+ });
1130
+
1131
+ //# sourceMappingURL=client.js.map