@yadsh/dsh-qa-surface 0.1.1 → 0.2.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 (89) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +162 -14
  3. package/SPEC-dsh-qa-surface.md +600 -96
  4. package/capability-policy.json +5 -0
  5. package/compatibility.json +7 -1
  6. package/cordis.patch.yml +11 -0
  7. package/docs/ARCHITECTURE.md +49 -7
  8. package/docs/COMPATIBILITY.md +5 -4
  9. package/docs/CONFIGURATION.md +96 -2
  10. package/lib/attestation.js +10 -0
  11. package/lib/attestation.js.map +1 -0
  12. package/lib/client/QaConfigController.js +111 -4
  13. package/lib/client/QaConfigController.js.map +1 -1
  14. package/lib/client/QaSessionController.js +501 -119
  15. package/lib/client/QaSessionController.js.map +1 -1
  16. package/lib/client/QaSurface.js +137 -20
  17. package/lib/client/QaSurface.js.map +1 -1
  18. package/lib/client/QaTranscriptAdapter.js +534 -45
  19. package/lib/client/QaTranscriptAdapter.js.map +1 -1
  20. package/lib/client/attestation.js +60 -0
  21. package/lib/client/attestation.js.map +1 -0
  22. package/lib/client/chat-index.js +86 -0
  23. package/lib/client/chat-index.js.map +1 -0
  24. package/lib/client/components/Markdown.js +75 -5
  25. package/lib/client/components/Markdown.js.map +1 -1
  26. package/lib/client/components/QaAgentsDrawer.js +40 -0
  27. package/lib/client/components/QaAgentsDrawer.js.map +1 -0
  28. package/lib/client/components/QaComposer.js +132 -15
  29. package/lib/client/components/QaComposer.js.map +1 -1
  30. package/lib/client/components/QaMessage.js +184 -10
  31. package/lib/client/components/QaMessage.js.map +1 -1
  32. package/lib/client/components/QaSidebar.js +124 -0
  33. package/lib/client/components/QaSidebar.js.map +1 -0
  34. package/lib/client/components/QaSourcesDrawer.js +46 -0
  35. package/lib/client/components/QaSourcesDrawer.js.map +1 -0
  36. package/lib/client/components/QaWorkGroup.js +135 -0
  37. package/lib/client/components/QaWorkGroup.js.map +1 -0
  38. package/lib/client/components/VariantSwitcher.js +29 -0
  39. package/lib/client/components/VariantSwitcher.js.map +1 -0
  40. package/lib/client/components/format.js +48 -0
  41. package/lib/client/components/format.js.map +1 -0
  42. package/lib/client/index.js +77 -34
  43. package/lib/client/index.js.map +1 -1
  44. package/lib/client/styles.js +308 -43
  45. package/lib/client/styles.js.map +1 -1
  46. package/lib/client/types.js +13 -0
  47. package/lib/client/types.js.map +1 -0
  48. package/lib/client/wait-for.js +49 -0
  49. package/lib/client/wait-for.js.map +1 -0
  50. package/lib/client.js +8228 -383
  51. package/lib/client.js.map +1 -1
  52. package/lib/config.js +61 -9
  53. package/lib/config.js.map +1 -1
  54. package/lib/index.js +166 -70
  55. package/lib/index.js.map +1 -1
  56. package/lib/lockdown-policy.js +20 -0
  57. package/lib/lockdown-policy.js.map +1 -0
  58. package/lib/resolve-config.js +122 -10
  59. package/lib/resolve-config.js.map +1 -1
  60. package/lib/secure-session.js +170 -0
  61. package/lib/secure-session.js.map +1 -0
  62. package/lib/typert.host.d.ts +3 -0
  63. package/lib/typert.host.js +191 -0
  64. package/lib/typert.remote-client.d.ts +24 -0
  65. package/lib/typert.remote-client.js +129 -0
  66. package/lib/types/attestation.d.ts +17 -0
  67. package/lib/types/client/QaConfigController.d.ts +34 -2
  68. package/lib/types/client/QaSessionController.d.ts +106 -19
  69. package/lib/types/client/QaSurface.d.ts +5 -4
  70. package/lib/types/client/QaTranscriptAdapter.d.ts +17 -2
  71. package/lib/types/client/attestation.d.ts +25 -0
  72. package/lib/types/client/chat-index.d.ts +28 -0
  73. package/lib/types/client/components/Markdown.d.ts +7 -3
  74. package/lib/types/client/components/QaAgentsDrawer.d.ts +25 -0
  75. package/lib/types/client/components/QaComposer.d.ts +12 -2
  76. package/lib/types/client/components/QaMessage.d.ts +14 -1
  77. package/lib/types/client/components/QaSidebar.d.ts +41 -0
  78. package/lib/types/client/components/QaSourcesDrawer.d.ts +8 -0
  79. package/lib/types/client/components/QaWorkGroup.d.ts +19 -0
  80. package/lib/types/client/components/VariantSwitcher.d.ts +19 -0
  81. package/lib/types/client/components/format.d.ts +9 -0
  82. package/lib/types/client/index.d.ts +1 -5
  83. package/lib/types/client/types.d.ts +29 -0
  84. package/lib/types/client/wait-for.d.ts +11 -0
  85. package/lib/types/index.d.ts +22 -6
  86. package/lib/types/lockdown-policy.d.ts +18 -0
  87. package/lib/types/secure-session.d.ts +22 -0
  88. package/lib/types/types.d.ts +148 -1
  89. package/package.json +29 -3
