@yadsh/dsh-qa-surface 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/README.md +239 -16
  3. package/SPEC-dsh-qa-surface.md +600 -96
  4. package/capability-policy.json +5 -0
  5. package/compatibility.json +18 -4
  6. package/cordis.patch.yml +11 -0
  7. package/docs/ARCHITECTURE.md +50 -8
  8. package/docs/COMPATIBILITY.md +5 -4
  9. package/docs/CONFIGURATION.md +184 -3
  10. package/docs/SOURCES-MIGRATION.md +28 -0
  11. package/docs/SOURCES-PROVENANCE-AUDIT.md +88 -0
  12. package/docs/SPEC-dsh-qa-surface-accounts.md +219 -0
  13. package/docs/SPEC-dsh-qa-surface-sources-provenance.md +1692 -0
  14. package/lib/accounts/index.js +2 -0
  15. package/lib/accounts/index.js.map +1 -0
  16. package/lib/accounts/store.js +478 -0
  17. package/lib/accounts/store.js.map +1 -0
  18. package/lib/attestation.js +10 -0
  19. package/lib/attestation.js.map +1 -0
  20. package/lib/cli.js +143 -0
  21. package/lib/cli.js.map +1 -0
  22. package/lib/client/QaAccountsController.js +329 -0
  23. package/lib/client/QaAccountsController.js.map +1 -0
  24. package/lib/client/QaConfigController.js +111 -4
  25. package/lib/client/QaConfigController.js.map +1 -1
  26. package/lib/client/QaSessionController.js +621 -163
  27. package/lib/client/QaSessionController.js.map +1 -1
  28. package/lib/client/QaSurface.js +303 -24
  29. package/lib/client/QaSurface.js.map +1 -1
  30. package/lib/client/QaTranscriptAdapter.js +324 -49
  31. package/lib/client/QaTranscriptAdapter.js.map +1 -1
  32. package/lib/client/attestation.js +66 -0
  33. package/lib/client/attestation.js.map +1 -0
  34. package/lib/client/chat-index.js +86 -0
  35. package/lib/client/chat-index.js.map +1 -0
  36. package/lib/client/components/Markdown.js +124 -13
  37. package/lib/client/components/Markdown.js.map +1 -1
  38. package/lib/client/components/QaAgentsDrawer.js +40 -0
  39. package/lib/client/components/QaAgentsDrawer.js.map +1 -0
  40. package/lib/client/components/QaAuthGate.js +40 -0
  41. package/lib/client/components/QaAuthGate.js.map +1 -0
  42. package/lib/client/components/QaChangelog.js +112 -0
  43. package/lib/client/components/QaChangelog.js.map +1 -0
  44. package/lib/client/components/QaComposer.js +132 -15
  45. package/lib/client/components/QaComposer.js.map +1 -1
  46. package/lib/client/components/QaMessage.js +214 -10
  47. package/lib/client/components/QaMessage.js.map +1 -1
  48. package/lib/client/components/QaSidebar.js +167 -0
  49. package/lib/client/components/QaSidebar.js.map +1 -0
  50. package/lib/client/components/QaSourcesDrawer.js +127 -0
  51. package/lib/client/components/QaSourcesDrawer.js.map +1 -0
  52. package/lib/client/components/QaTurnRail.js +281 -0
  53. package/lib/client/components/QaTurnRail.js.map +1 -0
  54. package/lib/client/components/QaWelcomeNotice.js +125 -0
  55. package/lib/client/components/QaWelcomeNotice.js.map +1 -0
  56. package/lib/client/components/QaWidthHandle.js +142 -0
  57. package/lib/client/components/QaWidthHandle.js.map +1 -0
  58. package/lib/client/components/QaWorkGroup.js +135 -0
  59. package/lib/client/components/QaWorkGroup.js.map +1 -0
  60. package/lib/client/components/VariantSwitcher.js +29 -0
  61. package/lib/client/components/VariantSwitcher.js.map +1 -0
  62. package/lib/client/components/format.js +48 -0
  63. package/lib/client/components/format.js.map +1 -0
  64. package/lib/client/components/source-refs.js +78 -0
  65. package/lib/client/components/source-refs.js.map +1 -0
  66. package/lib/client/create-session.js +51 -0
  67. package/lib/client/create-session.js.map +1 -0
  68. package/lib/client/index.js +166 -35
  69. package/lib/client/index.js.map +1 -1
  70. package/lib/client/project-session-state.js +78 -0
  71. package/lib/client/project-session-state.js.map +1 -0
  72. package/lib/client/session-admission.js +38 -0
  73. package/lib/client/session-admission.js.map +1 -0
  74. package/lib/client/session-assets.js +52 -0
  75. package/lib/client/session-assets.js.map +1 -0
  76. package/lib/client/session-sources.js +61 -0
  77. package/lib/client/session-sources.js.map +1 -0
  78. package/lib/client/settlement.js +24 -0
  79. package/lib/client/settlement.js.map +1 -0
  80. package/lib/client/styles.js +450 -43
  81. package/lib/client/styles.js.map +1 -1
  82. package/lib/client/tool-presentation.js +102 -0
  83. package/lib/client/tool-presentation.js.map +1 -0
  84. package/lib/client/turn-sources.js +98 -0
  85. package/lib/client/turn-sources.js.map +1 -0
  86. package/lib/client/types.js +15 -0
  87. package/lib/client/types.js.map +1 -0
  88. package/lib/client/use-session-ui-state.js +68 -0
  89. package/lib/client/use-session-ui-state.js.map +1 -0
  90. package/lib/client/use-transcript-view.js +44 -0
  91. package/lib/client/use-transcript-view.js.map +1 -0
  92. package/lib/client/wait-for.js +49 -0
  93. package/lib/client/wait-for.js.map +1 -0
  94. package/lib/client.js +12551 -918
  95. package/lib/client.js.map +1 -1
  96. package/lib/config.js +226 -58
  97. package/lib/config.js.map +1 -1
  98. package/lib/entry-redirect.js +44 -0
  99. package/lib/entry-redirect.js.map +1 -0
  100. package/lib/host-route.js +25 -1
  101. package/lib/host-route.js.map +1 -1
  102. package/lib/index.js +354 -72
  103. package/lib/index.js.map +1 -1
  104. package/lib/launch-token.js +42 -0
  105. package/lib/launch-token.js.map +1 -0
  106. package/lib/lockdown-policy.js +20 -0
  107. package/lib/lockdown-policy.js.map +1 -0
  108. package/lib/provenance/collector.js +40 -0
  109. package/lib/provenance/collector.js.map +1 -0
  110. package/lib/provenance/connectors.js +147 -0
  111. package/lib/provenance/connectors.js.map +1 -0
  112. package/lib/provenance/dedupe.js +89 -0
  113. package/lib/provenance/dedupe.js.map +1 -0
  114. package/lib/provenance/extractors.js +340 -0
  115. package/lib/provenance/extractors.js.map +1 -0
  116. package/lib/provenance/file-preview.js +39 -0
  117. package/lib/provenance/file-preview.js.map +1 -0
  118. package/lib/provenance/host-store.js +387 -0
  119. package/lib/provenance/host-store.js.map +1 -0
  120. package/lib/provenance/index.js +9 -0
  121. package/lib/provenance/index.js.map +1 -0
  122. package/lib/provenance/normalize.js +128 -0
  123. package/lib/provenance/normalize.js.map +1 -0
  124. package/lib/provenance/reported.js +53 -0
  125. package/lib/provenance/reported.js.map +1 -0
  126. package/lib/provenance/types.js +2 -0
  127. package/lib/provenance/types.js.map +1 -0
  128. package/lib/resolve-config.js +283 -10
  129. package/lib/resolve-config.js.map +1 -1
  130. package/lib/secure-session.js +212 -0
  131. package/lib/secure-session.js.map +1 -0
  132. package/lib/typert.host.d.ts +3 -0
  133. package/lib/typert.host.js +784 -0
  134. package/lib/typert.remote-client.d.ts +40 -0
  135. package/lib/typert.remote-client.js +597 -0
  136. package/lib/types/accounts/index.d.ts +2 -0
  137. package/lib/types/accounts/store.d.ts +121 -0
  138. package/lib/types/attestation.d.ts +17 -0
  139. package/lib/types/cli.d.ts +9 -0
  140. package/lib/types/client/QaAccountsController.d.ts +95 -0
  141. package/lib/types/client/QaConfigController.d.ts +35 -3
  142. package/lib/types/client/QaSessionController.d.ts +146 -20
  143. package/lib/types/client/QaSurface.d.ts +11 -5
  144. package/lib/types/client/QaTranscriptAdapter.d.ts +13 -3
  145. package/lib/types/client/attestation.d.ts +25 -0
  146. package/lib/types/client/chat-index.d.ts +28 -0
  147. package/lib/types/client/components/Markdown.d.ts +17 -3
  148. package/lib/types/client/components/QaAgentsDrawer.d.ts +25 -0
  149. package/lib/types/client/components/QaAuthGate.d.ts +15 -0
  150. package/lib/types/client/components/QaChangelog.d.ts +21 -0
  151. package/lib/types/client/components/QaComposer.d.ts +12 -2
  152. package/lib/types/client/components/QaMessage.d.ts +22 -2
  153. package/lib/types/client/components/QaSidebar.d.ts +69 -0
  154. package/lib/types/client/components/QaSourcesDrawer.d.ts +20 -0
  155. package/lib/types/client/components/QaTurnRail.d.ts +59 -0
  156. package/lib/types/client/components/QaWelcomeNotice.d.ts +27 -0
  157. package/lib/types/client/components/QaWidthHandle.d.ts +36 -0
  158. package/lib/types/client/components/QaWorkGroup.d.ts +19 -0
  159. package/lib/types/client/components/VariantSwitcher.d.ts +19 -0
  160. package/lib/types/client/components/format.d.ts +9 -0
  161. package/lib/types/client/components/source-refs.d.ts +16 -0
  162. package/lib/types/client/create-session.d.ts +16 -0
  163. package/lib/types/client/index.d.ts +2 -5
  164. package/lib/types/client/project-session-state.d.ts +30 -0
  165. package/lib/types/client/session-admission.d.ts +23 -0
  166. package/lib/types/client/session-assets.d.ts +20 -0
  167. package/lib/types/client/session-sources.d.ts +28 -0
  168. package/lib/types/client/settlement.d.ts +6 -0
  169. package/lib/types/client/tool-presentation.d.ts +6 -0
  170. package/lib/types/client/turn-sources.d.ts +14 -0
  171. package/lib/types/client/types.d.ts +93 -0
  172. package/lib/types/client/use-session-ui-state.d.ts +35 -0
  173. package/lib/types/client/use-transcript-view.d.ts +20 -0
  174. package/lib/types/client/wait-for.d.ts +11 -0
  175. package/lib/types/entry-redirect.d.ts +20 -0
  176. package/lib/types/host-route.d.ts +14 -1
  177. package/lib/types/index.d.ts +67 -7
  178. package/lib/types/launch-token.d.ts +21 -0
  179. package/lib/types/lockdown-policy.d.ts +18 -0
  180. package/lib/types/provenance/collector.d.ts +26 -0
  181. package/lib/types/provenance/connectors.d.ts +11 -0
  182. package/lib/types/provenance/dedupe.d.ts +6 -0
  183. package/lib/types/provenance/extractors.d.ts +14 -0
  184. package/lib/types/provenance/file-preview.d.ts +10 -0
  185. package/lib/types/provenance/host-store.d.ts +54 -0
  186. package/lib/types/provenance/index.d.ts +10 -0
  187. package/lib/types/provenance/normalize.d.ts +13 -0
  188. package/lib/types/provenance/reported.d.ts +5 -0
  189. package/lib/types/provenance/types.d.ts +78 -0
  190. package/lib/types/secure-session.d.ts +31 -0
  191. package/lib/types/types.d.ts +304 -1
  192. package/package.json +70 -27
