@powerhousedao/reactor-browser 6.2.2-dev.8 → 6.2.2-dev.81

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 (74) hide show
  1. package/README.md +341 -0
  2. package/dist/attachment-service-LwpgZ358.d.ts +1057 -0
  3. package/dist/attachment-service-LwpgZ358.d.ts.map +1 -0
  4. package/dist/{client-u13cr-Vg.js → client-B8ezRmBB.js} +30 -3
  5. package/dist/client-B8ezRmBB.js.map +1 -0
  6. package/dist/client-BIHN_UyR.d.ts +14 -0
  7. package/dist/client-BIHN_UyR.d.ts.map +1 -0
  8. package/dist/{client-_Gh2Uf0E.js → client-D66jVnot.js} +79 -13
  9. package/dist/client-D66jVnot.js.map +1 -0
  10. package/dist/{document-by-id-dLYFX-xz.js → document-by-id-BSZqTN66.js} +81 -51
  11. package/dist/document-by-id-BSZqTN66.js.map +1 -0
  12. package/dist/document-model-modules-DMIR-uSD.js +411 -0
  13. package/dist/document-model-modules-DMIR-uSD.js.map +1 -0
  14. package/dist/document-operations-BO-SlTDw.js +2539 -0
  15. package/dist/document-operations-BO-SlTDw.js.map +1 -0
  16. package/dist/global-1rvI8VdA.d.ts +400 -0
  17. package/dist/global-1rvI8VdA.d.ts.map +1 -0
  18. package/dist/index-BE3wBHLP.d.ts +349 -0
  19. package/dist/index-BE3wBHLP.d.ts.map +1 -0
  20. package/dist/index-BIVDXw3K.d.ts +144 -0
  21. package/dist/index-BIVDXw3K.d.ts.map +1 -0
  22. package/dist/index.d.ts +313 -715
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +611 -1696
  25. package/dist/index.js.map +1 -1
  26. package/dist/{inspector-proxy-CvQDDxmT.d.ts → inspector-proxy-6h5VKAwt.d.ts} +4 -2
  27. package/dist/inspector-proxy-6h5VKAwt.d.ts.map +1 -0
  28. package/dist/{make-ph-event-functions-BHoLPif5.js → make-ph-event-functions-DBq3iWYn.js} +1 -1
  29. package/dist/{make-ph-event-functions-BHoLPif5.js.map → make-ph-event-functions-DBq3iWYn.js.map} +1 -1
  30. package/dist/{index-DS4W07Y8.d.ts → pglite-CntadC_p-9NDG14dS.d.ts} +2 -142
  31. package/dist/pglite-CntadC_p-9NDG14dS.d.ts.map +1 -0
  32. package/dist/{relational-Dj06-YkI.js → relational-D4sxGPz7.js} +2 -2
  33. package/dist/{relational-Dj06-YkI.js.map → relational-D4sxGPz7.js.map} +1 -1
  34. package/dist/renown-BHxwP7xU.js +97 -0
  35. package/dist/renown-BHxwP7xU.js.map +1 -0
  36. package/dist/{renown-D-1c_Bvx.js → renown-BIEv6bSI.js} +523 -57
  37. package/dist/renown-BIEv6bSI.js.map +1 -0
  38. package/dist/selected-document-xFBNPc9Q.js +42 -0
  39. package/dist/selected-document-xFBNPc9Q.js.map +1 -0
  40. package/dist/src/ai/index.d.ts +369 -0
  41. package/dist/src/ai/index.d.ts.map +1 -0
  42. package/dist/src/ai/index.js +1296 -0
  43. package/dist/src/ai/index.js.map +1 -0
  44. package/dist/src/analytics/index.d.ts +1 -1
  45. package/dist/src/analytics/index.js +2 -1
  46. package/dist/src/analytics/index.js.map +1 -1
  47. package/dist/src/graphql/client.d.ts +3 -2
  48. package/dist/src/graphql/client.js +1 -1
  49. package/dist/src/graphql-client/entry.d.ts +4 -0
  50. package/dist/src/graphql-client/entry.js +4 -0
  51. package/dist/src/relational/index.d.ts +1 -1
  52. package/dist/src/relational/index.js +1 -1
  53. package/dist/src/renown/index.d.ts +2 -2
  54. package/dist/src/renown/index.js +3 -2
  55. package/dist/src/rpc/index.d.ts +6 -3
  56. package/dist/src/rpc/index.d.ts.map +1 -1
  57. package/dist/src/rpc/index.js +41 -10
  58. package/dist/src/rpc/index.js.map +1 -1
  59. package/dist/store-BJYcr_FL.js +37 -0
  60. package/dist/store-BJYcr_FL.js.map +1 -0
  61. package/dist/{types-CPV0Cnsy.d.ts → types-6vDzbRRS.d.ts} +1 -1
  62. package/dist/{types-CPV0Cnsy.d.ts.map → types-6vDzbRRS.d.ts.map} +1 -1
  63. package/dist/{client-09xv0Dq6.d.ts → types-Dcv1ZLoW.d.ts} +87 -19
  64. package/dist/types-Dcv1ZLoW.d.ts.map +1 -0
  65. package/package.json +26 -10
  66. package/dist/client-09xv0Dq6.d.ts.map +0 -1
  67. package/dist/client-_Gh2Uf0E.js.map +0 -1
  68. package/dist/client-u13cr-Vg.js.map +0 -1
  69. package/dist/document-by-id-dLYFX-xz.js.map +0 -1
  70. package/dist/index-D3bV5rcl.d.ts +0 -216
  71. package/dist/index-D3bV5rcl.d.ts.map +0 -1
  72. package/dist/index-DS4W07Y8.d.ts.map +0 -1
  73. package/dist/inspector-proxy-CvQDDxmT.d.ts.map +0 -1
  74. package/dist/renown-D-1c_Bvx.js.map +0 -1