@@ -1,59 +1,146 @@
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;
9
- }
1
+ import type { HostDescriptionSource } from "@deepseek-ai/dsh-client-connection/client";
2
+ import type { QaImageDraft, QaSessionState, ResolvedQaSurfaceConfig } from "../types.js";
3
+ import type { QaSecureSession, QaSessions, QaSessionsApi, StorageLike } from "./types.js";
10
4
  export interface QaSessionControllerOptions {
11
- readonly sessions: ISessions;
5
+ readonly sessions: QaSessions;
12
6
  readonly api: QaSessionsApi;
13
7
  readonly connection: HostDescriptionSource;
14
8
  readonly config: ResolvedQaSurfaceConfig;
9
+ readonly secureSession: QaSecureSession;
15
10
  readonly storage?: StorageLike;
16
11
  readonly timeoutMs?: number;
12
+ /**
13
+ * Minimum spacing between projections of a running turn's stream frames.
14
+ * The host runtime already batches deltas to one notification per
15
+ * animation frame; this ceiling keeps the render path flat on long
16
+ * transcripts and weak hardware. Zero disables the spacing.
17
+ */
18
+ readonly streamIntervalMs?: number;
17
19
  }
18
- /** The only module that couples QA behavior to DSH Session/client APIs. */
20
+ /**
21
+ * The only module that couples QA behavior to DSH Session/client APIs. It owns
22
+ * the chat lifecycle state machine; the observable wait, browser-local chat
23
+ * bookkeeping and policy-attestation details live in their own modules.
24
+ */
19
25
  export declare class QaSessionController {
20
26
  private readonly listeners;
21
27
  private readonly sessions;
22
28
  private readonly api;
23
29
  private readonly connection;
24
30
  private readonly config;
25
- private readonly storage;
31
+ private readonly secureSessionRemote;
32
+ private readonly chats;
26
33
  private readonly timeoutMs;
34
+ private readonly streamIntervalMs;
27
35
  private state;
28
36
  private session;
29
37
  private unsubscribeSession;
30
38
  private readonly unsubscribeConnection;
31
39
  private ensuring;
40
+ private materializing;
32
41
  private operationError;
33
42
  private admissionPending;
43
+ private policyReady;
44
+ private drafting;
45
+ private streamFlushTimer;
46
+ /** The chat session to return to when a subagent view closes. */
47
+ private chatSessionId;
48
+ private viewingSubagent;
34
49
  private connectedOnce;
35
50
  private disposed;
36
51
  private generation;
52
+ private chatsRevision;
37
53
  constructor(options: QaSessionControllerOptions);
38
54
  getSnapshot: () => QaSessionState;
39
55
  subscribe: (listener: () => void) => (() => void);
40
56
  ensureSession(): Promise<void>;
41
- send(text: string): Promise<boolean>;
57
+ send(text: string, images?: readonly QaImageDraft[]): Promise<boolean>;
58
+ /**
59
+ * Ask for a fresh variant of the previous answer. The session has no
60
+ * truncation seam, so this sends the hidden regeneration instruction as an
61
+ * ordinary prompt: the model produces a follow-up turn, the projection
62
+ * hides the marker message, and the consecutive turns read as variants of
63
+ * the original question (switched in the surface).
64
+ */
65
+ regenerate(): Promise<boolean>;
42
66
  stop(): Promise<void>;
43
- reset(): Promise<void>;
67
+ /**
68
+ * Enter the "new chat" draft: show an empty composer without creating any
69
+ * session. Nothing appears in the chat list and no Host session is spent
70
+ * until the first prompt is actually sent, which materializes the session
71
+ * lazily ({@link materializeDraft}). Fixed-policy deployments cannot draft.
72
+ */
73
+ startDraft(): Promise<void>;
74
+ /**
75
+ * Turn the current draft into a real attested session. Runs inside the
76
+ * send flow; concurrent sends share one materialization. A refusal here is
77
+ * reported at once — a fresh session's first attestation is the
78
+ * deployment's only chance to learn the reason.
79
+ */
80
+ private materializeDraft;
81
+ /**
82
+ * Open one of this browser's indexed chats. The id must be present in the
83
+ * host session list and pass policy attestation; an unknown id is forgotten
84
+ * from the index. Fixed-policy deployments cannot switch.
85
+ */
86
+ switchTo(sessionId: string): Promise<void>;
87
+ /**
88
+ * Watch a subagent's transcript live. The binding is read-only by
89
+ * construction: no attestation runs (attestation admits sending, and the
90
+ * composer stays disabled here), the chat index is untouched, and
91
+ * {@link closeSubagent} returns to the chat the panel was opened from.
92
+ */
93
+ viewSubagent(id: string, title: string): Promise<void>;
94
+ /** Leave the subagent transcript and re-open the chat it was launched from. */
95
+ closeSubagent(): Promise<void>;
96
+ /** This browser's indexed chat ids, most recently used first. */
97
+ chatIds(): readonly string[];
98
+ /** The bound session id, or null while no chat is bound. */
99
+ activeSessionId(): string | null;
100
+ /**
101
+ * Resolve one durable image attachment of the bound session into a
102
+ * browser-usable URL. The caller caches; failures surface as broken views.
103
+ */
104
+ readImage(attachmentId: string): Promise<string>;
105
+ /**
106
+ * Remove one chat from this browser's index. Deleting the chat that is
107
+ * currently open also forgets the persisted id and starts a fresh attested
108
+ * session (when the deployment allows session resets); the Host-side
109
+ * session itself is not touched — no host deletion seam exists.
110
+ */
111
+ deleteChat(id: string): Promise<void>;
112
+ /** Drop one chat id from this browser's index. */
113
+ forgetChat(sessionId: string): void;
44
114
  dispose(): void;
45
115
  private ensureSessionNow;
116
+ /**
117
+ * Recovery ladder after a refused send-time attestation. The prompt was
118
+ * never admitted, so nothing here can duplicate it. Step 1 re-opens the
119
+ * session: a Host that idled the agent out dismantles its tool view, and
120
+ * re-opening re-materializes it. Step 2 — when that refused session is
121
+ * still blank (it never received the prompt, so there is nothing to lose) —
122
+ * starts a fresh attested session instead. A session with history is left
123
+ * alone: recreating it would silently drop the conversation.
124
+ */
125
+ private recoverForSend;
46
126
  private createSession;
47
127
  private bind;
48
128
  private unbind;
49
129
  private waitForConnection;
130
+ /**
131
+ * Project one session notification. A running turn's frames arrive at
132
+ * animation-frame cadence, so re-projections are spaced at least
133
+ * {@link streamIntervalMs} apart: the first frame of a window projects at
134
+ * once and further frames are absorbed (the memoized render path replays
135
+ * nothing, so absorbing a frame only defers it). Everything outside a
136
+ * running turn - phase flips, errors, turn completion - projects
137
+ * immediately.
138
+ */
139
+ private publishSessionUpdate;
140
+ private clearStreamFlush;
50
141
  private publish;
51
142
  private fail;
52
- private storageKey;
53
- private readPersisted;
54
- private persist;
55
- private clearPersisted;
143
+ private attestPolicy;
56
144
  private emit;
57
145
  }