@@ -0,0 +1,121 @@
1
+ import type { QaAccountRole, QaAccountSession, QaAccountUserPublic, QaClaimResult, QaOwnershipEntry, QaWhoamiResult } from "../types.js";
2
+ /**
3
+ * Coarse, wire-safe account failure codes. They ride the `(reason: <code>)`
4
+ * marker pattern the attestation path established; the browser maps them to
5
+ * audience-safe copy and the precise cause stays in the Host logs.
6
+ */
7
+ export type QaAccountsErrorReason = "auth-required" | "admin-required" | "invalid-credentials" | "account-disabled" | "email-taken" | "invalid-email" | "invalid-display-name" | "invalid-role" | "weak-password" | "registration-disabled" | "rate-limited" | "session-owned-elsewhere";
8
+ export declare class QaAccountsError extends Error {
9
+ readonly reason: QaAccountsErrorReason;
10
+ constructor(reason: QaAccountsErrorReason, message: string);
11
+ }
12
+ interface StoredUser {
13
+ readonly id: string;
14
+ readonly email: string;
15
+ readonly displayName: string;
16
+ readonly role: QaAccountRole;
17
+ /** scrypt material: hex salt + hex hash. */
18
+ readonly passwordHash: {
19
+ readonly salt: string;
20
+ readonly hash: string;
21
+ };
22
+ readonly createdAt: string;
23
+ lastLoginAt: string | null;
24
+ /** Disabled accounts are refused at login and lose their live tokens. */
25
+ disabled?: boolean;
26
+ /**
27
+ * Bumped to invalidate every token ever issued to the account: the value
28
+ * rides the token payload and must match at verification time.
29
+ */
30
+ tokenVersion?: number;
31
+ }
32
+ export interface QaAccountsOptions {
33
+ readonly sessionTtlDays: number;
34
+ readonly allowRegistration: boolean;
35
+ /** Login/registration attempts accepted per rolling minute, store-wide. */
36
+ readonly maxAuthAttemptsPerMinute?: number;
37
+ }
38
+ /** The accounts file lives next to the DSH home the launcher exports. */
39
+ export declare function defaultAccountsFilePath(): string;
40
+ /**
41
+ * Host authority for QA accounts: the file-backed user list, the session
42
+ * ownership map and the HMAC account tokens. All mutations are synchronous
43
+ * and persisted atomically (temp file + rename); LAN scale keeps this trivial.
44
+ */
45
+ export declare class QaAccounts {
46
+ readonly filePath: string;
47
+ private file;
48
+ private readonly authAttempts;
49
+ private readonly maxAuthAttemptsPerMinute;
50
+ private readonly sessionTtlDays;
51
+ private readonly allowRegistration;
52
+ /** mtime+size of the file as of the last load; the external-change probe. */
53
+ private lastLoadStamp;
54
+ constructor(filePath: string | undefined, options: QaAccountsOptions);
55
+ /**
56
+ * The qa-accounts CLI runs in its own process against the same file. Re-read
57
+ * it whenever it changed on disk, before every read or read-modify-write:
58
+ * without this a running Host would keep honoring tokens the CLI revoked
59
+ * and its next save would resurrect the CLI's change from a stale snapshot.
60
+ * One file, one writer at a time remains the deployment's discipline; LAN
61
+ * scale keeps the stat per operation trivial.
62
+ */
63
+ private reloadIfChanged;
64
+ private load;
65
+ private persist;
66
+ private save;
67
+ private pruneAuthAttempts;
68
+ private assertAuthBudget;
69
+ private userByEmail;
70
+ register(email: string, password: string, displayName?: string): QaAccountSession;
71
+ login(email: string, password: string): QaAccountSession;
72
+ whoami(token: string): QaWhoamiResult;
73
+ /** Resolve a token to its user, or null for absent/invalid/expired ones. */
74
+ verifyToken(token: string): string | null;
75
+ private mintToken;
76
+ /** The account behind a token, or an auth-required refusal. */
77
+ requireUser(token: string): StoredUser;
78
+ /**
79
+ * Ownership gate for one session: claim unowned sessions for the requesting
80
+ * user (first come, first served — the pre-accounts migration path), refuse
81
+ * foreign sessions unless the caller administers the deployment.
82
+ */
83
+ ensureSessionAccess(token: string, sessionId: string): QaAccountUserPublic;
84
+ /** Bulk-claim a browser's local chat index; foreign ids come back as conflicts. */
85
+ claimSessions(token: string, sessionIds: readonly string[]): QaClaimResult;
86
+ /** Session ids owned by the token's user, most recently claimed first. */
87
+ ownedSessionIds(token: string): readonly string[];
88
+ /**
89
+ * Every ownership entry with the owner's display name resolved at read
90
+ * time, oldest claim first. Admin-only: this is the cross-user view the
91
+ * admin sidebar groups chats by; ordinary accounts get a dedicated refusal.
92
+ */
93
+ listOwnership(token: string): readonly QaOwnershipEntry[];
94
+ /** Every account, file order; for `qa-accounts list`. */
95
+ listUsers(): readonly {
96
+ readonly email: string;
97
+ readonly displayName: string;
98
+ readonly role: QaAccountRole;
99
+ readonly disabled: boolean;
100
+ readonly createdAt: string;
101
+ readonly lastLoginAt: string | null;
102
+ }[];
103
+ /** Create an account outside the self-registration gate. */
104
+ addUser(email: string, password: string, options?: {
105
+ displayName?: string;
106
+ role?: QaAccountRole;
107
+ }): QaAccountUserPublic;
108
+ private editUser;
109
+ /** Grant or revoke the admin role. */
110
+ setUserRole(email: string, role: QaAccountRole): QaAccountUserPublic;
111
+ /**
112
+ * Disable an account: future logins are refused and every live token is
113
+ * invalidated (the token version bumps). `enable` does not resurrect old
114
+ * tokens — the account signs in again.
115
+ */
116
+ setUserDisabled(email: string, disabled: boolean): QaAccountUserPublic;
117
+ /** Invalidate every token of the account (password-leak response). */
118
+ revokeTokens(email: string): QaAccountUserPublic;
119
+ }
120
+ export {};
121
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Stable, coarse reason codes for QA policy attestation rejections.
3
+ *
4
+ * The RPC carrier folds a thrown Host error into `{ code, message, details }`
5
+ * and always empties `details`, so the wire message is the only channel back
6
+ * to the browser. The message therefore carries a `(reason: <code>)` marker:
7
+ * coarse enough to reveal nothing about the deployment policy to the QA
8
+ * audience, precise enough for an operator reading the browser console to
9
+ * know which class of mismatch to look for in the Host logs.
10
+ */
11
+ export type QaAttestationReason = "agent-unavailable" | "workspace-unavailable" | "composition-mismatch" | "permission-preset" | "adoption-refused" | "unknown-tools" | "auth-required" | "session-owned-elsewhere" | "attestation-failed";
12
+ /** Host rejection whose wire message carries the coarse reason marker. */
13
+ export declare class QaAttestationError extends Error {
14
+ readonly reason: QaAttestationReason;
15
+ constructor(reason: QaAttestationReason, message: string);
16
+ }
17
+ //# sourceMappingURL=attestation.d.ts.map
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ export interface CliIo {
3
+ out(line: string): void;
4
+ err(line: string): void;
5
+ }
6
+ /** Execute one management command; returns the process exit code. */
7
+ export declare function run(argv: readonly string[], io: CliIo, readPassword?: () => string): number;
8
+ export declare function main(argv: readonly string[], io?: CliIo, readPassword?: () => string): number;
9
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1,95 @@
1
+ import type { QaAccountUserPublic, QaOwnershipEntry, ResolvedQaSurfaceConfig } from "../types.js";
2
+ import type { QaAccountsApi, StorageLike } from "./types.js";
3
+ export type QaAccountsSnapshot = {
4
+ readonly stage: "checking";
5
+ } | {
6
+ readonly stage: "gate";
7
+ readonly mode: "login" | "register";
8
+ readonly busy: boolean;
9
+ readonly error: string | null;
10
+ } | {
11
+ readonly stage: "authed";
12
+ readonly user: QaAccountUserPublic;
13
+ /**
14
+ * Server-known chat ids; the sidebar list authority. Own chats always,
15
+ * plus every mapped chat for an admin (the cross-user grouping view).
16
+ */
17
+ readonly ownedIds: readonly string[];
18
+ /**
19
+ * The full ownership map; admins only, empty for ordinary accounts and
20
+ * whenever the admin listing was refused or unavailable.
21
+ */
22
+ readonly ownership: readonly QaOwnershipEntry[];
23
+ /** Bumped whenever ownedIds or ownership changes; re-projects lists. */
24
+ readonly ownedRevision: number;
25
+ };
26
+ /**
27
+ * Audience-safe copy for the coarse account refusal codes; anything unknown
28
+ * falls back to the generic line.
29
+ */
30
+ export declare function accountsErrorMessage(code: string | null): string;
31
+ /** Extract the coarse account reason code from a Host wire failure. */
32
+ export declare function accountsReasonOf(failure: unknown): string | null;
33
+ export interface QaAccountsControllerOptions {
34
+ readonly remote: QaAccountsApi;
35
+ readonly storage: StorageLike | undefined;
36
+ /** Config snapshot getter; the token key follows the deployment identity. */
37
+ readonly config: () => ResolvedQaSurfaceConfig;
38
+ /**
39
+ * The legacy per-browser chat index at login time, for the one-shot
40
+ * ownership migration; conflicts are dropped through {@link forgetChat}.
41
+ */
42
+ readonly legacyChatIds?: () => readonly string[];
43
+ readonly forgetChat?: (sessionId: string) => void;
44
+ }
45
+ /**
46
+ * Browser-side account lifecycle: token persistence, the checking → gate →
47
+ * authed state machine, and the ownership bookkeeping the sidebar list is
48
+ * built from. Anonymous/failed identity never blocks a deployment with
49
+ * accounts disabled — the caller simply does not mount this controller.
50
+ */
51
+ export declare class QaAccountsController {
52
+ private readonly options;
53
+ private readonly listeners;
54
+ private snapshot;
55
+ private tokenValue;
56
+ private disposed;
57
+ constructor(options: QaAccountsControllerOptions);
58
+ getSnapshot: () => QaAccountsSnapshot;
59
+ subscribe: (listener: () => void) => (() => void);
60
+ dispose(): void;
61
+ /** The bearer token threaded into gated remotes; null while anonymous. */
62
+ /** The bearer token threaded into gated remotes; null while anonymous. */
63
+ token(): string | null;
64
+ /** Server-owned chat ids (empty until authed); falls back to nothing. */
65
+ ownedIds(): readonly string[];
66
+ /** Boot probe: restore the stored token and ask the Host who it is. */
67
+ start(): Promise<void>;
68
+ login(email: string, password: string): Promise<void>;
69
+ register(email: string, password: string): Promise<void>;
70
+ setMode(mode: "login" | "register"): void;
71
+ /** Drop the token and return to the gate (logout or expired identity). */
72
+ signOut(): void;
73
+ /** Claim one freshly created chat so the ownership map stays current. */
74
+ claimNewSession(sessionId: string): Promise<void>;
75
+ private authenticate;
76
+ /** Own the account, migrate the legacy index, then project authed. */
77
+ private enterSession;
78
+ /** Refresh the owned list after index changes; cheap enough to re-pull. */
79
+ refreshOwned(): Promise<void>;
80
+ /** The admin cross-user ownership view; failures degrade to an empty map. */
81
+ private fetchOwnership;
82
+ /** Session id → owner display name; empty unless an admin is signed in. */
83
+ ownerNames(): ReadonlyMap<string, string>;
84
+ /**
85
+ * The chat owner's display name for author labels: admins see it on
86
+ * foreign chats only; the owner themself and ordinary accounts see none.
87
+ */
88
+ messageAuthorOf(sessionId: string): string | undefined;
89
+ private readStoredToken;
90
+ private saveStoredToken;
91
+ private clearStoredToken;
92
+ private tokenKey;
93
+ private publish;
94
+ }
95
+ //# sourceMappingURL=QaAccountsController.d.ts.map
@@ -1,21 +1,53 @@
1
- import type { SettingsScope } from "@deepseek-ai/dsh-client-runtime/client";
1
+ import type { SettingsScope } from "@deepseek-ai/dsh-client-ui-settings/client";
2
2
  import type { QaSurfaceConfig, ResolvedQaSurfaceConfig } from "../types.js";