@@ -0,0 +1,1296 @@
1
+ import { H as ambientRenownTokenProvider, S as useSelectedDriveSafe, s as useSelectedNode } from "../../document-model-modules-DMIR-uSD.js";
2
+ import { n as useSelectedDocumentId } from "../../selected-document-xFBNPc9Q.js";
3
+ import { isFileNode, isFolderNode } from "@powerhousedao/shared/document-drive";
4
+ import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
5
+ import { DriveCollectionId } from "@powerhousedao/reactor";
6
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
+ import { z } from "zod";
8
+ import { isStepCount, streamText, tool } from "ai";
9
+ import { toResponseMessages } from "ai/internal";
10
+ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
11
+ import { ArrowUp, Check, Maximize2, MessageCircle, Minimize2, Square, Trash2, X } from "lucide-react";
12
+ import ReactMarkdown from "react-markdown";
13
+ import remarkGfm from "remark-gfm";
14
+ //#region src/ai/types.ts
15
+ /** Tool names whose execution mutates the reactor and requires approval. */
16
+ const WRITE_TOOLS = new Set([
17
+ "createDocument",
18
+ "addActions",
19
+ "deleteDocument",
20
+ "addDrive",
21
+ "deleteDrive",
22
+ "addRemoteDrive"
23
+ ]);
24
+ /**
25
+ * Whether executing the tool mutates state and requires user approval:
26
+ * the built-in write tools, or any tool flagged destructive in its
27
+ * annotations (covers package-provided tools outside the built-in set).
28
+ */
29
+ function isWriteTool(name, annotations) {
30
+ return WRITE_TOOLS.has(name) || annotations?.destructiveHint === true;
31
+ }
32
+ //#endregion
33
+ //#region src/ai/agent.ts
34
+ /** Hard cap on model round-trips per user message (each tool step counts). */
35
+ const MAX_AGENT_STEPS = 10;
36
+ /**
37
+ * Hard cap on the characters of one tool result fed back to the model.
38
+ *
39
+ * Large results (whole document states, catalog listings, schema
40
+ * summaries) accumulate across steps because every step resends the whole
41
+ * conversation; without a budget a few broad queries exhaust the model's
42
+ * context and stall the turn. Over-budget results are truncated with a
43
+ * marker so the model re-queries more narrowly.
44
+ */
45
+ const MAX_TOOL_RESULT_CHARS = 12e3;
46
+ const TRUNCATION_NOTE = "\n...[truncated: this tool result exceeded the context budget; re-query with a more specific filter or fewer items]";
47
+ function boundedToolResult(value) {
48
+ const text = resultText(value);
49
+ if (text.length <= 12e3) return value;
50
+ return text.slice(0, MAX_TOOL_RESULT_CHARS) + TRUNCATION_NOTE;
51
+ }
52
+ function resultText(value) {
53
+ if (typeof value === "string") return value;
54
+ const json = JSON.stringify(value);
55
+ return typeof json === "string" ? json : "undefined";
56
+ }
57
+ /**
58
+ * Number of the most recent turns whose tool results are kept in full in
59
+ * the committed history. Tool results are re-derivable (the store is the
60
+ * source of truth), so results older than that window are replaced with
61
+ * one-line stubs: this is what keeps a long conversation from exhausting
62
+ * the model's context.
63
+ */
64
+ const FULL_RESULT_TURNS = 2;
65
+ /** Tool results smaller than this are cheap to keep and are never stubbed. */
66
+ const STUB_MIN_CHARS = 500;
67
+ function toolResultChars(output) {
68
+ const part = output;
69
+ if (part.type === "text" && typeof part.value === "string") return part.value.length;
70
+ const target = part.value !== void 0 ? part.value : output;
71
+ const json = JSON.stringify(target);
72
+ return typeof json === "string" ? json.length : 0;
73
+ }
74
+ function argDigest(input) {
75
+ if (input === void 0) return "";
76
+ let text;
77
+ try {
78
+ text = JSON.stringify(input);
79
+ } catch {
80
+ text = "";
81
+ }
82
+ if (text.length <= 80) return text;
83
+ return `${text.slice(0, 77)}…`;
84
+ }
85
+ /**
86
+ * Replaces tool results older than the last {@link FULL_RESULT_TURNS}
87
+ * turns with one-line stubs naming the tool and its arguments. User and
88
+ * assistant messages are never touched, and tool calls keep their input,
89
+ * so the model can re-query any stubbed result with the same arguments.
90
+ * Idempotent: stubs are below {@link STUB_MIN_CHARS} and pass through.
91
+ */
92
+ function stubStaleToolResults(history) {
93
+ let users = 0;
94
+ let windowStart = 0;
95
+ for (let i = history.length - 1; i >= 0; i--) if (history[i].role === "user") {
96
+ users += 1;
97
+ if (users === 2) {
98
+ windowStart = i;
99
+ break;
100
+ }
101
+ }
102
+ if (users < 2) return history;
103
+ const callInputs = /* @__PURE__ */ new Map();
104
+ for (const m of history) if (m.role === "assistant" && Array.isArray(m.content)) {
105
+ for (const part of m.content) if (part.type === "tool-call") callInputs.set(part.toolCallId, part.input);
106
+ }
107
+ return history.map((m, i) => {
108
+ if (i >= windowStart || m.role !== "tool") return m;
109
+ const content = m.content.map((part) => {
110
+ if (part.type !== "tool-result") return part;
111
+ const chars = toolResultChars(part.output);
112
+ if (chars < 500) return part;
113
+ const stub = {
114
+ type: "text",
115
+ value: `[result omitted: ${part.toolName} ${argDigest(callInputs.get(part.toolCallId))} — ${chars} chars; re-query the tool to refetch]`
116
+ };
117
+ return {
118
+ ...part,
119
+ output: stub
120
+ };
121
+ });
122
+ if (!content.some((part, index) => part !== m.content[index])) return m;
123
+ return {
124
+ ...m,
125
+ content
126
+ };
127
+ });
128
+ }
129
+ /**
130
+ * Creates the chat language model for a user-supplied OpenAI-compatible
131
+ * endpoint. Requests go directly from the browser to the endpoint; the API
132
+ * key is never sent to any Powerhouse server.
133
+ */
134
+ /**
135
+ * Resolves the configured base URL. Relative paths (e.g. `/v1`) resolve
136
+ * against the page origin, so single-origin deployments can serve the
137
+ * endpoint behind the same reverse proxy without CORS.
138
+ */
139
+ function resolveBaseUrl(raw) {
140
+ const base = raw.trim().replace(/\/+$/, "");
141
+ if (/^https?:\/\//i.test(base)) return base;
142
+ if (typeof window === "undefined") return base;
143
+ return `${window.location.origin}${base.startsWith("/") ? "" : "/"}${base}`;
144
+ }
145
+ function createReactorChatModel(settings) {
146
+ return createOpenAICompatible({
147
+ name: "reactor-chat",
148
+ baseURL: resolveBaseUrl(settings.baseUrl),
149
+ apiKey: settings.apiKey
150
+ }).chatModel(settings.model.trim());
151
+ }
152
+ /**
153
+ * Unwraps the MCP `CallToolResult` envelope produced by the reactor tool
154
+ * core into a plain value for the AI SDK. Throws for error results so the
155
+ * SDK surfaces them as tool errors in the next model step.
156
+ */
157
+ function unwrapToolResult(raw) {
158
+ if (raw && typeof raw === "object") {
159
+ const envelope = raw;
160
+ if (typeof envelope.isError === "boolean" || Array.isArray(envelope.content)) {
161
+ if (envelope.isError) {
162
+ const text = envelope.content?.find((c) => c.type === "text")?.text ?? "Unknown tool error";
163
+ throw new Error(text.replace(/^Error:\s*/, ""));
164
+ }
165
+ if (envelope.structuredContent !== void 0) return envelope.structuredContent;
166
+ const textPart = envelope.content?.find((c) => c.type === "text");
167
+ if (textPart?.text !== void 0) try {
168
+ return JSON.parse(textPart.text);
169
+ } catch {
170
+ return textPart.text;
171
+ }
172
+ return null;
173
+ }
174
+ }
175
+ return raw;
176
+ }
177
+ /** Builds the system prompt, grounding the agent in the current selection. */
178
+ function buildSystemPrompt(context) {
179
+ const lines = [
180
+ "You are an assistant embedded in the Powerhouse Connect drive explorer.",
181
+ "You operate on a local-first document store (the reactor) exclusively through the provided tools.",
182
+ "Documents are instances of typed document models; drives are collections that group documents and folders.",
183
+ "Prefer read-only tools to discover state (document models, drives, documents, relationships) before making changes.",
184
+ "Never invent document ids, drive ids, folder ids or document model types — discover them with the read tools first.",
185
+ "Never accept secret values (passwords, tokens, API keys) in chat. When a connection or configuration requires a secret, tell the user to enter it in the relevant editor (e.g. the connection editor) and point them to the document. Never ask the user to paste a secret into the chat.",
186
+ "When the user refers to 'this', 'here' or 'it' without naming a target, they mean the current selection below; prefer it for create/modify targets.",
187
+ "Tool results may be truncated when they are large: if you see a truncation marker, narrow the query (more specific filter, fewer items) instead of retrying the same call.",
188
+ "Older tool results in this conversation may be replaced by a short '[result omitted: ...]' stub as the context grows; that is normal housekeeping, not an error. If you still need the data, call the same tool again with the same arguments."
189
+ ];
190
+ const selection = [];
191
+ if (context.driveName) selection.push(`The current drive is "${context.driveName}"` + (context.driveId ? ` (id: ${context.driveId})` : "") + ".");
192
+ if (context.nodeKind === "folder" && context.nodeName) selection.push(`The current folder is "${context.nodeName}".`);
193
+ if (context.documentType) selection.push(`The current document is "${context.documentName ?? "unnamed"}" of type "${context.documentType}"` + (context.documentId ? ` (id: ${context.documentId})` : "") + ".");
194
+ if (context.switchboardUrl) selection.push(`The switchboard for this drive is at ${context.switchboardUrl}; its GraphQL endpoint is ${context.switchboardGraphqlUrl ?? ""}. Use the getSwitchboardSchema tool to list the queries and mutations it exposes.`);
195
+ else if (context.driveId) selection.push("This drive is not synced to a switchboard, so no switchboard endpoints are available for it.");
196
+ if (selection.length > 0) lines.push("Current selection:", ...selection.map((s) => `- ${s}`));
197
+ else lines.push("Nothing is currently selected. When the target of an action is ambiguous, ask the user which drive, folder or document it should apply to.");
198
+ return lines.join("\n");
199
+ }
200
+ /**
201
+ * Runs one tool-loop conversation turn against an OpenAI-compatible model.
202
+ *
203
+ * The agent keeps the model message history across turns. Write tools are
204
+ * gated by the AI SDK's tool approval: when auto-approval is off, the
205
+ * pending user decision is bridged through {@link approve}/{@link reject}.
206
+ */
207
+ var ReactorChatAgent = class {
208
+ history;
209
+ approvals = /* @__PURE__ */ new Map();
210
+ constructor(options) {
211
+ this.options = options;
212
+ this.history = options.history ?? [];
213
+ }
214
+ /** The model history after the last completed turn, for the next agent. */
215
+ getHistory() {
216
+ return this.history;
217
+ }
218
+ /** Clears the conversation history (a fresh chat). */
219
+ reset() {
220
+ this.history = [];
221
+ this.cancelPendingApprovals();
222
+ }
223
+ /** Approves a pending write tool call, resuming the agent loop. */
224
+ approve(toolCallId) {
225
+ this.resolveApproval(toolCallId, true);
226
+ }
227
+ /** Rejects a pending write tool call; the rejection is fed to the model. */
228
+ reject(toolCallId) {
229
+ this.resolveApproval(toolCallId, false);
230
+ }
231
+ resolveApproval(toolCallId, approved) {
232
+ const resolve = this.approvals.get(toolCallId);
233
+ if (resolve) {
234
+ this.approvals.delete(toolCallId);
235
+ resolve(approved);
236
+ }
237
+ }
238
+ cancelPendingApprovals() {
239
+ for (const resolve of this.approvals.values()) resolve(false);
240
+ this.approvals.clear();
241
+ }
242
+ /** Runs one user turn: appends the message, streams the model, commits history. */
243
+ async send(text) {
244
+ const { onEvent, signal, settings } = this.options;
245
+ const userMessage = {
246
+ role: "user",
247
+ content: text
248
+ };
249
+ const messages = [...this.history, userMessage];
250
+ const tools = {};
251
+ for (const descriptor of this.options.tools) tools[descriptor.name] = tool({
252
+ description: descriptor.description,
253
+ inputSchema: z.object(descriptor.inputSchema),
254
+ execute: async (args) => boundedToolResult(unwrapToolResult(await descriptor.callback(args)))
255
+ });
256
+ const model = this.options.model ?? createReactorChatModel(settings);
257
+ const turnMessages = [];
258
+ let lastInputTokens;
259
+ const result = streamText({
260
+ model,
261
+ system: buildSystemPrompt(this.options.context),
262
+ messages,
263
+ tools,
264
+ stopWhen: isStepCount(10),
265
+ abortSignal: signal,
266
+ onStepEnd: async (step) => {
267
+ turnMessages.push(...await toResponseMessages({
268
+ content: step.content,
269
+ tools
270
+ }));
271
+ if (typeof step.usage.inputTokens === "number") lastInputTokens = step.usage.inputTokens;
272
+ },
273
+ toolApproval: async ({ toolCall }) => {
274
+ const descriptor = this.options.tools.find((t) => t.name === toolCall.toolName);
275
+ if (settings.autoApproveWrites || !isWriteTool(toolCall.toolName, descriptor?.annotations)) return "not-applicable";
276
+ const toolCallId = toolCall.toolCallId;
277
+ onEvent({
278
+ type: "approval-request",
279
+ toolCallId,
280
+ name: toolCall.toolName,
281
+ args: toolCall.input
282
+ });
283
+ const approved = await new Promise((resolve) => {
284
+ this.approvals.set(toolCallId, resolve);
285
+ });
286
+ onEvent({
287
+ type: "approval-resolved",
288
+ toolCallId,
289
+ approved
290
+ });
291
+ return approved ? "approved" : {
292
+ type: "denied",
293
+ reason: "The user denied this action. Do not retry it; acknowledge the denial and ask how to proceed."
294
+ };
295
+ }
296
+ });
297
+ for await (const part of result.fullStream) this.handleStreamPart(part, onEvent);
298
+ if (lastInputTokens !== void 0) onEvent({
299
+ type: "usage",
300
+ inputTokens: lastInputTokens
301
+ });
302
+ this.history = stubStaleToolResults([
303
+ ...this.history,
304
+ userMessage,
305
+ ...this.dropUnresolvedToolCalls(turnMessages)
306
+ ]);
307
+ onEvent({ type: "finish" });
308
+ }
309
+ /**
310
+ * Removes tool calls that were never executed (step cap or abort):
311
+ * OpenAI-compatible APIs reject a tool call without its tool result.
312
+ */
313
+ dropUnresolvedToolCalls(messages) {
314
+ const resolved = /* @__PURE__ */ new Set();
315
+ for (const m of messages) if (m.role === "tool") {
316
+ for (const part of m.content) if (part.type === "tool-result") resolved.add(part.toolCallId);
317
+ }
318
+ const out = [...messages];
319
+ if (out.length === 0) return out;
320
+ const last = out[out.length - 1];
321
+ if (last.role !== "assistant" || typeof last.content === "string") return out;
322
+ const content = last.content.filter((part) => part.type !== "tool-call" || resolved.has(part.toolCallId));
323
+ if (content.length === 0) out.pop();
324
+ else out[out.length - 1] = {
325
+ ...last,
326
+ content
327
+ };
328
+ return out;
329
+ }
330
+ handleStreamPart(part, onEvent) {
331
+ const p = part;
332
+ switch (p.type) {
333
+ case "text-delta": {
334
+ const { text } = p;
335
+ onEvent({
336
+ type: "text-delta",
337
+ delta: text
338
+ });
339
+ break;
340
+ }
341
+ case "tool-call": {
342
+ const { toolCallId, toolName, input } = p;
343
+ onEvent({
344
+ type: "tool-start",
345
+ toolCallId,
346
+ name: toolName,
347
+ args: input
348
+ });
349
+ break;
350
+ }
351
+ case "tool-result": {
352
+ const { toolCallId, toolName, output } = p;
353
+ onEvent({
354
+ type: "tool-result",
355
+ toolCallId,
356
+ name: toolName,
357
+ state: "done",
358
+ result: output
359
+ });
360
+ break;
361
+ }
362
+ case "tool-error": {
363
+ const { toolCallId, toolName, error } = p;
364
+ onEvent({
365
+ type: "tool-result",
366
+ toolCallId,
367
+ name: toolName,
368
+ state: "error",
369
+ error: error instanceof Error ? error.message : String(error)
370
+ });
371
+ break;
372
+ }
373
+ case "tool-approval-response": {
374
+ const { approved, toolCall } = p;
375
+ if (!approved) onEvent({
376
+ type: "tool-result",
377
+ toolCallId: toolCall.toolCallId,
378
+ name: toolCall.toolName,
379
+ state: "rejected"
380
+ });
381
+ break;
382
+ }
383
+ case "tool-output-denied": {
384
+ const { toolCallId, toolName } = p;
385
+ onEvent({
386
+ type: "tool-result",
387
+ toolCallId,
388
+ name: toolName,
389
+ state: "rejected"
390
+ });
391
+ break;
392
+ }
393
+ case "abort": break;
394
+ case "error": {
395
+ const { error } = p;
396
+ onEvent({
397
+ type: "error",
398
+ error: error instanceof Error ? error.message : String(error)
399
+ });
400
+ break;
401
+ }
402
+ default: break;
403
+ }
404
+ }
405
+ };
406
+ //#endregion
407
+ //#region src/ai/components/approval-card.tsx
408
+ function formatArgs$1(args) {
409
+ let json;
410
+ try {
411
+ json = JSON.stringify(args, null, 2);
412
+ } catch {
413
+ return String(args);
414
+ }
415
+ return json.length > 400 ? `${json.slice(0, 400)}\n…` : json;
416
+ }
417
+ /**
418
+ * In-chat approval card for a write tool call. Approving or rejecting
419
+ * resolves the pending approval inside the agent loop.
420
+ */
421
+ function ApprovalCard({ approval, onApprove, onReject }) {
422
+ return /* @__PURE__ */ jsxs("div", {
423
+ className: "rounded-lg border border-border bg-muted/40 p-3",
424
+ children: [
425
+ /* @__PURE__ */ jsxs("p", {
426
+ className: "mb-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground",
427
+ children: ["Approval required — ", approval.name]
428
+ }),
429
+ /* @__PURE__ */ jsx("pre", {
430
+ className: "mb-3 max-h-40 overflow-auto rounded bg-background p-2 text-xs text-foreground",
431
+ children: formatArgs$1(approval.args)
432
+ }),
433
+ /* @__PURE__ */ jsxs("div", {
434
+ className: "flex gap-2",
435
+ children: [/* @__PURE__ */ jsxs("button", {
436
+ type: "button",
437
+ onClick: () => onApprove(approval.toolCallId),
438
+ className: "flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90",
439
+ children: [/* @__PURE__ */ jsx(Check, { size: 14 }), "Approve"]
440
+ }), /* @__PURE__ */ jsxs("button", {
441
+ type: "button",
442
+ onClick: () => onReject(approval.toolCallId),
443
+ className: "flex items-center gap-1.5 rounded-md border border-border bg-background px-3 py-1.5 text-sm font-medium text-foreground hover:bg-muted",
444
+ children: [/* @__PURE__ */ jsx(X, { size: 14 }), "Reject"]
445
+ })]
446
+ })
447
+ ]
448
+ });
449
+ }
450
+ //#endregion
451
+ //#region src/ai/components/markdown.tsx
452
+ /**
453
+ * Markdown renderer for assistant messages.
454
+ *
455
+ * Uses react-markdown, which builds React elements from the markdown AST and
456
+ * does not execute raw HTML — safe for model-generated content. remark-gfm
457
+ * adds the table/strikethrough/task-list syntax the agent emits. Element
458
+ * styling is scoped to the chat window's text-sm scale via Tailwind
459
+ * utilities, so no extra CSS is needed.
460
+ */
461
+ function Markdown({ text }) {
462
+ return /* @__PURE__ */ jsx(ReactMarkdown, {
463
+ remarkPlugins: [remarkGfm],
464
+ components: {
465
+ p: ({ children }) => /* @__PURE__ */ jsx("p", {
466
+ className: "my-1.5 leading-relaxed first:mt-0 last:mb-0",
467
+ children
468
+ }),
469
+ h1: ({ children }) => /* @__PURE__ */ jsx("h1", {
470
+ className: "mb-1 mt-3 font-semibold first:mt-0",
471
+ children
472
+ }),
473
+ h2: ({ children }) => /* @__PURE__ */ jsx("h2", {
474
+ className: "mb-1 mt-3 font-semibold first:mt-0",
475
+ children
476
+ }),
477
+ h3: ({ children }) => /* @__PURE__ */ jsx("h3", {
478
+ className: "mb-1 mt-2.5 font-semibold first:mt-0",
479
+ children
480
+ }),
481
+ ul: ({ children }) => /* @__PURE__ */ jsx("ul", {
482
+ className: "my-1.5 list-disc space-y-0.5 pl-5",
483
+ children
484
+ }),
485
+ ol: ({ children }) => /* @__PURE__ */ jsx("ol", {
486
+ className: "my-1.5 list-decimal space-y-0.5 pl-5",
487
+ children
488
+ }),
489
+ li: ({ children }) => /* @__PURE__ */ jsx("li", {
490
+ className: "leading-relaxed",
491
+ children
492
+ }),
493
+ blockquote: ({ children }) => /* @__PURE__ */ jsx("blockquote", {
494
+ className: "my-1.5 border-l-2 border-border pl-2.5 text-muted-foreground",
495
+ children
496
+ }),
497
+ hr: () => /* @__PURE__ */ jsx("hr", { className: "my-2 border-border" }),
498
+ a: ({ children, href }) => /* @__PURE__ */ jsx("a", {
499
+ href,
500
+ target: "_blank",
501
+ rel: "noreferrer noopener",
502
+ className: "text-primary underline",
503
+ children
504
+ }),
505
+ table: ({ children }) => /* @__PURE__ */ jsx("div", {
506
+ className: "my-1.5 overflow-x-auto",
507
+ children: /* @__PURE__ */ jsx("table", {
508
+ className: "w-full border-collapse text-xs",
509
+ children
510
+ })
511
+ }),
512
+ thead: ({ children }) => /* @__PURE__ */ jsx("thead", { children }),
513
+ th: ({ children }) => /* @__PURE__ */ jsx("th", {
514
+ className: "border border-border bg-muted px-2 py-1 text-left font-semibold",
515
+ children
516
+ }),
517
+ td: ({ children }) => /* @__PURE__ */ jsx("td", {
518
+ className: "border border-border px-2 py-1 align-top",
519
+ children
520
+ }),
521
+ pre: ({ children }) => /* @__PURE__ */ jsx("pre", {
522
+ className: "my-1.5 overflow-x-auto rounded-md bg-muted p-2.5 text-xs leading-relaxed",
523
+ children
524
+ }),
525
+ code: ({ children, className }) => {
526
+ if ((className ?? "").startsWith("language-")) return /* @__PURE__ */ jsx("code", {
527
+ className,
528
+ children
529
+ });
530
+ return /* @__PURE__ */ jsx("code", {
531
+ className: "rounded bg-muted px-1 py-0.5 text-[0.8125rem]",
532
+ children
533
+ });
534
+ }
535
+ },
536
+ children: text
537
+ });
538
+ }
539
+ //#endregion
540
+ //#region src/ai/components/chat-window.tsx
541
+ function formatArgs(args) {
542
+ let json;
543
+ try {
544
+ json = JSON.stringify(args);
545
+ } catch {
546
+ return String(args);
547
+ }
548
+ return json.length > 200 ? `${json.slice(0, 200)}…` : json;
549
+ }
550
+ function ToolCard({ part }) {
551
+ const [expanded, setExpanded] = useState(false);
552
+ return /* @__PURE__ */ jsxs("div", {
553
+ className: "rounded-lg border border-border bg-background p-2",
554
+ children: [/* @__PURE__ */ jsxs("button", {
555
+ type: "button",
556
+ onClick: () => setExpanded((e) => !e),
557
+ className: "flex w-full items-center gap-2 text-left text-xs text-foreground",
558
+ children: [
559
+ part.state === "done" && /* @__PURE__ */ jsx(Check, {
560
+ size: 13,
561
+ className: "shrink-0 text-info"
562
+ }),
563
+ part.state === "error" && /* @__PURE__ */ jsx(X, {
564
+ size: 13,
565
+ className: "shrink-0 text-destructive"
566
+ }),
567
+ part.state === "rejected" && /* @__PURE__ */ jsx(X, {
568
+ size: 13,
569
+ className: "shrink-0 text-muted-foreground"
570
+ }),
571
+ part.state === "executing" && /* @__PURE__ */ jsx("span", {
572
+ className: "shrink-0 text-muted-foreground",
573
+ children: "⋯"
574
+ }),
575
+ /* @__PURE__ */ jsxs("span", {
576
+ className: "font-mono text-muted-foreground",
577
+ children: [
578
+ part.name,
579
+ "(",
580
+ formatArgs(part.args),
581
+ ")"
582
+ ]
583
+ })
584
+ ]
585
+ }), expanded && /* @__PURE__ */ jsx("pre", {
586
+ className: "mt-2 max-h-40 overflow-auto rounded bg-muted p-2 text-xs text-foreground",
587
+ children: JSON.stringify({
588
+ args: part.args,
589
+ result: part.result,
590
+ error: part.error
591
+ }, null, 2)
592
+ })]
593
+ });
594
+ }
595
+ function MessageView({ message, onApprove, onReject }) {
596
+ if (message.role === "user") return /* @__PURE__ */ jsx("div", {
597
+ className: "flex justify-end",
598
+ children: /* @__PURE__ */ jsx("div", {
599
+ className: "max-w-[85%] whitespace-pre-wrap rounded-lg bg-muted px-3 py-2 text-sm text-foreground",
600
+ children: message.parts.filter((p) => p.type === "text").map((p) => p.text).join("")
601
+ })
602
+ });
603
+ return /* @__PURE__ */ jsxs("div", {
604
+ className: "space-y-2",
605
+ children: [message.parts.map((part, index) => part.type === "text" ? /* @__PURE__ */ jsx("div", {
606
+ className: "text-sm text-foreground",
607
+ children: /* @__PURE__ */ jsx(Markdown, { text: part.text })
608
+ }, index) : /* @__PURE__ */ jsxs("div", {
609
+ className: "space-y-2",
610
+ children: [/* @__PURE__ */ jsx(ToolCard, { part }), part.state === "awaiting-approval" && /* @__PURE__ */ jsx(ApprovalCard, {
611
+ approval: {
612
+ toolCallId: part.toolCallId,
613
+ name: part.name,
614
+ args: part.args
615
+ },
616
+ onApprove,
617
+ onReject
618
+ })]
619
+ }, part.toolCallId)), message.parts.length === 0 && /* @__PURE__ */ jsx("div", {
620
+ className: "text-sm text-muted-foreground",
621
+ children: "Thinking…"
622
+ })]
623
+ });
624
+ }
625
+ /** The chat window panel, anchored above the FAB in the bottom-right. */
626
+ function ChatWindow({ chat, onClose }) {
627
+ const [fullscreen, setFullscreen] = useState(false);
628
+ const [draft, setDraft] = useState("");
629
+ const listRef = useRef(null);
630
+ useEffect(() => {
631
+ const el = listRef.current;
632
+ if (el) el.scrollTo({ top: el.scrollHeight });
633
+ }, [chat.messages]);
634
+ useEffect(() => {
635
+ if (!fullscreen) return;
636
+ const onKey = (event) => {
637
+ if (event.key === "Escape") setFullscreen(false);
638
+ };
639
+ window.addEventListener("keydown", onKey);
640
+ return () => window.removeEventListener("keydown", onKey);
641
+ }, [fullscreen]);
642
+ const submit = () => {
643
+ const text = draft;
644
+ setDraft("");
645
+ chat.send(text);
646
+ };
647
+ return /* @__PURE__ */ jsxs("div", {
648
+ className: fullscreen ? "fixed inset-0 z-50 flex flex-col overflow-hidden bg-background" : "fixed bottom-24 right-6 z-40 flex max-h-[min(28rem,calc(100vh-8rem))] w-[min(24rem,calc(100vw-3rem))] flex-col overflow-hidden rounded-xl border border-border bg-background shadow-2xl",
649
+ children: [
650
+ /* @__PURE__ */ jsxs("div", {
651
+ className: "flex items-center justify-between border-b border-border px-4 py-2.5",
652
+ children: [/* @__PURE__ */ jsx("span", {
653
+ className: "text-sm font-semibold text-foreground",
654
+ children: "AI Assistant"
655
+ }), /* @__PURE__ */ jsxs("div", {
656
+ className: "flex items-center gap-1 text-muted-foreground",
657
+ children: [
658
+ /* @__PURE__ */ jsx("button", {
659
+ type: "button",
660
+ "aria-label": fullscreen ? "Exit fullscreen" : "Fullscreen",
661
+ title: fullscreen ? "Exit fullscreen (Esc)" : "Fullscreen",
662
+ onClick: () => setFullscreen((f) => !f),
663
+ className: "rounded p-1 hover:bg-muted hover:text-foreground",
664
+ children: fullscreen ? /* @__PURE__ */ jsx(Minimize2, { size: 15 }) : /* @__PURE__ */ jsx(Maximize2, { size: 15 })
665
+ }),
666
+ /* @__PURE__ */ jsx("button", {
667
+ type: "button",
668
+ "aria-label": "Clear conversation",
669
+ title: "New chat",
670
+ onClick: chat.clear,
671
+ className: "rounded p-1 hover:bg-muted hover:text-foreground",
672
+ children: /* @__PURE__ */ jsx(Trash2, { size: 15 })
673
+ }),
674
+ /* @__PURE__ */ jsx("button", {
675
+ type: "button",
676
+ "aria-label": "Close AI chat",
677
+ title: "Close",
678
+ onClick: onClose,
679
+ className: "rounded p-1 hover:bg-muted hover:text-foreground",
680
+ children: /* @__PURE__ */ jsx(X, { size: 15 })
681
+ })
682
+ ]
683
+ })]
684
+ }),
685
+ /* @__PURE__ */ jsxs("div", {
686
+ ref: listRef,
687
+ className: "flex-1 space-y-3 overflow-y-auto px-4 py-3",
688
+ children: [
689
+ chat.messages.length === 0 && /* @__PURE__ */ jsx("div", {
690
+ className: "pt-8 text-center text-sm text-muted-foreground",
691
+ children: chat.configured ? /* @__PURE__ */ jsx("p", { children: "Ask the assistant to create or change documents, manage drives, or inspect read models." }) : /* @__PURE__ */ jsx("p", { children: "Add your OpenAI-compatible endpoint under Settings → AI Assistant, then start a conversation." })
692
+ }),
693
+ chat.messages.map((message) => /* @__PURE__ */ jsx(MessageView, {
694
+ message,
695
+ onApprove: chat.approve,
696
+ onReject: chat.reject
697
+ }, message.id)),
698
+ chat.error && /* @__PURE__ */ jsx("div", {
699
+ className: "rounded-lg border border-destructive/40 bg-destructive/10 p-2 text-xs text-destructive",
700
+ children: chat.error
701
+ })
702
+ ]
703
+ }),
704
+ /* @__PURE__ */ jsx("div", {
705
+ className: "border-t border-border p-3",
706
+ children: /* @__PURE__ */ jsxs("div", {
707
+ className: "flex items-end gap-2",
708
+ children: [/* @__PURE__ */ jsx("textarea", {
709
+ rows: 2,
710
+ value: draft,
711
+ onChange: (e) => setDraft(e.target.value),
712
+ onKeyDown: (e) => {
713
+ if (e.key === "Enter" && !e.shiftKey) {
714
+ e.preventDefault();
715
+ submit();
716
+ }
717
+ },
718
+ placeholder: chat.configured ? "Ask the assistant… (Enter to send)" : "Configure the endpoint in settings first",
719
+ className: "max-h-32 min-h-[2.5rem] flex-1 resize-none rounded-md border border-border bg-background p-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none",
720
+ disabled: chat.isStreaming
721
+ }), chat.isStreaming ? /* @__PURE__ */ jsx("button", {
722
+ type: "button",
723
+ "aria-label": "Stop generating",
724
+ title: "Stop",
725
+ onClick: chat.stop,
726
+ className: "flex size-9 shrink-0 items-center justify-center rounded-md bg-primary text-primary-foreground hover:bg-primary/90",
727
+ children: /* @__PURE__ */ jsx(Square, { size: 15 })
728
+ }) : /* @__PURE__ */ jsx("button", {
729
+ type: "button",
730
+ "aria-label": "Send message",
731
+ title: "Send",
732
+ onClick: submit,
733
+ disabled: !draft.trim() || !chat.configured,
734
+ className: "flex size-9 shrink-0 items-center justify-center rounded-md bg-primary text-primary-foreground hover:bg-primary/90 disabled:disabled-effect",
735
+ children: /* @__PURE__ */ jsx(ArrowUp, { size: 15 })
736
+ })]
737
+ })
738
+ })
739
+ ]
740
+ });
741
+ }
742
+ //#endregion
743
+ //#region src/ai/settings-store.ts
744
+ const STORAGE_KEY = "ph-ai-chat-settings";
745
+ const DEFAULT_AI_SETTINGS = {
746
+ enabled: false,
747
+ baseUrl: "",
748
+ apiKey: "",
749
+ model: "",
750
+ autoApproveWrites: false
751
+ };
752
+ let snapshot = load();
753
+ const listeners = /* @__PURE__ */ new Set();
754
+ function load() {
755
+ if (typeof localStorage === "undefined") return { ...DEFAULT_AI_SETTINGS };
756
+ try {
757
+ const raw = localStorage.getItem(STORAGE_KEY);
758
+ if (!raw) return { ...DEFAULT_AI_SETTINGS };
759
+ const parsed = JSON.parse(raw);
760
+ return {
761
+ enabled: typeof parsed.enabled === "boolean" ? parsed.enabled : false,
762
+ baseUrl: typeof parsed.baseUrl === "string" ? parsed.baseUrl : "",
763
+ apiKey: typeof parsed.apiKey === "string" ? parsed.apiKey : "",
764
+ model: typeof parsed.model === "string" ? parsed.model : "",
765
+ autoApproveWrites: typeof parsed.autoApproveWrites === "boolean" ? parsed.autoApproveWrites : false
766
+ };
767
+ } catch {
768
+ return { ...DEFAULT_AI_SETTINGS };
769
+ }
770
+ }
771
+ function persist(settings) {
772
+ try {
773
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
774
+ } catch {}
775
+ }
776
+ function emit() {
777
+ for (const listener of listeners) listener();
778
+ }
779
+ /** Returns the current settings (stable snapshot for useSyncExternalStore). */
780
+ function getAiSettings() {
781
+ return snapshot;
782
+ }
783
+ /** Merges a partial update, persists, and notifies subscribers. */
784
+ function updateAiSettings(patch) {
785
+ snapshot = {
786
+ ...snapshot,
787
+ ...patch
788
+ };
789
+ persist(snapshot);
790
+ emit();
791
+ return snapshot;
792
+ }
793
+ /** Clears all stored settings. */
794
+ function clearAiSettings() {
795
+ snapshot = { ...DEFAULT_AI_SETTINGS };
796
+ try {
797
+ localStorage.removeItem(STORAGE_KEY);
798
+ } catch {}
799
+ emit();
800
+ }
801
+ /** Subscribes to settings changes. Returns the unsubscribe function. */
802
+ function subscribeAiSettings(listener) {
803
+ listeners.add(listener);
804
+ return () => {
805
+ listeners.delete(listener);
806
+ };
807
+ }
808
+ /** True when the endpoint, key and model are all configured. */
809
+ function isAiConfigured(settings) {
810
+ return settings.baseUrl.trim().length > 0 && settings.apiKey.trim().length > 0 && settings.model.trim().length > 0;
811
+ }
812
+ //#endregion
813
+ //#region src/ai/switchboard.ts
814
+ /**
815
+ * Reads the tab-side sync manager from the browser global. Returns
816
+ * `undefined` in non-browser environments or before the reactor client
817
+ * module is initialized.
818
+ */
819
+ function getBrowserSyncManager() {
820
+ if (typeof window === "undefined") return;
821
+ return window.ph?.reactorClientModule?.reactorModule?.syncModule?.syncManager;
822
+ }
823
+ /** Suffix of the per-drive GraphQL channel endpoint the switchboard serves. */
824
+ const GQL_CHANNEL_SUFFIX = "/graphql/r";
825
+ /**
826
+ * Resolves the switchboard that serves a drive, if any.
827
+ *
828
+ * Every remote drive stores its switchboard's exact GraphQL channel
829
+ * endpoint in its sync remote — `channelConfig = { type: "gql",
830
+ * parameters: { url: "<origin>/graphql/r" } }` — and the switchboard base
831
+ * is that URL minus the `/graphql/r` suffix. Deriving it per drive (rather
832
+ * than from a global setting) is what keeps multi-switchboard deployments
833
+ * correct. Drives that are not synced to a remote switchboard (local
834
+ * drives, other channel types, unexpected shapes) return `undefined`.
835
+ */
836
+ function resolveDriveSwitchboard(driveId, syncManager) {
837
+ const manager = syncManager ?? getBrowserSyncManager();
838
+ if (!driveId || !manager) return;
839
+ const collectionId = DriveCollectionId.forDrive(driveId);
840
+ const remote = manager.list().find((r) => r.meta.collectionId.equals(collectionId));
841
+ if (!remote) return;
842
+ const { channelConfig } = remote.meta;
843
+ if (channelConfig.type !== "gql") return;
844
+ const url = channelConfig.parameters.url;
845
+ if (typeof url !== "string" || !url.endsWith(GQL_CHANNEL_SUFFIX)) return;
846
+ const switchboardUrl = url.slice(0, -10);
847
+ return {
848
+ switchboardUrl,
849
+ graphqlUrl: `${switchboardUrl}/graphql`
850
+ };
851
+ }
852
+ /**
853
+ * Introspection over the switchboard supergraph's root query and mutation
854
+ * fields. Types are requested with three levels of `ofType` nesting so
855
+ * that common shapes like `[X!]!` (LIST(NON_NULL(LIST(X)))) render fully.
856
+ */
857
+ const SWITCHBOARD_INTROSPECTION_QUERY = `
858
+ query SwitchboardIntrospection {
859
+ __schema {
860
+ queryType {
861
+ fields {
862
+ name
863
+ description
864
+ args {
865
+ name
866
+ type {
867
+ kind
868
+ name
869
+ ofType {
870
+ kind
871
+ name
872
+ ofType {
873
+ kind
874
+ name
875
+ ofType {
876
+ kind
877
+ name
878
+ }
879
+ }
880
+ }
881
+ }
882
+ }
883
+ type {
884
+ kind
885
+ name
886
+ ofType {
887
+ kind
888
+ name
889
+ ofType {
890
+ kind
891
+ name
892
+ ofType {
893
+ kind
894
+ name
895
+ }
896
+ }
897
+ }
898
+ }
899
+ }
900
+ }
901
+ mutationType {
902
+ fields {
903
+ name
904
+ description
905
+ args {
906
+ name
907
+ type {
908
+ kind
909
+ name
910
+ ofType {
911
+ kind
912
+ name
913
+ ofType {
914
+ kind
915
+ name
916
+ ofType {
917
+ kind
918
+ name
919
+ }
920
+ }
921
+ }
922
+ }
923
+ }
924
+ type {
925
+ kind
926
+ name
927
+ ofType {
928
+ kind
929
+ name
930
+ ofType {
931
+ kind
932
+ name
933
+ ofType {
934
+ kind
935
+ name
936
+ }
937
+ }
938
+ }
939
+ }
940
+ }
941
+ }
942
+ }
943
+ }
944
+ `;
945
+ /**
946
+ * Renders an introspected type reference as a GraphQL type string.
947
+ * Recursion is bounded by the `ofType` nesting of the query, so it always
948
+ * terminates.
949
+ */
950
+ function renderTypeRef(type) {
951
+ if (!type) return "Unknown";
952
+ switch (type.kind) {
953
+ case "NON_NULL": return `${renderTypeRef(type.ofType)}!`;
954
+ case "LIST": return `[${renderTypeRef(type.ofType)}]`;
955
+ default: return type.name ?? "Unknown";
956
+ }
957
+ }
958
+ /** Cap on the number of fields reported per root type. */
959
+ const MAX_FIELDS_PER_ROOT = 200;
960
+ function summarizeFields(fields) {
961
+ const list = fields ?? [];
962
+ return {
963
+ fields: list.slice(0, MAX_FIELDS_PER_ROOT).map((field) => ({
964
+ name: field.name,
965
+ ...field.description ? { description: field.description } : {},
966
+ args: field.args.map((arg) => `${arg.name}: ${renderTypeRef(arg.type)}`).join(", "),
967
+ returns: renderTypeRef(field.type)
968
+ })),
969
+ truncated: list.length > MAX_FIELDS_PER_ROOT
970
+ };
971
+ }
972
+ /**
973
+ * Reduces a GraphQL `__schema` introspection payload (the `data.__schema`
974
+ * object) to a compact summary of its root query and mutation fields.
975
+ * Accepts `unknown` because the payload arrives over the wire.
976
+ */
977
+ function summarizeIntrospectionSchema(schema) {
978
+ const introspection = schema ?? {};
979
+ const queries = summarizeFields(introspection.queryType?.fields);
980
+ const mutations = summarizeFields(introspection.mutationType?.fields);
981
+ return {
982
+ queries: queries.fields,
983
+ mutations: mutations.fields,
984
+ truncated: queries.truncated || mutations.truncated
985
+ };
986
+ }
987
+ const SWITCHBOARD_SCHEMA_TOOL_NAME = "getSwitchboardSchema";
988
+ /**
989
+ * Creates the read-only `getSwitchboardSchema` tool: it introspects the
990
+ * GraphQL supergraph of the switchboard serving a drive and returns a
991
+ * summary of its root queries and mutations. It reads the schema only and
992
+ * never modifies data.
993
+ */
994
+ function createSwitchboardSchemaTool(deps) {
995
+ const getSyncManager = deps?.getSyncManager ?? getBrowserSyncManager;
996
+ const getSelectedDriveId = deps?.getSelectedDriveId ?? (() => typeof window === "undefined" ? void 0 : window.ph?.selectedDriveId);
997
+ const tokenProvider = deps?.tokenProvider ?? ambientRenownTokenProvider;
998
+ const fetchImpl = deps?.fetchImpl ?? fetch;
999
+ const timeoutMs = deps?.timeoutMs ?? 1e4;
1000
+ return {
1001
+ name: SWITCHBOARD_SCHEMA_TOOL_NAME,
1002
+ description: "Introspect the GraphQL endpoint of the switchboard serving a drive and list the queries and mutations it exposes (reactor operations, document-model read models, package subgraphs). Read-only: it reads the schema only and never modifies data. Omit driveId to introspect the currently selected drive.",
1003
+ inputSchema: { driveId: z.string().describe("Drive to introspect. Omit to use the currently selected drive.").optional() },
1004
+ annotations: { readOnlyHint: true },
1005
+ callback: async (input) => {
1006
+ const driveId = input?.driveId ?? getSelectedDriveId();
1007
+ const sb = resolveDriveSwitchboard(driveId, getSyncManager());
1008
+ if (!sb) throw new Error(driveId ? `Drive "${driveId}" has no switchboard: it is not synced to a remote switchboard.` : "No drive is currently selected and no driveId was given.");
1009
+ const token = await tokenProvider();
1010
+ let response;
1011
+ try {
1012
+ response = await fetchImpl(sb.graphqlUrl, {
1013
+ method: "POST",
1014
+ headers: {
1015
+ "content-type": "application/json",
1016
+ ...token ? { authorization: `Bearer ${token}` } : {}
1017
+ },
1018
+ body: JSON.stringify({ query: SWITCHBOARD_INTROSPECTION_QUERY }),
1019
+ signal: AbortSignal.timeout(timeoutMs)
1020
+ });
1021
+ } catch (error) {
1022
+ throw new Error(`Switchboard at ${sb.graphqlUrl} is not reachable: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
1023
+ }
1024
+ if (response.status === 401 || response.status === 403) throw new Error(`Switchboard at ${sb.graphqlUrl} refused the request (HTTP ${response.status}). Sign in to the switchboard (Renown) and try again.`);
1025
+ if (!response.ok) throw new Error(`Switchboard introspection failed: HTTP ${response.status} ${response.statusText} from ${sb.graphqlUrl}`);
1026
+ const body = await response.json();
1027
+ const schemaErrors = body?.errors;
1028
+ if (schemaErrors && schemaErrors.length > 0) throw new Error(schemaErrors.map((e) => e.message ?? "unknown error").join("; "));
1029
+ const schema = body?.data?.__schema;
1030
+ if (!schema?.queryType) throw new Error(`Switchboard at ${sb.graphqlUrl} did not return a GraphQL schema.`);
1031
+ const summary = summarizeIntrospectionSchema(schema);
1032
+ return {
1033
+ switchboardUrl: sb.switchboardUrl,
1034
+ graphqlUrl: sb.graphqlUrl,
1035
+ ...summary
1036
+ };
1037
+ }
1038
+ };
1039
+ }
1040
+ //#endregion
1041
+ //#region src/ai/context.ts
1042
+ /**
1043
+ * Snapshot of the current selection in the drive explorer, used to make the
1044
+ * chat context-aware: "create a budget here" targets the selected drive,
1045
+ * "summarize this document" targets the selected document.
1046
+ */
1047
+ function useChatContext() {
1048
+ const [drive] = useSelectedDriveSafe();
1049
+ const node = useSelectedNode();
1050
+ const documentId = useSelectedDocumentId();
1051
+ const context = {
1052
+ driveId: drive?.header.id,
1053
+ driveName: drive?.header.name
1054
+ };
1055
+ if (node) {
1056
+ context.nodeId = node.id;
1057
+ context.nodeName = node.name;
1058
+ if (isFolderNode(node)) context.nodeKind = "folder";
1059
+ else if (isFileNode(node)) context.nodeKind = "file";
1060
+ }
1061
+ if (documentId && node && isFileNode(node)) {
1062
+ context.documentName = node.name;
1063
+ context.documentType = node.documentType;
1064
+ context.documentId = documentId;
1065
+ }
1066
+ const sb = resolveDriveSwitchboard(drive?.header.id);
1067
+ if (sb) {
1068
+ context.switchboardUrl = sb.switchboardUrl;
1069
+ context.switchboardGraphqlUrl = sb.graphqlUrl;
1070
+ }
1071
+ return context;
1072
+ }
1073
+ //#endregion
1074
+ //#region src/ai/use-reactor-chat.ts
1075
+ function updateAssistantPart(messages, assistantId, update) {
1076
+ if (!assistantId) return messages;
1077
+ return messages.map((message) => message.id === assistantId ? {
1078
+ ...message,
1079
+ parts: update(message.parts)
1080
+ } : message);
1081
+ }
1082
+ function toErrorMessage(error) {
1083
+ return error instanceof Error ? error.message : String(error);
1084
+ }
1085
+ /**
1086
+ * Drives one in-browser chat conversation against the reactor tools.
1087
+ *
1088
+ * `toolsProvider` lazily resolves the tool descriptors (e.g. from
1089
+ * `createReactorMcpProvider` bound to `window.ph.reactorClient`) when the
1090
+ * user sends a message, so the reactor does not need to be ready at render
1091
+ * time.
1092
+ */
1093
+ function useReactorChat(toolsProvider) {
1094
+ const [messages, setMessages] = useState([]);
1095
+ const [isStreaming, setIsStreaming] = useState(false);
1096
+ const [pendingApprovals, setPendingApprovals] = useState([]);
1097
+ const [error, setError] = useState(null);
1098
+ const agentRef = useRef(null);
1099
+ const abortRef = useRef(null);
1100
+ const streamingRef = useRef(false);
1101
+ const pendingRef = useRef(/* @__PURE__ */ new Set());
1102
+ const assistantIdRef = useRef(null);
1103
+ const historyRef = useRef([]);
1104
+ const settings = useSyncExternalStore(subscribeAiSettings, getAiSettings);
1105
+ const context = useChatContext();
1106
+ const handleEvent = useCallback((event) => {
1107
+ const assistantId = assistantIdRef.current;
1108
+ switch (event.type) {
1109
+ case "text-delta":
1110
+ setMessages((prev) => updateAssistantPart(prev, assistantId, (parts) => {
1111
+ const last = parts[parts.length - 1];
1112
+ if (last?.type === "text") return [...parts.slice(0, -1), {
1113
+ ...last,
1114
+ text: last.text + event.delta
1115
+ }];
1116
+ return [...parts, {
1117
+ type: "text",
1118
+ text: event.delta
1119
+ }];
1120
+ }));
1121
+ break;
1122
+ case "tool-start":
1123
+ setMessages((prev) => updateAssistantPart(prev, assistantId, (parts) => [...parts, {
1124
+ type: "tool",
1125
+ toolCallId: event.toolCallId,
1126
+ name: event.name,
1127
+ args: event.args,
1128
+ state: pendingRef.current.has(event.toolCallId) ? "awaiting-approval" : "executing"
1129
+ }]));
1130
+ break;
1131
+ case "approval-request":
1132
+ pendingRef.current.add(event.toolCallId);
1133
+ setMessages((prev) => updateAssistantPart(prev, assistantId, (parts) => parts.map((part) => part.type === "tool" && part.toolCallId === event.toolCallId ? {
1134
+ ...part,
1135
+ state: "awaiting-approval"
1136
+ } : part)));
1137
+ setPendingApprovals((prev) => [...prev, {
1138
+ toolCallId: event.toolCallId,
1139
+ name: event.name,
1140
+ args: event.args
1141
+ }]);
1142
+ break;
1143
+ case "approval-resolved":
1144
+ pendingRef.current.delete(event.toolCallId);
1145
+ setPendingApprovals((prev) => prev.filter((a) => a.toolCallId !== event.toolCallId));
1146
+ if (!event.approved) setMessages((prev) => updateAssistantPart(prev, assistantId, (parts) => parts.map((part) => part.type === "tool" && part.toolCallId === event.toolCallId ? {
1147
+ ...part,
1148
+ state: "rejected"
1149
+ } : part)));
1150
+ break;
1151
+ case "tool-result":
1152
+ setMessages((prev) => updateAssistantPart(prev, assistantId, (parts) => parts.map((part) => part.type === "tool" && part.toolCallId === event.toolCallId ? {
1153
+ ...part,
1154
+ state: event.state,
1155
+ result: event.result,
1156
+ error: event.error
1157
+ } : part)));
1158
+ break;
1159
+ case "error":
1160
+ setError(event.error);
1161
+ break;
1162
+ case "finish": break;
1163
+ }
1164
+ }, []);
1165
+ const send = useCallback((text) => {
1166
+ const trimmed = text.trim();
1167
+ if (!trimmed || streamingRef.current) return;
1168
+ setError(null);
1169
+ const currentSettings = getAiSettings();
1170
+ if (!isAiConfigured(currentSettings)) {
1171
+ setError("Configure the AI endpoint, API key and model in settings first.");
1172
+ return;
1173
+ }
1174
+ (async () => {
1175
+ let tools;
1176
+ try {
1177
+ if (!toolsProvider) throw new Error("No tool provider configured");
1178
+ tools = await toolsProvider();
1179
+ } catch (toolError) {
1180
+ setError(`Could not initialise the reactor tools: ${toErrorMessage(toolError)}`);
1181
+ return;
1182
+ }
1183
+ if (tools.length === 0) {
1184
+ setError("The reactor exposed no tools.");
1185
+ return;
1186
+ }
1187
+ const controller = new AbortController();
1188
+ abortRef.current = controller;
1189
+ const agent = new ReactorChatAgent({
1190
+ settings: currentSettings,
1191
+ tools,
1192
+ context,
1193
+ onEvent: handleEvent,
1194
+ signal: controller.signal,
1195
+ history: historyRef.current
1196
+ });
1197
+ agentRef.current = agent;
1198
+ const assistantId = crypto.randomUUID();
1199
+ assistantIdRef.current = assistantId;
1200
+ setMessages((prev) => [
1201
+ ...prev,
1202
+ {
1203
+ id: crypto.randomUUID(),
1204
+ role: "user",
1205
+ parts: [{
1206
+ type: "text",
1207
+ text: trimmed
1208
+ }]
1209
+ },
1210
+ {
1211
+ id: assistantId,
1212
+ role: "assistant",
1213
+ parts: []
1214
+ }
1215
+ ]);
1216
+ streamingRef.current = true;
1217
+ setIsStreaming(true);
1218
+ try {
1219
+ await agent.send(trimmed);
1220
+ } catch (sendError) {
1221
+ setError(toErrorMessage(sendError));
1222
+ } finally {
1223
+ streamingRef.current = false;
1224
+ setIsStreaming(false);
1225
+ abortRef.current = null;
1226
+ historyRef.current = agent.getHistory();
1227
+ }
1228
+ })();
1229
+ }, [
1230
+ toolsProvider,
1231
+ context,
1232
+ handleEvent
1233
+ ]);
1234
+ const stop = useCallback(() => {
1235
+ abortRef.current?.abort();
1236
+ }, []);
1237
+ const approve = useCallback((toolCallId) => {
1238
+ agentRef.current?.approve(toolCallId);
1239
+ }, []);
1240
+ const reject = useCallback((toolCallId) => {
1241
+ agentRef.current?.reject(toolCallId);
1242
+ }, []);
1243
+ const clear = useCallback(() => {
1244
+ abortRef.current?.abort();
1245
+ agentRef.current?.reset();
1246
+ agentRef.current = null;
1247
+ historyRef.current = [];
1248
+ pendingRef.current.clear();
1249
+ setPendingApprovals([]);
1250
+ setError(null);
1251
+ assistantIdRef.current = null;
1252
+ }, []);
1253
+ return {
1254
+ messages,
1255
+ isStreaming,
1256
+ pendingApprovals,
1257
+ error,
1258
+ settings,
1259
+ configured: isAiConfigured(settings),
1260
+ send,
1261
+ stop,
1262
+ approve,
1263
+ reject,
1264
+ clear
1265
+ };
1266
+ }
1267
+ //#endregion
1268
+ //#region src/ai/components/reactor-chat-fab.tsx
1269
+ /**
1270
+ * Bottom-right floating action button that opens the reactor AI chat window.
1271
+ *
1272
+ * `getTools` lazily resolves the reactor tool descriptors (from
1273
+ * `createReactorMcpProvider` bound to the browser reactor client) when the
1274
+ * user sends a message.
1275
+ */
1276
+ function ReactorChatFab({ getTools }) {
1277
+ const settings = useSyncExternalStore(subscribeAiSettings, getAiSettings);
1278
+ const [open, setOpen] = useState(false);
1279
+ const chat = useReactorChat(getTools);
1280
+ if (!settings.enabled) return null;
1281
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
1282
+ type: "button",
1283
+ "aria-label": open ? "Close AI chat" : "Open AI chat",
1284
+ title: open ? "Close AI chat" : "AI chat",
1285
+ onClick: () => setOpen((o) => !o),
1286
+ className: "fixed bottom-6 right-6 z-40 flex size-12 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg transition-transform hover:scale-105 disabled:disabled-effect",
1287
+ children: open ? /* @__PURE__ */ jsx(X, { size: 20 }) : /* @__PURE__ */ jsx(MessageCircle, { size: 20 })
1288
+ }), open && /* @__PURE__ */ jsx(ChatWindow, {
1289
+ chat,
1290
+ onClose: () => setOpen(false)
1291
+ })] });
1292
+ }
1293
+ //#endregion
1294
+ export { ApprovalCard, ChatWindow, DEFAULT_AI_SETTINGS, FULL_RESULT_TURNS, MAX_AGENT_STEPS, MAX_TOOL_RESULT_CHARS, ReactorChatAgent, ReactorChatFab, STUB_MIN_CHARS, SWITCHBOARD_INTROSPECTION_QUERY, SWITCHBOARD_SCHEMA_TOOL_NAME, WRITE_TOOLS, buildSystemPrompt, clearAiSettings, createReactorChatModel, createSwitchboardSchemaTool, getAiSettings, getBrowserSyncManager, isAiConfigured, isWriteTool, resolveDriveSwitchboard, stubStaleToolResults, subscribeAiSettings, summarizeIntrospectionSchema, unwrapToolResult, updateAiSettings, useChatContext, useReactorChat };
1295
+
1296
+ //# sourceMappingURL=index.js.map