58
- export type { SessionListState };
59
146
  //# sourceMappingURL=QaSessionController.d.ts.map
@@ -1,14 +1,15 @@
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 { HostDescriptionSource } 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 { QaSecureSession, QaSessions, QaSessionsApi } from "./types.js";
6
6
  export interface QaSurfaceFace {
7
7
  readonly route: QaRouteController;
8
8
  readonly config: QaConfigController;
9
- readonly sessions: ISessions;
10
- readonly api: Pick<IApiClient["sessions"], "create" | "selectModel">;
9
+ readonly sessions: QaSessions;
10
+ readonly api: QaSessionsApi;
11
11
  readonly connection: HostDescriptionSource;
12
+ readonly secureSession: QaSecureSession;
12
13
  }
13
14
  type QaSurfaceProps = PropsRuntime<"shell.overlay"> & InjectFace<QaSurfaceFace>;
14
15
  export declare function QaSurface(props: QaSurfaceProps): import("react/jsx-runtime").JSX.Element | null;
@@ -1,7 +1,22 @@
1
1
  import type { ConversationSnapshot } from "@deepseek-ai/dsh-client-runtime/client";
2
- import type { QaMessage } from "../types.js";
3
- /** Project only end-user-safe text from the public DSH conversation snapshot. */
2
+ import type { QaMessage, QaSource } from "../types.js";
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. */
4
11
  export declare function projectTranscript(snapshot: ConversationSnapshot, options?: {
5
12
  readonly showToolActivity?: boolean;
13
+ readonly showReasoning?: boolean;
6
14
  }): readonly QaMessage[];