3
3
  export interface QaConfigSnapshot {
4
4
  readonly status: "loading" | "ready" | "unavailable" | "error";
5
5
  readonly config: ResolvedQaSurfaceConfig;
6
6
  readonly error: string | null;
7
7
  }
8
- /** Browser projection of the Host-owned qa-surface settings namespace. */
8
+ /** Reads the effective Host configuration when the settings namespace cannot. */
9
+ export type QaConfigFallback = () => Promise<ResolvedQaSurfaceConfig>;
10
+ /**
11
+ * Browser projection of the Host-owned qa-surface settings namespace.
12
+ *
13
+ * The DSH gateway pins settings RPCs to loopback, so a browser served over the
14
+ * LAN always sees the namespace as `unavailable`. When the caller supplies the
15
+ * `qaSurface/describe` Remote as a fallback, that channel answers instead: the
16
+ * projection waits in `loading` until it settles, then reports the Host-owned
17
+ * configuration as `ready`. A rejected fallback returns the projection to
18
+ * `unavailable`, exactly as when no fallback exists.
19
+ */
9
20
  export declare class QaConfigController {
10
21
  private readonly scope;
11
22
  private readonly listeners;
12
23
  private snapshot;
13
24
  private readonly unsubscribe;
14
- constructor(scope: SettingsScope<QaSurfaceConfig>);
25
+ private readonly fallback;
26
+ private fallbackState;
27
+ private fallbackConfig;
28
+ private disposed;
29
+ constructor(scope: SettingsScope<QaSurfaceConfig>, fallback?: QaConfigFallback);
15
30
  getSnapshot: () => QaConfigSnapshot;
16
31
  subscribe: (listener: () => void) => (() => void);
17
32
  dispose(): void;
18
33
  private project;
19
34
  private refresh;
35
+ /**
36
+ * Re-read the Host configuration after a Host reconnect. Without this, an
37
+ * open page keeps the configuration it fetched at load time: after the Host
38
+ * restarts with a changed deployment config, the next attestation compares
39
+ * its proof against the stale client copy and refuses. While re-reading,
40
+ * the previous answer stays projected — no loading flicker; the snapshot
41
+ * updates only when the answer actually changed.
42
+ */
43
+ refreshFallback(): void;
44
+ /**
45
+ * The fallback trigger rides every refresh: the scope flips to
46
+ * `unavailable` whenever the browser is served over the LAN, which a
47
+ * constructor-time check can miss. The state guard makes repeats a no-op,
48
+ * so one fetch runs per controller lifetime; the recursive refresh lets the
49
+ * pending state surface before the first notification pass.
50
+ */
51
+ private requestFallbackIfNeeded;
20
52
  }