15
+ /**
16
+ * Collect the sources this chat has actually touched, in first-use order:
17
+ * fetched pages, searches, and files read. This is the same tool activity the
18
+ * work groups render, projected as a flat citation-style list, so it carries
19
+ * no information beyond `ui.showToolActivity`.
20
+ */
21
+ export declare function projectSources(snapshot: ConversationSnapshot): readonly QaSource[];
7
22
  //# 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,9 @@
1
- /** Deliberately small, HTML-free Markdown renderer for assistant-visible text. */
2
- export declare function Markdown({ text }: {
1
+ /**
2
+ * Deliberately small, HTML-free Markdown renderer for assistant-visible text.
3
+ * Memoized on the text value: stream updates re-render only the message whose
4
+ * text actually changed, so committed history is never re-parsed.
5
+ */
6
+ export declare const Markdown: import("react").MemoExoticComponent<({ text, }: {
3
7
  readonly text: string;
4
- }): import("react/jsx-runtime").JSX.Element;
8
+ }) => import("react/jsx-runtime").JSX.Element>;
5
9
  //# sourceMappingURL=Markdown.d.ts.map
@@ -0,0 +1,25 @@
1
+ import type { SessionSummary } from "@deepseek-ai/dsh-client-runtime/client";
2
+ /** One row of the agents panel: a subagent session of the open chat. */
3
+ export interface QaSubagentRow {
4
+ readonly id: string;
5
+ readonly title: string;
6
+ readonly running: boolean;
7
+ readonly completed: boolean;
8
+ readonly meta: string;
9
+ }
10
+ /**
11
+ * Direct subagent children of the open chat, running first. Reads the host
12
+ * session list's lineage (parentId + origin), so no extra subscription beyond
13
+ * the list the surface already follows.
14
+ */
15
+ export declare function collectSubagents(byId: Readonly<Record<string, SessionSummary>>, rootId: string | null, now?: number): readonly QaSubagentRow[];
16
+ export interface QaAgentsDrawerProps {
17
+ readonly agents: readonly QaSubagentRow[];
18
+ /** The subagent currently watched, if any. */
19
+ readonly activeId: string | null;
20
+ readonly onView: (id: string, title: string) => void;
21
+ readonly onClose: () => void;
22
+ }
23
+ /** Right-hand panel listing the chat's subagents. */
24
+ export declare function QaAgentsDrawer({ agents, activeId, onView, onClose, }: QaAgentsDrawerProps): import("react/jsx-runtime").JSX.Element;
25
+ //# sourceMappingURL=QaAgentsDrawer.d.ts.map
@@ -1,11 +1,21 @@
1
+ import type { QaImageDraft } from "../../types.js";
1
2
  export interface QaComposerProps {
2
3
  readonly placeholder: string;
4
+ readonly quickQuestions?: readonly string[];
3
5
  readonly canSend: boolean;
4
6
  readonly canStop: boolean;
5
7
  readonly running: boolean;
6
8
  readonly showStop: boolean;
7
- readonly onSend: (text: string) => Promise<boolean>;
9
+ readonly status: string | null;
10
+ readonly images: readonly QaImageDraft[];
11
+ readonly onImagesChange: (images: readonly QaImageDraft[]) => void;
12
+ readonly onSend: (text: string, images: readonly QaImageDraft[]) => Promise<boolean>;
8
13
  readonly onStop: () => Promise<void>;
9
14
  }
10
- export declare function QaComposer(props: QaComposerProps): import("react/jsx-runtime").JSX.Element;
15
+ /**
16
+ * Memoized with the default shallow compare: every prop is a scalar, a
17
+ * stable array or a stable callback, so a stream of transcript frames never
18
+ * re-renders the composer and typing stays responsive while an answer runs.
19
+ */
20
+ export declare const QaComposer: import("react").MemoExoticComponent<(props: QaComposerProps) => import("react/jsx-runtime").JSX.Element>;
11
21
  //# sourceMappingURL=QaComposer.d.ts.map
@@ -3,6 +3,19 @@ export interface QaMessageProps {
3
3
  readonly message: QaMessageModel;
4
4
  readonly renderMarkdown: boolean;
5
5
  readonly showTimestamp: boolean;
6
+ /** Storage prefix persisted message ratings live under. */
7
+ readonly stateKey?: string;
8
+ /** Ask for a fresh variant of this answer; omit to hide the control. */
9
+ readonly onRegenerate?: () => void;
10
+ /** Resolve one durable attachment into a viewable URL. */
11
+ readonly resolveImage?: (attachmentId: string) => Promise<string>;
6
12
  }
7
- export declare function QaMessage({ message, renderMarkdown, showTimestamp, }: QaMessageProps): import("react/jsx-runtime").JSX.Element;
13
+ /**
14
+ * Whether two transcript messages render identically. The transcript
15
+ * projection rebuilds every message object on each published frame, so the
16
+ * memoized row compares rendered content instead of references; string
17
+ * comparison is by value, which keeps fresh copies cheap to recognize.
18
+ */
19
+ export declare function sameMessage(a: QaMessageModel, b: QaMessageModel): boolean;
20
+ export declare const QaMessage: import("react").MemoExoticComponent<({ message, renderMarkdown, showTimestamp, stateKey, onRegenerate, resolveImage, }: QaMessageProps) => import("react/jsx-runtime").JSX.Element>;
8
21
  //# sourceMappingURL=QaMessage.d.ts.map