21
53
  //# sourceMappingURL=QaConfigController.d.ts.map
@@ -1,59 +1,185 @@
1
- import type { HostDescriptionSource, IApiClient } from "@deepseek-ai/dsh-client-connection/client";
2
- import type { ISessions, SessionListState } from "@deepseek-ai/dsh-client-runtime/client";
3
- import type { QaSessionState, ResolvedQaSurfaceConfig } from "../types.js";
4
- type QaSessionsApi = Pick<IApiClient["sessions"], "create" | "selectModel">;
5
- export interface StorageLike {
6
- getItem(key: string): string | null;
7
- setItem(key: string, value: string): void;
8
- removeItem(key: string): void;
1
+ import type { ConnectionGenerationState } from "@deepseek-ai/dsh-client-connection/client";
2
+ import type { QaImageDraft, QaSessionState, ResolvedQaSurfaceConfig } from "../types.js";
3
+ import type { QaConversation, QaSecureSession, QaSessions, QaSessionsApi, QaSourceApi, StorageLike } from "./types.js";
4
+ export interface QaAccountsFacade {
5
+ /** The account bearer token, or null while anonymous. */
6
+ readonly token: () => string | null;
7
+ /** Server-owned chat ids; the list authority while accounts are on. */
8
+ readonly ownedIds: () => readonly string[];
9
+ /**
10
+ * The chat owner's display name for author labels; admins see it on
11
+ * foreign chats only, everyone else gets undefined.
12
+ */
13
+ readonly messageAuthorOf: (sessionId: string) => string | undefined;
14
+ /** Called after a new chat binds, so ownership stays current. */
15
+ readonly onSessionCreated: (sessionId: string) => void;
16
+ /** Called when the Host refuses with auth-required (expired/rotated). */
17
+ readonly onAuthRequired: () => void;
9
18
  }
10
19
  export interface QaSessionControllerOptions {
11
- readonly sessions: ISessions;
20
+ readonly sessions: QaSessions;
12
21
  readonly api: QaSessionsApi;
13
- readonly connection: HostDescriptionSource;
22
+ /** Conversation assembly feeding the transcript projection. */
23
+ readonly conversation: QaConversation;
24
+ readonly connection: ConnectionGenerationState;
14
25
  readonly config: ResolvedQaSurfaceConfig;
26
+ readonly secureSession: QaSecureSession;
27
+ readonly sourceApi?: QaSourceApi;
15
28
  readonly storage?: StorageLike;
29
+ /** Present while the deployment gates QA users with accounts. */
30
+ readonly accounts?: QaAccountsFacade;
16
31
  readonly timeoutMs?: number;
32
+ /**
33
+ * Minimum spacing between projections of a running turn's stream frames.
34
+ * The host runtime already batches deltas to one notification per
35
+ * animation frame; this ceiling keeps the render path flat on long
36
+ * transcripts and weak hardware. Zero disables the spacing.
37
+ */
38
+ readonly streamIntervalMs?: number;
17
39
  }
18
- /** The only module that couples QA behavior to DSH Session/client APIs. */
40
+ /**
41
+ * The only module that couples QA behavior to DSH Session/client APIs. It owns
42
+ * the chat lifecycle state machine; the observable wait, browser-local chat
43
+ * bookkeeping and policy-attestation details live in their own modules.
44
+ */
19
45
  export declare class QaSessionController {
20
46
  private readonly listeners;
21
47
  private readonly sessions;
22
48
  private readonly api;
49
+ private readonly conversation;
23
50
  private readonly connection;
24
51
  private readonly config;
25
- private readonly storage;
52
+ private readonly secureSessionRemote;
53
+ private readonly sourceApi;
54
+ private readonly chats;
55
+ private readonly accounts;
56
+ /** Per-chat attachment URL cache; blob URLs die with the chat binding. */
57
+ private readonly assets;
26
58
  private readonly timeoutMs;
59
+ private readonly streamIntervalMs;
27
60
  private state;
28
61
  private session;
62
+ private conversationBinding;
29
63
  private unsubscribeSession;
64
+ private unsubscribeChat;
30
65
  private readonly unsubscribeConnection;
31
66
  private ensuring;
67
+ private materializing;
32
68
  private operationError;
33
69
  private admissionPending;
70
+ private policyReady;
71
+ private drafting;
72
+ private streamFlushTimer;
73
+ /** The chat session to return to when a subagent view closes. */
74
+ private chatSessionId;
75
+ private viewingSubagent;
34
76
  private connectedOnce;
35
77
  private disposed;
36
78
  private generation;
79
+ private chatsRevision;
80
+ /** Host-side provenance of the bound chat, merged into the projection. */
81
+ private readonly hostSources;
37
82
  constructor(options: QaSessionControllerOptions);
38
83
  getSnapshot: () => QaSessionState;
39
84
  subscribe: (listener: () => void) => (() => void);
40
85
  ensureSession(): Promise<void>;
41
- send(text: string): Promise<boolean>;
86
+ send(text: string, images?: readonly QaImageDraft[]): Promise<boolean>;
87
+ /**
88
+ * Ask for a fresh variant of the previous answer. The session has no
89
+ * truncation seam, so this sends the hidden regeneration instruction as an
90
+ * ordinary prompt: the model produces a follow-up turn, the projection
91
+ * hides the marker message, and the consecutive turns read as variants of
92
+ * the original question (switched in the surface).
93
+ */
94
+ regenerate(): Promise<boolean>;
42
95
  stop(): Promise<void>;
43
- reset(): Promise<void>;
96
+ /**
97
+ * Enter the "new chat" draft: show an empty composer without creating any
98
+ * session. Nothing appears in the chat list and no Host session is spent
99
+ * until the first prompt is actually sent, which materializes the session
100
+ * lazily ({@link materializeDraft}). Fixed-policy deployments cannot draft.
101
+ */
102
+ startDraft(): Promise<void>;
103
+ /**
104
+ * Turn the current draft into a real attested session. Runs inside the
105
+ * send flow; concurrent sends share one materialization. A refusal here is
106
+ * reported at once — a fresh session's first attestation is the
107
+ * deployment's only chance to learn the reason.
108
+ */
109
+ private materializeDraft;
110
+ /**
111
+ * Open one of this browser's indexed chats. The id must be present in the
112
+ * host session list and pass policy attestation; an unknown id is forgotten
113
+ * from the index. Fixed-policy deployments cannot switch.
114
+ */
115
+ switchTo(sessionId: string): Promise<void>;
116
+ /**
117
+ * Watch a subagent's transcript live. The binding is read-only by
118
+ * construction: no attestation runs (attestation admits sending, and the
119
+ * composer stays disabled here), the chat index is untouched, and
120
+ * {@link closeSubagent} returns to the chat the panel was opened from.
121
+ */
122
+ viewSubagent(id: string, title: string): Promise<void>;
123
+ /** Leave the subagent transcript and re-open the chat it was launched from. */
124
+ closeSubagent(): Promise<void>;
125
+ /** This browser's chat ids: the owned list when accounts are on, else the
126
+ * local index. Both intersect the host session list at projection time. */
127
+ chatIds(): readonly string[];
128
+ /** The bound session id, or null while no chat is bound. */
129
+ activeSessionId(): string | null;
130
+ /**
131
+ * Resolve one durable image attachment of the bound session into a
132
+ * browser-usable URL. Resolutions are cached per chat in the asset
133
+ * repository (attachment ids repeat across chats) and revoked when the chat
134
+ * is unbound; failures surface as broken views and retry on demand.
135
+ */
136
+ readImage(attachmentId: string): Promise<string>;
137
+ private readAttachmentUrl;
138
+ /**
139
+ * Remove one chat from this browser's index. Deleting the chat that is
140
+ * currently open also forgets the persisted id and starts a fresh attested
141
+ * session (when the deployment allows session resets); the Host-side
142
+ * session itself is not touched — no host deletion seam exists.
143
+ */
144
+ deleteChat(id: string): Promise<void>;
145
+ /** Drop one chat id from this browser's index. */
146
+ forgetChat(sessionId: string): void;
44
147
  dispose(): void;
45
148
  private ensureSessionNow;
46
- private createSession;
149
+ /**
150
+ * Recovery ladder after a refused send-time attestation. The prompt was
151
+ * never admitted, so nothing here can duplicate it. Step 1 re-opens the
152
+ * session: a Host that idled the agent out dismantles its tool view, and
153
+ * re-opening re-materializes it. Step 2 — when that refused session is
154
+ * still blank (it never received the prompt, so there is nothing to lose) —
155
+ * starts a fresh attested session instead. A session with history is left
156
+ * alone: recreating it would silently drop the conversation. Returns the
157
+ * attested session id, or null when nothing was attested.
158
+ */
159
+ private recoverForSend;
47
160
  private bind;
48
161
  private unbind;
49
162
  private waitForConnection;
163
+ /**
164
+ * Project one session notification. A running turn's frames arrive at
165
+ * animation-frame cadence, so re-projections are spaced at least
166
+ * {@link streamIntervalMs} apart: the first frame of a window projects at
167
+ * once and further frames are absorbed (the memoized render path replays
168
+ * nothing, so absorbing a frame only defers it). Everything outside a
169
+ * running turn - phase flips, errors, turn completion - projects
170
+ * immediately.
171
+ */
172
+ private publishSessionUpdate;
173
+ private clearStreamFlush;
50
174
  private publish;
51
175
  private fail;
52
- private storageKey;
53
- private readPersisted;
54
- private persist;
55
- private clearPersisted;
176
+ /**
177
+ * Attest the currently bound session. `ok` carries the session id the proof
178
+ * was issued for; `stale` means the binding changed while the request was
179
+ * in flight, so nothing was written — the newer binding manages its own
180
+ * attestation; `refused` is a genuine admission refusal of this binding.
181
+ */
182
+ private attestPolicy;
56
183
  private emit;
57
184
  }
58
- export type { SessionListState };
59
185
  //# sourceMappingURL=QaSessionController.d.ts.map
@@ -1,14 +1,20 @@
1
- import type { HostDescriptionSource, IApiClient } from "@deepseek-ai/dsh-client-connection/client";
2
- import type { ISessions } from "@deepseek-ai/dsh-client-runtime/client";
1
+ import type { ConnectionGenerationState } from "@deepseek-ai/dsh-client-connection/client";
3
2
  import type { InjectFace, PropsRuntime } from "@deepseek-ai/dsh-client-ui-slots";
4
3
  import type { QaConfigController } from "./QaConfigController.js";
5
4
  import type { QaRouteController } from "./QaRouteController.js";
5
+ import type { QaAccountsController } from "./QaAccountsController.js";
6
+ import type { QaConversation, QaSecureSession, QaSessions, QaSessionsApi, QaSourceApi } from "./types.js";
6
7
  export interface QaSurfaceFace {
7
8
  readonly route: QaRouteController;
8
9
  readonly config: QaConfigController;
9
- readonly sessions: ISessions;
10
- readonly api: Pick<IApiClient["sessions"], "create" | "selectModel">;
11
- readonly connection: HostDescriptionSource;
10
+ readonly sessions: QaSessions;
11
+ readonly conversation: QaConversation;
12
+ readonly api: QaSessionsApi;
13
+ readonly connection: ConnectionGenerationState;
14
+ readonly secureSession: QaSecureSession;
15
+ readonly sourceApi: QaSourceApi;
16
+ /** Present when the deployment mounts the QA account gate. */
17
+ readonly accounts?: QaAccountsController;
12
18
  }
13
19
  type QaSurfaceProps = PropsRuntime<"shell.overlay"> & InjectFace<QaSurfaceFace>;
14
20
  export declare function QaSurface(props: QaSurfaceProps): import("react/jsx-runtime").JSX.Element | null;
@@ -1,7 +1,17 @@
1
- import type { ConversationSnapshot } from "@deepseek-ai/dsh-client-runtime/client";
1
+ import type { ConversationSnapshot } from "@deepseek-ai/dsh-client-ui-conversation/client";
2
2
  import type { QaMessage } from "../types.js";
3
- /** Project only end-user-safe text from the public DSH conversation snapshot. */
4
- export declare function projectTranscript(snapshot: ConversationSnapshot, options?: {
3
+ /**
4
+ * The hidden regeneration prompt. Regenerate sends this as an ordinary
5
+ * prompt — the session has no truncation seam, so a "variant" is a real
6
+ * follow-up turn — and the projection drops it from the visible transcript,
7
+ * leaving the consecutive answer turns to read as variants of one question.
8
+ */
9
+ export declare const QA_REGENERATE_MARKER = "\u041F\u0435\u0440\u0435\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0439 \u0441\u0432\u043E\u0439 \u043F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0438\u0439 \u043E\u0442\u0432\u0435\u0442 \u2014 \u0434\u0430\u0439 \u043D\u043E\u0432\u044B\u0439 \u0432\u0430\u0440\u0438\u0430\u043D\u0442, \u043D\u0435 \u043F\u043E\u0432\u0442\u043E\u0440\u044F\u044F \u043F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0438\u0439.";
10
+ /** Project end-user messages plus optional operator-approved work detail. */
11
+ export declare function projectTranscript(snapshot: ConversationSnapshot | undefined, options?: {
12
+ /** Live turn lifecycle from the bound Session (drives running-row status). */
13
+ readonly running?: boolean;
5
14
  readonly showToolActivity?: boolean;
15
+ readonly showReasoning?: boolean;
6
16
  }): readonly QaMessage[];
7
17
  //# sourceMappingURL=QaTranscriptAdapter.d.ts.map
@@ -0,0 +1,25 @@
1
+ import type { QaLockdownProof, ResolvedQaSurfaceConfig } from "../types.js";
2
+ /**
3
+ * Browser-side reading of Host policy attestation: one error type the session
4
+ * lifecycle branches on, plus pure diagnostics for the console. The specific
5
+ * mismatch facts stay in the Host logs; a refusal only reveals the coarse
6
+ * reason class and an operator hint.
7
+ */
8
+ export declare class QaPolicyAttestationError extends Error {
9
+ constructor();
10
+ }
11
+ /**
12
+ * Extract the coarse attestation reason code from a Host wire failure, if the
13
+ * failure carries the marker.
14
+ */
15
+ export declare function attestationReasonOf(failure: {
16
+ readonly message?: string;
17
+ } | undefined | null): string | null;
18
+ export declare function attestationHint(reason: string | null): string;
19
+ /**
20
+ * Whether a Host proof answers the deployment lockdown config this browser
21
+ * holds. Every field must match; the allow-list is compared as exact JSON so
22
+ * order differences refuse too, mirroring the Host's pinned list.
23
+ */
24
+ export declare function proofMatchesConfig(proof: QaLockdownProof, lockdown: ResolvedQaSurfaceConfig["lockdown"], sessionId: string): boolean;
25
+ //# sourceMappingURL=attestation.d.ts.map
@@ -0,0 +1,28 @@
1
+ import type { StorageLike } from "./types.js";
2
+ /**
3
+ * Browser-local chat bookkeeping: a most-recently-used index of chat ids for
4
+ * the sidebar plus the id of the chat this browser reopens on load. Both live
5
+ * under the deployment's storage key and route path, and every access
6
+ * tolerates a denied or broken store — persistence must never prevent a real
7
+ * DSH session.
8
+ */
9
+ export declare class QaChatIndex {
10
+ private readonly storage;
11
+ private readonly sessionKey;
12
+ private readonly chatKey;
13
+ constructor(storage: StorageLike | undefined, prefix: string);
14
+ /** This browser's indexed chat ids, most recently used first. */
15
+ chatIds(): readonly string[];
16
+ /**
17
+ * Add one chat to the index without reordering anything. Display order is
18
+ * the host's `updatedAt`, so merely opening a chat must not move it — the
19
+ * index is membership only.
20
+ */
21
+ addChat(sessionId: string): void;
22
+ forgetChat(sessionId: string): void;
23
+ /** The persisted chat id this browser reopens on load, or null. */
24
+ activeId(): string | null;
25
+ saveActive(sessionId: string): void;
26
+ clearActive(): void;
27
+ }
28
+ //# sourceMappingURL=chat-index.d.ts.map
@@ -1,5 +1,19 @@
1
- /** Deliberately small, HTML-free Markdown renderer for assistant-visible text. */
2
- export declare function Markdown({ text }: {
1
+ import type { QaSource } from "../../types.js";
2
+ import { type QaSourceRefs } from "./source-refs.js";
3
+ /** Inline source awareness handed down from the owning assistant message. */
4
+ export interface MarkdownSourceContext {
5
+ readonly refs?: QaSourceRefs;
6
+ /** Open a path-backed source in the sources drawer's detail view. */
7
+ readonly onSourceOpen?: (source: QaSource) => void;
8
+ }
9
+ /**
10
+ * Deliberately small, HTML-free Markdown renderer for assistant-visible text.
11
+ * Memoized on the text value: stream updates re-render only the message whose
12
+ * text actually changed, so committed history is never re-parsed.
13
+ */
14
+ export declare const Markdown: import("react").MemoExoticComponent<({ text, sourceRefs, onSourceOpen, }: {
3
15
  readonly text: string;
4
- }): import("react/jsx-runtime").JSX.Element;
16
+ readonly sourceRefs?: QaSourceRefs;
17
+ readonly onSourceOpen?: (source: QaSource) => void;
18
+ }) => import("react/jsx-runtime").JSX.Element>;
5
19
  //# sourceMappingURL=Markdown.d.ts.map