@@ -0,0 +1,41 @@
1
+ import type { SessionSummary } from "@deepseek-ai/dsh-client-runtime/client";
2
+ /** One renderable row of the chat-history sidebar. */
3
+ export interface QaChatRow {
4
+ readonly id: string;
5
+ readonly title: string;
6
+ readonly running: boolean;
7
+ readonly active: boolean;
8
+ readonly meta: string;
9
+ readonly updatedAt: number;
10
+ }
11
+ /**
12
+ * Project this browser's indexed chat ids onto the host session list, most
13
+ * recently updated first. Opening a chat is not an update: only the host's
14
+ * `updatedAt` (fresh messages) orders the list. Ids the host no longer lists
15
+ * are skipped (the controller prunes them on switch).
16
+ */
17
+ export declare function buildChatRows(chatIds: readonly string[], byId: Readonly<Record<string, SessionSummary>>, activeId: string | null, now?: number): readonly QaChatRow[];
18
+ export interface QaSidebarProps {
19
+ readonly rows: readonly QaChatRow[];
20
+ /** Deployment brand shown next to the logo (also while collapsed). */
21
+ readonly title: string;
22
+ /** Optional image logo; a built-in mark is drawn when null. */
23
+ readonly logoUrl: string | null;
24
+ /** Storage prefix used to remember the collapsed state across reloads. */
25
+ readonly stateKey: string;
26
+ readonly showNewChat: boolean;
27
+ readonly busy: boolean;
28
+ readonly onSwitch: (sessionId: string) => void;
29
+ readonly onNewChat: () => void;
30
+ /** Removes a chat from this browser's index; omit to hide the control. */
31
+ readonly onDelete?: (sessionId: string) => void;
32
+ }
33
+ /**
34
+ * Whether two sidebar row lists show the same thing. Rows are rebuilt on
35
+ * every surface render (relative timestamps drift), so the memoized sidebar
36
+ * compares by content and skips frames that only move the transcript.
37
+ */
38
+ export declare function sameChatRows(a: readonly QaChatRow[], b: readonly QaChatRow[]): boolean;
39
+ /** The per-browser chat history shown beside the QA conversation. */
40
+ export declare const QaSidebar: import("react").MemoExoticComponent<(props: QaSidebarProps) => import("react/jsx-runtime").JSX.Element>;
41
+ //# sourceMappingURL=QaSidebar.d.ts.map
@@ -0,0 +1,8 @@
1
+ import type { QaSource } from "../../types.js";
2
+ export interface QaSourcesDrawerProps {
3
+ readonly sources: readonly QaSource[];
4
+ readonly onClose: () => void;
5
+ }
6
+ /** Right-hand panel listing tool-derived sources with a detail pane. */
7
+ export declare function QaSourcesDrawer({ sources, onClose }: QaSourcesDrawerProps): import("react/jsx-runtime").JSX.Element;
8
+ //# sourceMappingURL=QaSourcesDrawer.d.ts.map
@@ -0,0 +1,19 @@
1
+ import type { QaWorkItem } from "../../types.js";
2
+ export interface QaWorkGroupProps {
3
+ readonly status: "running" | "complete";
4
+ readonly startedAt?: number;
5
+ readonly endedAt?: number;
6
+ readonly items: readonly QaWorkItem[];
7
+ readonly renderMarkdown: boolean;
8
+ }
9
+ export declare const THINKING_PHRASES: readonly string[];
10
+ /**
11
+ * Whether two work items render identically. The transcript projection
12
+ * rebuilds every item object on each published frame, so memoized work rows
13
+ * compare by rendered content instead of by reference.
14
+ */
15
+ export declare function sameWorkItem(a: QaWorkItem, b: QaWorkItem): boolean;
16
+ /** Whether two work item lists render identically, item for item. */
17
+ export declare function sameWorkItems(a: readonly QaWorkItem[], b: readonly QaWorkItem[]): boolean;
18
+ export declare const QaWorkGroup: import("react").MemoExoticComponent<({ status, startedAt, endedAt, items, renderMarkdown, }: QaWorkGroupProps) => import("react/jsx-runtime").JSX.Element>;
19
+ //# sourceMappingURL=QaWorkGroup.d.ts.map
@@ -0,0 +1,19 @@
1
+ import type { QaMessage } from "../../types.js";
2
+ export interface QaVariantGroup {
3
+ /** The user message that anchors the group. */
4
+ readonly groupId: string;
5
+ /** Answer turns triggered by that message, in order. */
6
+ readonly turns: readonly number[];
7
+ }
8
+ /**
9
+ * Group answer turns under the user message that triggered them. The session
10
+ * has no truncation seam, so a regenerated answer is a real follow-up turn;
11
+ * consecutive turns after one user message read as its variants.
12
+ */
13
+ export declare function collectVariantGroups(messages: readonly QaMessage[]): readonly QaVariantGroup[];
14
+ export declare function VariantSwitcher({ count, offset, onStep, }: {
15
+ readonly count: number;
16
+ readonly offset: number;
17
+ readonly onStep: (offset: number) => void;
18
+ }): import("react/jsx-runtime").JSX.Element;
19
+ //# sourceMappingURL=VariantSwitcher.d.ts.map
@@ -0,0 +1,9 @@
1
+ /** Shared time formatting for the QA surface UI. */
2
+ /** `только что`, `5 мин`, `3 ч`, `2 дн` or a ru-RU date past the week. */
3
+ export declare function relativeTime(timestamp: number, now: number): string;
4
+ /** `9 сент 15:44` - compact ru-RU day/month plus time. */
5
+ export declare function formatDayTime(timestamp: number): string;
6
+ /** `1,1 с` below ten seconds, `8 с` above. */
7
+ export declare function formatSeconds(ms: number): string;
8
+ export declare function formatWorkDuration(durationMs: number): string;
9
+ //# sourceMappingURL=format.d.ts.map
@@ -1,17 +1,13 @@
1
1
  import type { Context } from "@deepseek-ai/cordis";
2
2
  import type { ConnectionHandle } from "@deepseek-ai/dsh-client-connection/client";
3
- import type { ISessions } from "@deepseek-ai/dsh-client-runtime/client";
4
- import type { SettingsScopeBinder } from "@deepseek-ai/dsh-client-ui-settings/client";
5
3
  declare module "@deepseek-ai/cordis" {
6
4
  interface Context {
7
5
  connection: ConnectionHandle;
8
- sessions: ISessions;
9
- settingsScope: SettingsScopeBinder;
10
6
  }
11
7
  }
12
8
  export declare const inject: string[];
13
9
  /** Register the route-aware, full-frame QA entry in the additive overlay slot. */
14
- export declare function apply(ctx: Context): void;
10
+ export declare function apply(ctx: Context): Promise<() => Promise<void>>;
15
11
  export { QaConfigController } from "./QaConfigController.js";
16
12
  export { QaRouteController, matchesQaRoute } from "./QaRouteController.js";
17
13
  export { QaSessionController } from "./QaSessionController.js";
@@ -0,0 +1,29 @@
1
+ import type { IApiClient } from "@deepseek-ai/dsh-client-connection/client";
2
+ import type { ISessions, SessionRuntime } from "@deepseek-ai/dsh-client-runtime/client";
3
+ import type { QaLockdownProof, QaSessionState } from "../types.js";
4
+ import type { SessionFace } from "@deepseek-ai/dsh-client-runtime/client";
5
+ /** Wire content one prompt carries: text plus base64 image uploads. */
6
+ export type QaPromptContent = Parameters<SessionFace["prompt"]>[0];
7
+ /** DSH session runtime surface the QA controller is allowed to touch. */
8
+ export type QaSessions = ISessions & Pick<SessionRuntime, "create">;
9
+ /** Narrow client API slice used to pin the preset and model of a fresh chat. */
10
+ export type QaSessionsApi = Pick<IApiClient["sessions"], "selectModel"> & {
11
+ readonly selectAgentPreset: IApiClient["agentPresets"]["select"];
12
+ };
13
+ /** Attestation channel to the Host admission boundary (`qaSurface/secureSession`). */
14
+ export type QaSecureSession = (sessionId: string) => Promise<{
15
+ readonly ok: true;
16
+ readonly value: QaLockdownProof;
17
+ } | {
18
+ readonly ok: false;
19
+ readonly error: unknown;
20
+ }>;
21
+ /** Minimal persistence contract; backed by `window.localStorage` in the app. */
22
+ export interface StorageLike {
23
+ getItem(key: string): string | null;
24
+ setItem(key: string, value: string): void;
25
+ removeItem(key: string): void;
26
+ }
27
+ /** The state projected while no QA chat is bound (route inactive or booting). */
28
+ export declare const QA_SESSION_IDLE_STATE: QaSessionState;
29
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Resolve with the first snapshot of an observable source that satisfies
3
+ * `predicate`, or reject after `timeoutMs`. This is the DSH-runtime glue the
4
+ * QA surface needs instead of ad-hoc sleeps; the source only has to expose
5
+ * the getSnapshot/subscribe store pair used across the client bundle.
6
+ */
7
+ export declare function waitFor<T>(source: {
8
+ getSnapshot(): T;
9
+ subscribe(listener: () => void): () => void;
10
+ }, predicate: (value: T) => boolean, timeoutMs: number): Promise<T>;
11
+ //# sourceMappingURL=wait-for.d.ts.map
@@ -1,7 +1,8 @@
1
- import { Service, type Context } from "@deepseek-ai/cordis";
2
- import type { QaSurfaceConfig, ResolvedQaSurfaceConfig } from "./types.js";
1
+ import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
2
+ import type { Context } from "@deepseek-ai/cordis";
3
+ import type { QaLockdownProof, QaSurfaceConfig, ResolvedQaSurfaceConfig } from "./types.js";
3
4
  export declare const name = "qa-surface";
4
- export declare const inject: readonly string[];
5
+ export declare const inject: string[];
5
6
  export declare const QA_SURFACE_SETTINGS_NAMESPACE: import("@deepseek-ai/dsh-settings").SettingsNamespace;
6
7
  export declare const Config: import("@deepseek-ai/schemastery").default<QaSurfaceConfig>;
7
8
  declare module "@deepseek-ai/cordis" {
@@ -9,21 +10,36 @@ declare module "@deepseek-ai/cordis" {
9
10
  qaSurface: QaSurface;
10
11
  }
11
12
  }
12
- /** Small Host companion: validates config and exposes it through DSH settings. */
13
- export declare class QaSurface extends Service {
14
- static inject: readonly string[];
13
+ /** Host companion: validates config, owns the admission boundary and the route. */
14
+ export declare class QaSurface extends TypertRemoteService {
15
+ static inject: string[];
15
16
  static Config: import("@deepseek-ai/schemastery").default<QaSurfaceConfig>;
16
17
  private source;
17
18
  private readonly logger;
19
+ private readonly admission;
18
20
  private webServer;
19
21
  private disposeRoute;
20
22
  private routeKey;
21
23
  constructor(ctx: Context, entry?: QaSurfaceConfig);
22
24
  getConfig(): ResolvedQaSurfaceConfig;
25
+ /**
26
+ * Serve the effective QA configuration to the browser. The DSH gateway pins
27
+ * settings RPCs to loopback, so a browser served over the LAN always sees
28
+ * the settings namespace as unavailable; this method is the config channel
29
+ * such a browser falls back to. It is read-only projection — the admission
30
+ * boundary stays in {@link secureSession}.
31
+ */
32
+ describe(): ResolvedQaSurfaceConfig;
33
+ /** Pin and attest the effective policy. The browser supplies identity only. */
34
+ secureSession(sessionId: string): QaLockdownProof;
23
35
  private refreshRoute;
24
36
  }
25
37
  export { ConfigSchema, DEFAULT_QA_SURFACE_CONFIG, normalizeRoutePath, resolveConfig, } from "./config.js";
38
+ export { QaAttestationError } from "./attestation.js";
39
+ export type { QaAttestationReason } from "./attestation.js";
26
40
  export { registerQaNavigationRoute } from "./host-route.js";
41
+ export { qaToolDenial, qaToolPolicyPlan } from "./lockdown-policy.js";
42
+ export { QaPolicyAdmission } from "./secure-session.js";
27
43
  export type * from "./types.js";
28
44
  export default QaSurface;
29
45
  //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,18 @@
1
+ export interface QaToolPolicyPlan {
2
+ /** Exact names that must remain visible after the scoped restriction. */
3
+ readonly allow: readonly string[];
4
+ /** Configured names absent from the agent's composed tool view. */
5
+ readonly unknown: readonly string[];
6
+ }
7
+ /**
8
+ * Validate an allow-list against the complete agent-scoped view.
9
+ *
10
+ * Preset tools live on an ancestor scope, not necessarily in the global tool
11
+ * layer. The restriction must therefore retain the configured names verbatim;
12
+ * deriving it from the global view would hide every preset-owned tool after a
13
+ * successful first attestation and make the next attestation fail.
14
+ */
15
+ export declare function qaToolPolicyPlan(allow: readonly string[], isMounted: (toolName: string) => boolean): QaToolPolicyPlan;
16
+ /** Monotonic execution decision shared by inherited, scoped and transport tools. */
17
+ export declare function qaToolDenial(allowed: ReadonlySet<string>, toolName: string): string | undefined;
18
+ //# sourceMappingURL=lockdown-policy.d.ts.map
@@ -0,0 +1,22 @@
1
+ import type { Context } from "@deepseek-ai/cordis";
2
+ import type { PluginLogger } from "@yadsh/dsh-plugin-log";
3
+ import type { QaLockdownProof, ResolvedQaSurfaceConfig } from "./types.js";
4
+ /**
5
+ * Host-side QA policy admission boundary — the only browser-callable operation
6
+ * that turns a session into a locked QA session. It verifies the immutable
7
+ * session composition against the effective deployment config, refuses to
8
+ * adopt a non-QA session with user history, validates the named permission
9
+ * preset, pins the scoped tool restriction plus the monotonic execution guard,
10
+ * and returns a sanitized proof. Every refusal carries only a coarse reason.
11
+ */
12
+ export declare class QaPolicyAdmission {
13
+ private readonly ctx;
14
+ private readonly config;
15
+ private readonly logger;
16
+ private readonly appliedPolicies;
17
+ constructor(ctx: Context, config: () => ResolvedQaSurfaceConfig, logger: PluginLogger);
18
+ secureSession(sessionId: string): QaLockdownProof;
19
+ /** Detach every pinned tool policy; wired as a disposal effect by the entry. */
20
+ dispose(): void;
21
+ }
22
+ //# sourceMappingURL=secure-session.d.ts.map