@tangle-network/sandbox-ui 0.60.0 → 0.62.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/assistant.js DELETED
@@ -1,2610 +0,0 @@
1
- import {
2
- ProviderIcon
3
- } from "./chunk-GTJ5PSK5.js";
4
- import {
5
- timeAgo
6
- } from "./chunk-TK46XFLM.js";
7
- import {
8
- providerLabel
9
- } from "./chunk-7JLB7UZ3.js";
10
- import {
11
- ModelPicker
12
- } from "./chunk-SF6LPMMN.js";
13
- import "./chunk-EI44GEQ5.js";
14
- import "./chunk-U67V476Y.js";
15
-
16
- // src/assistant/sse.ts
17
- var SSEChunkParser = class {
18
- buffer = "";
19
- current = {};
20
- push(chunk) {
21
- this.buffer += chunk;
22
- const lines = this.buffer.split("\n");
23
- this.buffer = lines.pop() ?? "";
24
- return this.processLines(lines);
25
- }
26
- flush() {
27
- const lines = this.buffer ? [this.buffer] : [];
28
- this.buffer = "";
29
- const events = this.processLines(lines);
30
- const finalEvent = this.parseCurrent();
31
- if (finalEvent) {
32
- events.push(finalEvent);
33
- this.current = {};
34
- }
35
- return events;
36
- }
37
- processLines(lines) {
38
- const events = [];
39
- for (const rawLine of lines) {
40
- const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
41
- if (line.startsWith(":")) continue;
42
- if (line === "") {
43
- const parsed = this.parseCurrent();
44
- if (parsed) events.push(parsed);
45
- this.current = {};
46
- continue;
47
- }
48
- if (line.startsWith("id:")) {
49
- this.current.id = line.slice(3).trim();
50
- } else if (line.startsWith("event:")) {
51
- this.current.event = line.slice(6).trim();
52
- } else if (line.startsWith("data:")) {
53
- let value = line.slice(5);
54
- if (value.startsWith(" ")) value = value.slice(1);
55
- this.current.data = this.current.data !== void 0 ? `${this.current.data}
56
- ${value}` : value;
57
- }
58
- }
59
- return events;
60
- }
61
- parseCurrent() {
62
- if (this.current.data === void 0) return null;
63
- const rawData = this.current.data.trim();
64
- if (!rawData) return null;
65
- let data;
66
- try {
67
- data = JSON.parse(rawData);
68
- } catch {
69
- data = rawData;
70
- }
71
- return {
72
- data,
73
- rawData,
74
- eventId: this.current.id,
75
- eventType: this.current.event
76
- };
77
- }
78
- };
79
- async function readSSEEvents(body, onEvent) {
80
- const reader = body.getReader();
81
- const decoder = new TextDecoder();
82
- const parser = new SSEChunkParser();
83
- try {
84
- while (true) {
85
- const { done, value } = await reader.read();
86
- if (done) break;
87
- for (const event of parser.push(decoder.decode(value, { stream: true }))) {
88
- onEvent(event);
89
- }
90
- }
91
- const tail = decoder.decode();
92
- if (tail) {
93
- for (const event of parser.push(tail)) onEvent(event);
94
- }
95
- for (const event of parser.flush()) onEvent(event);
96
- } catch (err) {
97
- await reader.cancel(err).catch(() => {
98
- });
99
- throw err;
100
- } finally {
101
- reader.releaseLock();
102
- }
103
- }
104
-
105
- // src/assistant/client.ts
106
- var EMPTY_MODELS = { default: null, models: [] };
107
- function asObject(v) {
108
- return v && typeof v === "object" && !Array.isArray(v) ? v : null;
109
- }
110
- function reqStr(v) {
111
- return typeof v === "string" && v !== "" ? v : null;
112
- }
113
- function numOrNull(v) {
114
- return typeof v === "number" && Number.isFinite(v) ? v : null;
115
- }
116
- function parseRequirement(raw) {
117
- if (!raw || typeof raw !== "object") return null;
118
- const r = raw;
119
- if (typeof r.provider !== "string" || r.provider === "") return null;
120
- if (typeof r.connected !== "boolean") return null;
121
- const kind = r.kind === "integration" || r.kind === "github_app" ? r.kind : void 0;
122
- const connectUrl = typeof r.connectUrl === "string" || r.connectUrl === null ? r.connectUrl : void 0;
123
- return {
124
- provider: r.provider,
125
- connected: r.connected,
126
- ...kind ? { kind } : {},
127
- ...connectUrl !== void 0 ? { connectUrl } : {}
128
- };
129
- }
130
- function parseRequirements(v) {
131
- if (!Array.isArray(v)) return void 0;
132
- const out = [];
133
- for (const item of v) {
134
- const parsed = parseRequirement(item);
135
- if (parsed) out.push(parsed);
136
- }
137
- return out;
138
- }
139
- function toStreamEvent(event, data) {
140
- const obj = asObject(data);
141
- if (!obj) return null;
142
- switch (event) {
143
- case "thread": {
144
- const threadId = reqStr(obj.threadId);
145
- const turnId = reqStr(obj.turnId);
146
- if (!threadId || !turnId) return null;
147
- return {
148
- type: "thread",
149
- data: { threadId, turnId, model: reqStr(obj.model) }
150
- };
151
- }
152
- case "delta": {
153
- if (typeof obj.text !== "string") return null;
154
- return { type: "delta", data: { text: obj.text } };
155
- }
156
- case "reasoning": {
157
- if (typeof obj.text !== "string") return null;
158
- return { type: "reasoning", data: { text: obj.text } };
159
- }
160
- case "tool_call": {
161
- const callId = reqStr(obj.callId);
162
- const name = reqStr(obj.name);
163
- if (!callId || !name) return null;
164
- return { type: "tool_call", data: { callId, name } };
165
- }
166
- case "tool_result": {
167
- const callId = reqStr(obj.callId);
168
- const name = reqStr(obj.name);
169
- if (!callId || !name) return null;
170
- return {
171
- type: "tool_result",
172
- data: {
173
- callId,
174
- name,
175
- ok: Boolean(obj.ok),
176
- output: obj.output,
177
- error: obj.error
178
- }
179
- };
180
- }
181
- case "tool_proposal": {
182
- const callId = reqStr(obj.callId);
183
- const name = reqStr(obj.name);
184
- if (!callId || !name) return null;
185
- return {
186
- type: "tool_proposal",
187
- data: {
188
- proposalId: obj.proposalId == null ? null : reqStr(obj.proposalId),
189
- callId,
190
- name,
191
- args: obj.args,
192
- requirements: parseRequirements(obj.requirements)
193
- }
194
- };
195
- }
196
- case "usage":
197
- return {
198
- type: "usage",
199
- data: {
200
- promptTokens: numOrNull(obj.promptTokens),
201
- completionTokens: numOrNull(obj.completionTokens),
202
- costUsd: numOrNull(obj.costUsd),
203
- balanceUsd: numOrNull(obj.balanceUsd),
204
- replayed: Boolean(obj.replayed)
205
- }
206
- };
207
- case "done": {
208
- const turnId = reqStr(obj.turnId);
209
- const status = reqStr(obj.status);
210
- if (!turnId || !status) return null;
211
- return {
212
- type: "done",
213
- data: {
214
- turnId,
215
- status,
216
- proposed: Boolean(obj.proposed),
217
- capped: Boolean(obj.capped)
218
- }
219
- };
220
- }
221
- case "error":
222
- return {
223
- type: "error",
224
- data: {
225
- code: reqStr(obj.code) ?? "STREAM_FAILED",
226
- message: reqStr(obj.message) ?? "The assistant stream failed"
227
- }
228
- };
229
- default:
230
- return null;
231
- }
232
- }
233
- async function readErrorEvent(res) {
234
- try {
235
- const body = await res.json();
236
- return {
237
- type: "error",
238
- data: {
239
- code: body.error?.code ?? `HTTP_${res.status}`,
240
- message: body.error?.message ?? `Request failed (${res.status})`
241
- }
242
- };
243
- } catch {
244
- return {
245
- type: "error",
246
- data: {
247
- code: `HTTP_${res.status}`,
248
- message: `Request failed (${res.status})`
249
- }
250
- };
251
- }
252
- }
253
- function parseRestoredProposal(raw) {
254
- if (!raw || typeof raw !== "object") return null;
255
- const r = raw;
256
- if (typeof r.proposalId !== "string" || r.proposalId === "") return null;
257
- if (typeof r.callId !== "string" || r.callId === "") return null;
258
- if (typeof r.name !== "string" || r.name === "") return null;
259
- const requirements = Array.isArray(r.requirements) ? r.requirements.map(parseRequirement).filter((x) => x !== null) : void 0;
260
- return {
261
- proposalId: r.proposalId,
262
- callId: r.callId,
263
- name: r.name,
264
- args: r.args,
265
- ...requirements ? { requirements } : {}
266
- };
267
- }
268
- function createAssistantClient(config) {
269
- const base = config.baseUrl.replace(/\/+$/, "");
270
- const credentials = config.credentials ?? "include";
271
- const authHeaders = () => config.headers?.() ?? {};
272
- const url = (path) => `${base}${path}`;
273
- async function postJson(path, body) {
274
- try {
275
- const res = await fetch(url(path), {
276
- method: "POST",
277
- headers: { ...authHeaders(), "Content-Type": "application/json" },
278
- credentials,
279
- body: JSON.stringify(body)
280
- });
281
- const json = await res.json();
282
- if (!res.ok) {
283
- return {
284
- success: false,
285
- error: json?.error?.message || `HTTP ${res.status}`
286
- };
287
- }
288
- return { success: true, data: json?.data ?? json };
289
- } catch (err) {
290
- return {
291
- success: false,
292
- error: err instanceof Error ? err.message : "Request failed"
293
- };
294
- }
295
- }
296
- return {
297
- async fetchModels(signal) {
298
- try {
299
- const res = await fetch(url("/models"), {
300
- method: "GET",
301
- headers: authHeaders(),
302
- credentials,
303
- signal
304
- });
305
- if (!res.ok) return { ok: false, data: EMPTY_MODELS };
306
- const body = await res.json();
307
- if (!Array.isArray(body.models))
308
- return { ok: false, data: EMPTY_MODELS };
309
- const models = [];
310
- for (const m of body.models) {
311
- const slug = typeof m.slug === "string" ? m.slug : null;
312
- if (!slug) continue;
313
- const label = typeof m.label === "string" ? m.label : slug;
314
- const option = { slug, label };
315
- if (typeof m.promptUsdPerMillion === "number") {
316
- option.promptUsdPerMillion = m.promptUsdPerMillion;
317
- }
318
- if (typeof m.contextTokens === "number") {
319
- option.contextTokens = m.contextTokens;
320
- }
321
- models.push(option);
322
- }
323
- return {
324
- // Empty ⇒ catalog unavailable: report not-ok so the caller retries next
325
- // mount instead of caching an empty picker for the session.
326
- ok: models.length > 0,
327
- data: {
328
- default: typeof body.default === "string" ? body.default : null,
329
- models
330
- }
331
- };
332
- } catch {
333
- return { ok: false, data: EMPTY_MODELS };
334
- }
335
- },
336
- async fetchThreads(signal) {
337
- try {
338
- const res = await fetch(url("/threads"), {
339
- method: "GET",
340
- headers: authHeaders(),
341
- credentials,
342
- signal
343
- });
344
- if (!res.ok) return null;
345
- const body = await res.json();
346
- if (!Array.isArray(body.threads)) return null;
347
- const out = [];
348
- for (const t of body.threads) {
349
- const id = typeof t.id === "string" ? t.id : null;
350
- if (!id) continue;
351
- out.push({
352
- id,
353
- title: typeof t.title === "string" ? t.title : null,
354
- createdAt: typeof t.createdAt === "string" ? t.createdAt : "",
355
- updatedAt: typeof t.updatedAt === "string" ? t.updatedAt : ""
356
- });
357
- }
358
- return out;
359
- } catch {
360
- return null;
361
- }
362
- },
363
- async fetchThreadHistory(threadId, signal) {
364
- try {
365
- const res = await fetch(
366
- url(`/threads/${encodeURIComponent(threadId)}/messages`),
367
- {
368
- method: "GET",
369
- headers: authHeaders(),
370
- credentials,
371
- signal
372
- }
373
- );
374
- if (res.status === 404) return { status: "gone" };
375
- if (!res.ok) return { status: "error" };
376
- const body = await res.json();
377
- if (!Array.isArray(body.messages)) return { status: "error" };
378
- const out = [];
379
- for (const m of body.messages) {
380
- const id = typeof m.id === "string" ? m.id : null;
381
- const role = m.role === "user" || m.role === "assistant" ? m.role : null;
382
- const text = typeof m.text === "string" ? m.text : null;
383
- if (id && role && text != null) out.push({ id, role, text });
384
- }
385
- const proposals = [];
386
- if (Array.isArray(body.proposals)) {
387
- for (const p of body.proposals) {
388
- const parsed = parseRestoredProposal(p);
389
- if (parsed) proposals.push(parsed);
390
- }
391
- }
392
- return { status: "ok", messages: out, proposals };
393
- } catch {
394
- if (signal?.aborted) return { status: "error" };
395
- return { status: "error" };
396
- }
397
- },
398
- async streamChat(req, onEvent, signal) {
399
- const res = await fetch(url("/chat"), {
400
- method: "POST",
401
- headers: {
402
- ...authHeaders(),
403
- "Content-Type": "application/json",
404
- Accept: "text/event-stream"
405
- },
406
- credentials,
407
- body: JSON.stringify(req),
408
- signal
409
- });
410
- if (!res.ok) {
411
- onEvent(await readErrorEvent(res));
412
- return;
413
- }
414
- if (!res.body) {
415
- onEvent({
416
- type: "error",
417
- data: {
418
- code: "NO_BODY",
419
- message: "The assistant stream is unavailable"
420
- }
421
- });
422
- return;
423
- }
424
- let settled = false;
425
- await readSSEEvents(res.body, (frame) => {
426
- const ev = toStreamEvent(frame.eventType ?? null, frame.data);
427
- if (!ev) return;
428
- if (ev.type === "done" || ev.type === "error") settled = true;
429
- onEvent(ev);
430
- });
431
- if (!settled) {
432
- onEvent({
433
- type: "error",
434
- data: {
435
- code: "STREAM_CLOSED",
436
- message: "The assistant stream ended unexpectedly"
437
- }
438
- });
439
- }
440
- },
441
- async confirmProposal(proposalId) {
442
- const res = await postJson("/tools/execute", { proposalId });
443
- if (!res.success) {
444
- return {
445
- ok: false,
446
- error: res.error ?? "The action could not be completed"
447
- };
448
- }
449
- const body = res.data;
450
- if (body?.success) {
451
- return { ok: true, output: body.output, retryable: body.retryable };
452
- }
453
- return {
454
- ok: false,
455
- error: body?.error?.message ?? "The action could not be completed"
456
- };
457
- },
458
- async deleteThread(threadId) {
459
- try {
460
- const res = await fetch(url(`/threads/${encodeURIComponent(threadId)}`), {
461
- method: "DELETE",
462
- headers: authHeaders(),
463
- credentials
464
- });
465
- return { ok: res.ok || res.status === 404 };
466
- } catch {
467
- return { ok: false };
468
- }
469
- }
470
- };
471
- }
472
-
473
- // src/assistant/client-context.tsx
474
- import { createContext, useContext } from "react";
475
- import { jsx } from "react/jsx-runtime";
476
- var AssistantClientContext = createContext(null);
477
- function AssistantClientProvider({
478
- client,
479
- children
480
- }) {
481
- return /* @__PURE__ */ jsx(AssistantClientContext.Provider, { value: client, children });
482
- }
483
- function useAssistantClient() {
484
- const client = useContext(AssistantClientContext);
485
- if (!client) {
486
- throw new Error(
487
- "useAssistantClient must be used within an <AssistantClientProvider>"
488
- );
489
- }
490
- return client;
491
- }
492
-
493
- // src/assistant/useAssistantChat.ts
494
- import { useCallback, useEffect, useReducer, useRef, useState } from "react";
495
-
496
- // src/assistant/persistence.ts
497
- var VERSION = "v1";
498
- function keyFor(userId) {
499
- return userId ? `assistant:${VERSION}:${userId}` : null;
500
- }
501
- function loadThread(userId) {
502
- const key = keyFor(userId);
503
- if (!key) return { threadId: null, model: null };
504
- try {
505
- const raw = localStorage.getItem(key);
506
- if (!raw) return { threadId: null, model: null };
507
- const parsed = JSON.parse(raw);
508
- return {
509
- threadId: typeof parsed.threadId === "string" ? parsed.threadId : null,
510
- model: typeof parsed.model === "string" ? parsed.model : null
511
- };
512
- } catch {
513
- return { threadId: null, model: null };
514
- }
515
- }
516
- function saveThread(userId, thread) {
517
- const key = keyFor(userId);
518
- if (!key) return;
519
- try {
520
- localStorage.setItem(
521
- key,
522
- JSON.stringify({ threadId: thread.threadId, model: thread.model })
523
- );
524
- } catch {
525
- }
526
- }
527
-
528
- // src/assistant/presentation.ts
529
- var LOW_BALANCE_THRESHOLD = 1;
530
- var ADD_CREDITS_CTA = { label: "Add credits", to: "/app/billing" };
531
- var CONNECT_CTA = {
532
- label: "Connect an integration",
533
- to: "/app/integrations"
534
- };
535
- function presentError(code, message) {
536
- switch (code) {
537
- case "INSUFFICIENT_BALANCE":
538
- return {
539
- message: "You're out of credits. Add credits to keep using the assistant.",
540
- cta: ADD_CREDITS_CTA
541
- };
542
- case "MODEL_ACCESS_UNCONFIGURED":
543
- return {
544
- message: "Model access isn't configured for your account yet. Please contact support.",
545
- cta: null
546
- };
547
- case "BILLING_UNAVAILABLE":
548
- return {
549
- message: "Billing is temporarily unavailable. Try again in a moment.",
550
- cta: null
551
- };
552
- case "TOO_MANY_STREAMS":
553
- return {
554
- message: "You have too many assistant requests in flight. Wait a moment and retry.",
555
- cta: null
556
- };
557
- case "THREAD_BUSY":
558
- case "TURN_IN_PROGRESS":
559
- return {
560
- message: "A previous request is still finishing. Try again shortly.",
561
- cta: null
562
- };
563
- case "INTEGRATION_DISCONNECTED":
564
- return {
565
- message: `${message} Connect the integration, then ask again.`,
566
- cta: CONNECT_CTA
567
- };
568
- case "TOOL_FAILED":
569
- case "NETWORK":
570
- return { message: message || "Something went wrong.", cta: null };
571
- default:
572
- return { message: message || "Something went wrong.", cta: null };
573
- }
574
- }
575
- function asRecord(args) {
576
- return args && typeof args === "object" && !Array.isArray(args) ? args : {};
577
- }
578
- function str(v) {
579
- if (v == null) return "";
580
- return typeof v === "string" ? v : JSON.stringify(v);
581
- }
582
- function nonEmptyStr(v) {
583
- return typeof v === "string" && v.trim() !== "" ? v : null;
584
- }
585
- function parseProposalSkills(v) {
586
- if (!Array.isArray(v) || v.length === 0) return void 0;
587
- const out = [];
588
- for (const item of v) {
589
- const rec = asRecord(item);
590
- const name = nonEmptyStr(rec.name);
591
- if (!name) continue;
592
- out.push({ name, description: nonEmptyStr(rec.description) });
593
- }
594
- return out.length > 0 ? out : void 0;
595
- }
596
- function humanizeToolName(name) {
597
- const spaced = name.replace(/_/g, " ").trim();
598
- return spaced.charAt(0).toUpperCase() + spaced.slice(1);
599
- }
600
- var TOOL_ACTIVITY_LABELS = {
601
- get_workflow_schema: "Reading the workflow format",
602
- list_workflows: "Listing workflows",
603
- get_workflow: "Reading workflow",
604
- validate_workflow: "Validating workflow",
605
- list_skills: "Listing skills",
606
- get_skill: "Reading skill",
607
- list_integrations: "Checking integrations",
608
- get_credit_balance: "Checking balance",
609
- get_usage: "Checking usage",
610
- list_api_keys: "Listing API keys"
611
- };
612
- function describeToolActivity(name) {
613
- return TOOL_ACTIVITY_LABELS[name] ?? humanizeToolName(name);
614
- }
615
- function describeProposal(proposal) {
616
- const args = asRecord(proposal.args);
617
- const workflowYaml = nonEmptyStr(args.yaml);
618
- const workflowPreview = workflowYaml ? {
619
- label: "Workflow definition",
620
- content: workflowYaml,
621
- kind: "workflow"
622
- } : null;
623
- switch (proposal.name) {
624
- case "create_workflow":
625
- return { title: "Create workflow", preview: workflowPreview, fields: [] };
626
- // author_workflow creates a workflow PLUS the new skills it needs in one
627
- // unit; show the YAML and name each new skill rather than dumping the raw
628
- // skills JSON (the card is the canonical, readable view of the proposal).
629
- case "author_workflow":
630
- return {
631
- title: "Create workflow",
632
- preview: workflowPreview,
633
- fields: [],
634
- skills: parseProposalSkills(args.skills)
635
- };
636
- case "update_workflow":
637
- return {
638
- title: "Update workflow",
639
- preview: workflowPreview,
640
- fields: [{ label: "Workflow id", value: str(args.id) }]
641
- };
642
- case "set_workflow_enabled":
643
- return {
644
- title: args.enabled ? "Enable workflow" : "Disable workflow",
645
- preview: null,
646
- fields: [{ label: "Workflow id", value: str(args.id) }]
647
- };
648
- case "create_skill": {
649
- const prompt = nonEmptyStr(args.systemPrompt);
650
- const fields = [
651
- { label: "Name", value: str(args.name) }
652
- ];
653
- if (nonEmptyStr(args.description))
654
- fields.push({ label: "Description", value: str(args.description) });
655
- return {
656
- title: "Create skill",
657
- preview: prompt ? { label: "Instructions", content: prompt, kind: "text" } : null,
658
- fields
659
- };
660
- }
661
- case "update_skill": {
662
- const prompt = nonEmptyStr(args.systemPrompt);
663
- const fields = [
664
- { label: "Skill id", value: str(args.id) }
665
- ];
666
- if (nonEmptyStr(args.name))
667
- fields.push({ label: "Name", value: str(args.name) });
668
- if (nonEmptyStr(args.description))
669
- fields.push({ label: "Description", value: str(args.description) });
670
- return {
671
- title: "Update skill",
672
- preview: prompt ? { label: "Instructions", content: prompt, kind: "text" } : null,
673
- fields
674
- };
675
- }
676
- case "delete_skill":
677
- return {
678
- title: "Delete skill",
679
- preview: null,
680
- fields: [{ label: "Skill id", value: str(args.id) }]
681
- };
682
- case "create_api_key": {
683
- const fields = [
684
- { label: "Name", value: str(args.name) }
685
- ];
686
- if (args.product != null)
687
- fields.push({ label: "Product", value: str(args.product) });
688
- if (args.budgetUsd != null)
689
- fields.push({ label: "Budget (USD)", value: str(args.budgetUsd) });
690
- return { title: "Create API key", preview: null, fields };
691
- }
692
- case "revoke_api_key":
693
- return {
694
- title: "Revoke API key",
695
- preview: null,
696
- fields: [{ label: "Key id", value: str(args.keyId) }]
697
- };
698
- case "invoke_integration": {
699
- const fields = [
700
- { label: "Action", value: str(args.path) }
701
- ];
702
- if (args.input != null)
703
- fields.push({ label: "Input", value: str(args.input) });
704
- return { title: "Run integration action", preview: null, fields };
705
- }
706
- default: {
707
- const fields = Object.entries(args).map(([label, value]) => ({
708
- label,
709
- value: str(value)
710
- }));
711
- return { title: humanizeToolName(proposal.name), preview: null, fields };
712
- }
713
- }
714
- }
715
- function describeOutcome(name, output) {
716
- const o = asRecord(output);
717
- switch (name) {
718
- case "author_workflow":
719
- case "create_workflow": {
720
- const wf = asRecord(o.workflow);
721
- const skillCount = Array.isArray(o.skills) ? o.skills.length : 0;
722
- if (wf.name) {
723
- return skillCount > 0 ? `Created workflow "${str(wf.name)}" and ${skillCount} skill${skillCount === 1 ? "" : "s"}.` : `Created workflow "${str(wf.name)}".`;
724
- }
725
- return "Workflow created.";
726
- }
727
- case "update_workflow": {
728
- const wf = asRecord(o.workflow);
729
- return wf.name ? `Updated workflow "${str(wf.name)}".` : "Workflow updated.";
730
- }
731
- case "set_workflow_enabled": {
732
- const wf = asRecord(o.workflow);
733
- return wf.enabled ? "Workflow enabled." : "Workflow disabled.";
734
- }
735
- case "create_api_key":
736
- return o.prefix ? `Created API key (${str(o.prefix)}\u2026). Copy it from the API Keys page.` : "API key created.";
737
- case "revoke_api_key":
738
- return "API key revoked.";
739
- case "invoke_integration":
740
- return "Integration action completed.";
741
- default:
742
- return "Action completed.";
743
- }
744
- }
745
- function describeFailure(output) {
746
- const o = asRecord(output);
747
- const errors = Array.isArray(o.errors) ? o.errors : [];
748
- const joined = errors.map((e) => typeof e === "string" ? e : str(asRecord(e).message)).filter((m) => m.length > 0).join("; ");
749
- if (joined) return joined;
750
- if (typeof o.message === "string" && o.message) return o.message;
751
- if (o.notFound === true) return "That no longer exists.";
752
- if (o.conflict === true) return "It changed since it was loaded. Try again.";
753
- return "The action could not be completed.";
754
- }
755
- function isLowBalance(balanceUsd) {
756
- return balanceUsd != null && balanceUsd < LOW_BALANCE_THRESHOLD;
757
- }
758
- function resolveConfirmation(name, result) {
759
- if (result.ok) {
760
- const out = asRecord(result.output);
761
- if (out.ok === false && out.code === "NOT_CONNECTED") {
762
- return {
763
- statusText: null,
764
- error: {
765
- code: "INTEGRATION_DISCONNECTED",
766
- message: str(out.message) || "That integration isn't connected."
767
- }
768
- };
769
- }
770
- if (out.created === false || out.updated === false || out.deleted === false || out.ok === false) {
771
- return {
772
- statusText: null,
773
- error: { code: "TOOL_FAILED", message: describeFailure(result.output) }
774
- };
775
- }
776
- return { statusText: describeOutcome(name, result.output), error: null };
777
- }
778
- return {
779
- statusText: null,
780
- error: { code: "TOOL_FAILED", message: result.error }
781
- };
782
- }
783
-
784
- // src/assistant/reducer.ts
785
- var MAX_MESSAGES = 200;
786
- function capMessages(messages) {
787
- return messages.length > MAX_MESSAGES ? messages.slice(-MAX_MESSAGES) : messages;
788
- }
789
- function initialAssistantState() {
790
- return {
791
- ownerId: null,
792
- threadId: null,
793
- messages: [],
794
- status: "idle",
795
- streamingId: null,
796
- streamBaseId: null,
797
- segmentSeq: 0,
798
- pendingProposals: [],
799
- usage: null,
800
- model: null,
801
- reasoning: null,
802
- error: null
803
- };
804
- }
805
- function selectVisibleState(state, userId) {
806
- return state.ownerId === userId ? state : initialAssistantState();
807
- }
808
- function dropEmptyStreaming(messages, streamingId) {
809
- if (!streamingId) return messages;
810
- const msg = messages.find((m) => m.id === streamingId);
811
- if (msg && msg.role === "assistant" && msg.text === "") {
812
- return messages.filter((m) => m.id !== streamingId);
813
- }
814
- return messages;
815
- }
816
- function appendDelta(messages, streamingId, text) {
817
- if (!streamingId) return messages;
818
- return messages.map(
819
- (m) => m.id === streamingId ? { ...m, text: m.text + text } : m
820
- );
821
- }
822
- function applyStreamEvent(state, event) {
823
- switch (event.type) {
824
- case "thread":
825
- return {
826
- ...state,
827
- threadId: event.data.threadId,
828
- // The model the server actually ran this turn against (lets the picker
829
- // reflect reality even when the user never explicitly chose one).
830
- model: event.data.model ?? state.model
831
- };
832
- case "delta": {
833
- if (state.streamingId) {
834
- return {
835
- ...state,
836
- messages: appendDelta(
837
- state.messages,
838
- state.streamingId,
839
- event.data.text
840
- )
841
- };
842
- }
843
- if (!state.streamBaseId) return state;
844
- const segmentSeq = state.segmentSeq + 1;
845
- const id = `${state.streamBaseId}-s${segmentSeq}`;
846
- return {
847
- ...state,
848
- segmentSeq,
849
- streamingId: id,
850
- messages: capMessages([
851
- ...state.messages,
852
- { id, role: "assistant", text: event.data.text }
853
- ])
854
- };
855
- }
856
- case "reasoning":
857
- return {
858
- ...state,
859
- reasoning: (state.reasoning ?? "") + event.data.text
860
- };
861
- case "tool_call": {
862
- const trimmed = dropEmptyStreaming(state.messages, state.streamingId);
863
- const chipId = `tool-${event.data.callId}`;
864
- if (trimmed.some((m) => m.id === chipId)) {
865
- return { ...state, messages: trimmed, streamingId: null };
866
- }
867
- const chip = {
868
- id: chipId,
869
- role: "tool",
870
- text: "",
871
- tool: {
872
- name: event.data.name,
873
- status: "running",
874
- args: event.data.args
875
- }
876
- };
877
- return {
878
- ...state,
879
- streamingId: null,
880
- messages: capMessages([...trimmed, chip])
881
- };
882
- }
883
- case "tool_result": {
884
- const chipId = `tool-${event.data.callId}`;
885
- const status = event.data.ok ? "ok" : "failed";
886
- const errText = event.data.ok ? "" : event.data.error?.message ?? "unknown error";
887
- const outcome = event.data.ok ? { ok: true, result: event.data.output } : { ok: false, error: event.data.error };
888
- if (state.messages.some((m) => m.id === chipId)) {
889
- return {
890
- ...state,
891
- messages: state.messages.map(
892
- (m) => m.id === chipId ? {
893
- ...m,
894
- text: errText,
895
- // Preserve the args the matching tool_call recorded — the
896
- // result event doesn't carry them.
897
- tool: {
898
- name: event.data.name,
899
- status,
900
- args: m.tool?.args,
901
- outcome
902
- }
903
- } : m
904
- )
905
- };
906
- }
907
- const chip = {
908
- id: chipId,
909
- role: "tool",
910
- text: errText,
911
- tool: { name: event.data.name, status, outcome }
912
- };
913
- return { ...state, messages: capMessages([...state.messages, chip]) };
914
- }
915
- case "tool_proposal": {
916
- if (state.pendingProposals.some((p) => p.callId === event.data.callId)) {
917
- return state;
918
- }
919
- return {
920
- ...state,
921
- pendingProposals: [...state.pendingProposals, event.data]
922
- };
923
- }
924
- case "usage":
925
- return {
926
- ...state,
927
- usage: {
928
- costUsd: event.data.costUsd,
929
- balanceUsd: event.data.balanceUsd,
930
- promptTokens: event.data.promptTokens,
931
- completionTokens: event.data.completionTokens,
932
- durationMs: event.data.durationMs ?? null,
933
- replayed: event.data.replayed ?? false
934
- }
935
- };
936
- case "done": {
937
- let messages = dropEmptyStreaming(state.messages, state.streamingId);
938
- if (event.data.capped) {
939
- messages = capMessages([
940
- ...messages,
941
- {
942
- id: `cap-${event.data.turnId}`,
943
- role: "status",
944
- text: "I reached the step limit for this turn. Ask me to continue and I'll pick up where I left off."
945
- }
946
- ]);
947
- }
948
- return {
949
- ...state,
950
- messages,
951
- streamingId: null,
952
- status: state.pendingProposals.length > 0 ? "awaiting_confirm" : "idle"
953
- };
954
- }
955
- case "error": {
956
- const messages = dropEmptyStreaming(state.messages, state.streamingId);
957
- return {
958
- ...state,
959
- messages,
960
- streamingId: null,
961
- status: "idle",
962
- // A turn that didn't complete cleanly leaves no confirmable action: a
963
- // proposal buffered before the failure must not stay actionable.
964
- pendingProposals: [],
965
- error: { code: event.data.code, message: event.data.message }
966
- };
967
- }
968
- }
969
- }
970
- function assistantReducer(state, action) {
971
- switch (action.type) {
972
- case "send":
973
- return {
974
- ...state,
975
- messages: capMessages([
976
- ...state.messages,
977
- { id: action.messageId, role: "user", text: action.text },
978
- { id: action.assistantId, role: "assistant", text: "" }
979
- ]),
980
- status: "streaming",
981
- streamingId: action.assistantId,
982
- // The first bubble is segment 0; tool-finalized segments derive their id
983
- // from this base, so they stay unique across turns.
984
- streamBaseId: action.assistantId,
985
- segmentSeq: 0,
986
- usage: null,
987
- reasoning: null,
988
- error: null
989
- };
990
- case "stream":
991
- return applyStreamEvent(state, action.event);
992
- case "stream_failed": {
993
- const messages = dropEmptyStreaming(state.messages, state.streamingId);
994
- return {
995
- ...state,
996
- messages,
997
- streamingId: null,
998
- status: "idle",
999
- // A failed turn leaves no confirmable action (see the `error` case).
1000
- pendingProposals: [],
1001
- error: action.error
1002
- };
1003
- }
1004
- case "proposal_resolved": {
1005
- const pendingProposals = state.pendingProposals.filter(
1006
- (p) => p.callId !== action.callId
1007
- );
1008
- const messages = action.status ? capMessages([...state.messages, action.status]) : state.messages;
1009
- return {
1010
- ...state,
1011
- messages,
1012
- pendingProposals,
1013
- error: action.error,
1014
- status: pendingProposals.length > 0 ? "awaiting_confirm" : state.status === "awaiting_confirm" ? "idle" : state.status
1015
- };
1016
- }
1017
- case "proposal_retry_failed": {
1018
- const pendingProposals = state.pendingProposals.map(
1019
- (p) => p.callId === action.callId ? { ...p, retryError: action.message } : p
1020
- );
1021
- return {
1022
- ...state,
1023
- pendingProposals,
1024
- status: pendingProposals.length > 0 ? "awaiting_confirm" : state.status === "awaiting_confirm" ? "idle" : state.status
1025
- };
1026
- }
1027
- case "stopped":
1028
- return {
1029
- ...state,
1030
- messages: dropEmptyStreaming(state.messages, state.streamingId),
1031
- status: "idle",
1032
- streamingId: null,
1033
- pendingProposals: []
1034
- };
1035
- case "hydrate":
1036
- return {
1037
- ...initialAssistantState(),
1038
- ownerId: action.ownerId,
1039
- threadId: action.threadId,
1040
- messages: action.messages
1041
- };
1042
- case "restore_history":
1043
- if (state.ownerId !== action.ownerId || state.threadId !== action.threadId || state.status !== "idle" || state.messages.length > 0 || state.pendingProposals.length > 0) {
1044
- return state;
1045
- }
1046
- return {
1047
- ...state,
1048
- messages: capMessages(action.messages),
1049
- pendingProposals: action.proposals,
1050
- status: action.proposals.length > 0 ? "awaiting_confirm" : state.status
1051
- };
1052
- case "thread_gone":
1053
- if (state.ownerId !== action.ownerId || state.threadId !== action.threadId || state.status !== "idle" || state.messages.length > 0) {
1054
- return state;
1055
- }
1056
- return { ...state, threadId: null };
1057
- case "history_failed":
1058
- if (state.ownerId !== action.ownerId || state.threadId !== action.threadId || state.status !== "idle" || state.messages.length > 0) {
1059
- return state;
1060
- }
1061
- return {
1062
- ...initialAssistantState(),
1063
- ownerId: state.ownerId,
1064
- error: action.error
1065
- };
1066
- case "switch_thread":
1067
- if (state.threadId === action.threadId) return state;
1068
- return {
1069
- ...initialAssistantState(),
1070
- ownerId: state.ownerId,
1071
- threadId: action.threadId
1072
- };
1073
- case "reset":
1074
- return { ...initialAssistantState(), ownerId: state.ownerId };
1075
- }
1076
- }
1077
-
1078
- // src/assistant/useAssistantChat.ts
1079
- var EMPTY_IDS = /* @__PURE__ */ new Set();
1080
- var WORKFLOW_MUTATING_TOOLS = /* @__PURE__ */ new Set([
1081
- "create_workflow",
1082
- "author_workflow",
1083
- "update_workflow",
1084
- "set_workflow_enabled"
1085
- ]);
1086
- function statusMessage(text) {
1087
- return { id: `status-${uuid()}`, role: "status", text };
1088
- }
1089
- function uuid() {
1090
- return crypto.randomUUID();
1091
- }
1092
- function useAssistantChat(userId, options) {
1093
- const [state, dispatch] = useReducer(
1094
- assistantReducer,
1095
- userId,
1096
- (uid) => {
1097
- return {
1098
- ...initialAssistantState(),
1099
- ownerId: uid,
1100
- threadId: loadThread(uid).threadId
1101
- };
1102
- }
1103
- );
1104
- const abortRef = useRef(null);
1105
- const historyAbortRef = useRef(null);
1106
- const hydratedUserRef = useRef(userId);
1107
- const streamSeqRef = useRef(0);
1108
- const confirmSeqRef = useRef(0);
1109
- const stateRef = useRef(state);
1110
- const userIdRef = useRef(userId);
1111
- stateRef.current = state;
1112
- userIdRef.current = userId;
1113
- const client = useAssistantClient();
1114
- const clientRef = useRef(client);
1115
- clientRef.current = client;
1116
- const onWorkflowMutationRef = useRef(options?.onWorkflowMutation);
1117
- onWorkflowMutationRef.current = options?.onWorkflowMutation;
1118
- const confirmingRef = useRef(/* @__PURE__ */ new Set());
1119
- const [confirmingIds, setConfirmingIds] = useState(EMPTY_IDS);
1120
- const sendingRef = useRef(false);
1121
- const restoringRef = useRef(false);
1122
- const [restoring, setRestoring] = useState(false);
1123
- const setRestoringBoth = useCallback((v) => {
1124
- restoringRef.current = v;
1125
- setRestoring(v);
1126
- }, []);
1127
- const [selectedModel, setSelectedModel] = useState(
1128
- () => loadThread(userId).model
1129
- );
1130
- const selectedModelRef = useRef(selectedModel);
1131
- selectedModelRef.current = selectedModel;
1132
- useEffect(() => {
1133
- if (hydratedUserRef.current === userId) return;
1134
- streamSeqRef.current += 1;
1135
- confirmSeqRef.current += 1;
1136
- abortRef.current?.abort();
1137
- historyAbortRef.current?.abort();
1138
- setRestoringBoth(false);
1139
- sendingRef.current = false;
1140
- confirmingRef.current.clear();
1141
- setConfirmingIds(EMPTY_IDS);
1142
- hydratedUserRef.current = userId;
1143
- const nextModel = loadThread(userId).model;
1144
- selectedModelRef.current = nextModel;
1145
- setSelectedModel(nextModel);
1146
- dispatch({
1147
- type: "hydrate",
1148
- ownerId: userId,
1149
- threadId: loadThread(userId).threadId,
1150
- messages: []
1151
- });
1152
- }, [userId, setRestoringBoth]);
1153
- useEffect(() => {
1154
- const threadId = loadThread(userId).threadId;
1155
- if (!userId || !threadId) return;
1156
- const ac = new AbortController();
1157
- historyAbortRef.current?.abort();
1158
- historyAbortRef.current = ac;
1159
- void clientRef.current.fetchThreadHistory(threadId, ac.signal).then((result) => {
1160
- if (ac.signal.aborted) return;
1161
- if (result.status === "ok") {
1162
- if (result.messages.length > 0 || result.proposals.length > 0) {
1163
- dispatch({
1164
- type: "restore_history",
1165
- ownerId: userId,
1166
- threadId,
1167
- messages: result.messages,
1168
- proposals: result.proposals
1169
- });
1170
- }
1171
- } else if (result.status === "gone") {
1172
- dispatch({ type: "thread_gone", ownerId: userId, threadId });
1173
- }
1174
- }).catch(() => {
1175
- });
1176
- return () => ac.abort();
1177
- }, [userId]);
1178
- useEffect(() => {
1179
- if (state.status === "streaming") return;
1180
- if (state.ownerId !== userIdRef.current) return;
1181
- saveThread(state.ownerId, {
1182
- threadId: state.threadId,
1183
- model: selectedModelRef.current
1184
- });
1185
- }, [state.ownerId, state.threadId, state.status]);
1186
- useEffect(() => {
1187
- return () => abortRef.current?.abort();
1188
- }, []);
1189
- const send = useCallback((message) => {
1190
- if (sendingRef.current) return;
1191
- const text = message.trim();
1192
- const current = stateRef.current;
1193
- if (current.ownerId !== userIdRef.current) return;
1194
- if (restoringRef.current) return;
1195
- if (!text || current.status === "streaming") return;
1196
- if (current.pendingProposals.length > 0) return;
1197
- dispatch({
1198
- type: "send",
1199
- messageId: uuid(),
1200
- assistantId: uuid(),
1201
- text
1202
- });
1203
- const seq = ++streamSeqRef.current;
1204
- const ac = new AbortController();
1205
- abortRef.current = ac;
1206
- sendingRef.current = true;
1207
- clientRef.current.streamChat(
1208
- {
1209
- message: text,
1210
- // null → omit, so the server applies its default model.
1211
- model: selectedModelRef.current ?? void 0,
1212
- threadId: current.threadId ?? void 0,
1213
- turnKey: uuid()
1214
- },
1215
- (event) => {
1216
- if (streamSeqRef.current === seq) dispatch({ type: "stream", event });
1217
- },
1218
- ac.signal
1219
- ).catch((err) => {
1220
- if (ac.signal.aborted || streamSeqRef.current !== seq) return;
1221
- dispatch({
1222
- type: "stream_failed",
1223
- error: {
1224
- code: "NETWORK",
1225
- message: err instanceof Error ? err.message : "The connection failed"
1226
- }
1227
- });
1228
- }).finally(() => {
1229
- if (streamSeqRef.current === seq) sendingRef.current = false;
1230
- });
1231
- }, []);
1232
- const stop = useCallback(() => {
1233
- streamSeqRef.current += 1;
1234
- abortRef.current?.abort();
1235
- sendingRef.current = false;
1236
- dispatch({ type: "stopped" });
1237
- }, []);
1238
- const confirm = useCallback(async (proposal) => {
1239
- const pid = proposal.proposalId;
1240
- if (!pid) {
1241
- dispatch({
1242
- type: "proposal_resolved",
1243
- callId: proposal.callId,
1244
- status: null,
1245
- error: {
1246
- code: "TOOL_FAILED",
1247
- message: "This action can no longer be confirmed."
1248
- }
1249
- });
1250
- return;
1251
- }
1252
- if (confirmingRef.current.has(pid)) return;
1253
- confirmingRef.current.add(pid);
1254
- setConfirmingIds(new Set(confirmingRef.current));
1255
- const seq = confirmSeqRef.current;
1256
- try {
1257
- const result = await clientRef.current.confirmProposal(pid);
1258
- if (confirmSeqRef.current !== seq) return;
1259
- if (result.ok && result.retryable) {
1260
- const { error: error2 } = resolveConfirmation(proposal.name, result);
1261
- dispatch({
1262
- type: "proposal_retry_failed",
1263
- callId: proposal.callId,
1264
- message: error2?.message ?? "Connect the required integration, then confirm again."
1265
- });
1266
- return;
1267
- }
1268
- const { statusText, error } = resolveConfirmation(proposal.name, result);
1269
- dispatch({
1270
- type: "proposal_resolved",
1271
- callId: proposal.callId,
1272
- status: statusText ? statusMessage(statusText) : null,
1273
- error
1274
- });
1275
- if (!error && WORKFLOW_MUTATING_TOOLS.has(proposal.name)) {
1276
- onWorkflowMutationRef.current?.();
1277
- }
1278
- } catch (err) {
1279
- if (confirmSeqRef.current !== seq) return;
1280
- dispatch({
1281
- type: "proposal_resolved",
1282
- callId: proposal.callId,
1283
- status: null,
1284
- error: {
1285
- code: "TOOL_FAILED",
1286
- message: err instanceof Error ? err.message : "The action could not be completed"
1287
- }
1288
- });
1289
- } finally {
1290
- if (confirmSeqRef.current === seq) {
1291
- confirmingRef.current.delete(pid);
1292
- setConfirmingIds(new Set(confirmingRef.current));
1293
- }
1294
- }
1295
- }, []);
1296
- const cancel = useCallback((proposal) => {
1297
- if (proposal.proposalId && confirmingRef.current.has(proposal.proposalId)) {
1298
- return;
1299
- }
1300
- dispatch({
1301
- type: "proposal_resolved",
1302
- callId: proposal.callId,
1303
- status: statusMessage("Action cancelled."),
1304
- error: null
1305
- });
1306
- }, []);
1307
- const reset = useCallback(() => {
1308
- streamSeqRef.current += 1;
1309
- confirmSeqRef.current += 1;
1310
- abortRef.current?.abort();
1311
- historyAbortRef.current?.abort();
1312
- setRestoringBoth(false);
1313
- sendingRef.current = false;
1314
- confirmingRef.current.clear();
1315
- setConfirmingIds(EMPTY_IDS);
1316
- dispatch({ type: "reset" });
1317
- }, [setRestoringBoth]);
1318
- const switchThread = useCallback(
1319
- (threadId) => {
1320
- const current = stateRef.current;
1321
- const uid = userIdRef.current;
1322
- if (current.ownerId !== uid) return;
1323
- if (threadId === current.threadId) return;
1324
- if (current.status === "streaming" || current.pendingProposals.length > 0) {
1325
- return;
1326
- }
1327
- streamSeqRef.current += 1;
1328
- confirmSeqRef.current += 1;
1329
- abortRef.current?.abort();
1330
- sendingRef.current = false;
1331
- confirmingRef.current.clear();
1332
- setConfirmingIds(EMPTY_IDS);
1333
- dispatch({ type: "switch_thread", threadId });
1334
- setRestoringBoth(true);
1335
- saveThread(uid, { threadId, model: selectedModelRef.current });
1336
- const ac = new AbortController();
1337
- historyAbortRef.current?.abort();
1338
- historyAbortRef.current = ac;
1339
- void clientRef.current.fetchThreadHistory(threadId, ac.signal).then((result) => {
1340
- if (ac.signal.aborted) return;
1341
- if (result.status === "ok") {
1342
- if (result.messages.length > 0 || result.proposals.length > 0) {
1343
- dispatch({
1344
- type: "restore_history",
1345
- ownerId: uid,
1346
- threadId,
1347
- messages: result.messages,
1348
- proposals: result.proposals
1349
- });
1350
- }
1351
- } else if (result.status === "gone") {
1352
- dispatch({ type: "thread_gone", ownerId: uid, threadId });
1353
- } else {
1354
- dispatch({
1355
- type: "history_failed",
1356
- ownerId: uid,
1357
- threadId,
1358
- error: {
1359
- code: "HISTORY_LOAD_FAILED",
1360
- message: "Couldn't load that conversation. You're in a new chat \u2014 reopen it from history to try again."
1361
- }
1362
- });
1363
- }
1364
- setRestoringBoth(false);
1365
- }).catch(() => {
1366
- if (!ac.signal.aborted) setRestoringBoth(false);
1367
- });
1368
- },
1369
- [setRestoringBoth]
1370
- );
1371
- const setModel = useCallback((model) => {
1372
- selectedModelRef.current = model;
1373
- setSelectedModel(model);
1374
- const currentUserId = userIdRef.current;
1375
- if (stateRef.current.ownerId === currentUserId) {
1376
- saveThread(currentUserId, {
1377
- threadId: stateRef.current.threadId,
1378
- model
1379
- });
1380
- }
1381
- }, []);
1382
- useEffect(() => {
1383
- if (state.error?.code === "MODEL_NOT_ALLOWED" && state.ownerId === userIdRef.current && selectedModelRef.current !== null) {
1384
- setModel(null);
1385
- }
1386
- }, [state.error, state.ownerId, setModel]);
1387
- const visibleState = selectVisibleState(state, userId);
1388
- return {
1389
- state: visibleState,
1390
- confirmingIds,
1391
- selectedModel,
1392
- setModel,
1393
- send,
1394
- stop,
1395
- confirm,
1396
- cancel,
1397
- reset,
1398
- switchThread,
1399
- restoring
1400
- };
1401
- }
1402
-
1403
- // src/assistant/useAssistantModels.ts
1404
- import { useEffect as useEffect2, useReducer as useReducer2 } from "react";
1405
- var EMPTY = { default: null, models: [] };
1406
- var byClient = /* @__PURE__ */ new WeakMap();
1407
- function cacheFor(client) {
1408
- let entry = byClient.get(client);
1409
- if (!entry) {
1410
- entry = { cache: null, inflight: null };
1411
- byClient.set(client, entry);
1412
- }
1413
- return entry;
1414
- }
1415
- function useAssistantModels() {
1416
- const client = useAssistantClient();
1417
- const [, bump] = useReducer2((n) => n + 1, 0);
1418
- useEffect2(() => {
1419
- const entry = cacheFor(client);
1420
- if (entry.cache) return;
1421
- let active = true;
1422
- entry.inflight ??= client.fetchModels();
1423
- void entry.inflight.then((result) => {
1424
- if (result.ok) entry.cache = result.data;
1425
- entry.inflight = null;
1426
- if (active) bump();
1427
- }).catch(() => {
1428
- entry.inflight = null;
1429
- });
1430
- return () => {
1431
- active = false;
1432
- };
1433
- }, [client]);
1434
- return cacheFor(client).cache ?? EMPTY;
1435
- }
1436
-
1437
- // src/assistant/useAssistantThreads.ts
1438
- import { useCallback as useCallback2, useEffect as useEffect3, useRef as useRef2, useState as useState2 } from "react";
1439
- function useAssistantThreads(userId) {
1440
- const client = useAssistantClient();
1441
- const userRef = useRef2(userId);
1442
- userRef.current = userId;
1443
- const clientRef = useRef2(client);
1444
- clientRef.current = client;
1445
- const abortRef = useRef2(null);
1446
- const pendingDeletesRef = useRef2(/* @__PURE__ */ new Set());
1447
- const [state, setState] = useState2(() => ({
1448
- threads: [],
1449
- loading: false,
1450
- loaded: false,
1451
- ownerUserId: userId,
1452
- ownerClient: client
1453
- }));
1454
- const refresh = useCallback2(() => {
1455
- const requestedUserId = userRef.current;
1456
- const requestedClient = clientRef.current;
1457
- if (!requestedUserId) {
1458
- setState({
1459
- threads: [],
1460
- loading: false,
1461
- loaded: true,
1462
- ownerUserId: requestedUserId,
1463
- ownerClient: requestedClient
1464
- });
1465
- return;
1466
- }
1467
- abortRef.current?.abort();
1468
- const ac = new AbortController();
1469
- abortRef.current = ac;
1470
- setState((s) => ({
1471
- ...s,
1472
- loading: true,
1473
- ownerUserId: requestedUserId,
1474
- ownerClient: requestedClient
1475
- }));
1476
- const isCurrent = () => !ac.signal.aborted && userRef.current === requestedUserId && clientRef.current === requestedClient;
1477
- void requestedClient.fetchThreads(ac.signal).then((result) => {
1478
- if (!isCurrent()) return;
1479
- setState((s) => ({
1480
- // null = transient failure: keep the prior list, just drop the spinner.
1481
- // Drop any in-flight/finished deletions so a stale fetch can't resurrect
1482
- // a row we already removed.
1483
- threads: (result ?? s.threads).filter(
1484
- (t) => !pendingDeletesRef.current.has(t.id)
1485
- ),
1486
- loading: false,
1487
- loaded: true,
1488
- ownerUserId: requestedUserId,
1489
- ownerClient: requestedClient
1490
- }));
1491
- }).catch(() => {
1492
- if (isCurrent()) {
1493
- setState((s) => ({ ...s, loading: false, loaded: true }));
1494
- }
1495
- });
1496
- }, []);
1497
- const remove = useCallback2(
1498
- async (threadId) => {
1499
- const requestedClient = clientRef.current;
1500
- const requestedUserId = userRef.current;
1501
- if (!requestedClient.deleteThread) return { ok: false };
1502
- pendingDeletesRef.current.add(threadId);
1503
- setState(
1504
- (s) => s.ownerClient === requestedClient && s.ownerUserId === requestedUserId ? { ...s, threads: s.threads.filter((t) => t.id !== threadId) } : s
1505
- );
1506
- let res;
1507
- try {
1508
- res = await requestedClient.deleteThread(threadId);
1509
- } catch {
1510
- res = { ok: false };
1511
- }
1512
- if (!res.ok) {
1513
- pendingDeletesRef.current.delete(threadId);
1514
- if (userRef.current === requestedUserId && clientRef.current === requestedClient) {
1515
- refresh();
1516
- }
1517
- }
1518
- return res;
1519
- },
1520
- [refresh]
1521
- );
1522
- useEffect3(() => {
1523
- return () => abortRef.current?.abort();
1524
- }, [userId, client]);
1525
- useEffect3(() => {
1526
- return () => pendingDeletesRef.current.clear();
1527
- }, []);
1528
- const stale = state.ownerUserId !== userId || state.ownerClient !== client;
1529
- return {
1530
- threads: stale ? [] : state.threads,
1531
- loading: stale ? false : state.loading,
1532
- loaded: stale ? false : state.loaded,
1533
- refresh,
1534
- remove,
1535
- canRemove: typeof client.deleteThread === "function"
1536
- };
1537
- }
1538
-
1539
- // src/assistant/AssistantDock.tsx
1540
- import { MessageSquare } from "lucide-react";
1541
- import {
1542
- useEffect as useEffect6,
1543
- useRef as useRef6
1544
- } from "react";
1545
-
1546
- // src/assistant/AssistantPanel.tsx
1547
- import { AgentTimeline, ChatInput, ThinkingIndicator } from "@tangle-network/ui/chat";
1548
- import { History, MessageSquarePlus, Minus, Plus, X } from "lucide-react";
1549
- import { useEffect as useEffect5, useMemo as useMemo2, useRef as useRef4, useState as useState6 } from "react";
1550
-
1551
- // src/assistant/AssistantHistory.tsx
1552
- import { Search, Trash2 } from "lucide-react";
1553
- import { useMemo, useState as useState3 } from "react";
1554
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
1555
- function parsedTime(iso) {
1556
- if (!iso) return null;
1557
- const ms = Date.parse(iso);
1558
- return Number.isFinite(ms) ? ms : null;
1559
- }
1560
- function AssistantHistory({
1561
- threads,
1562
- loaded,
1563
- activeThreadId,
1564
- activeBusy,
1565
- canRemove,
1566
- onSelect,
1567
- onDelete
1568
- }) {
1569
- const [query, setQuery] = useState3("");
1570
- const sorted = useMemo(
1571
- () => [...threads].sort((a, b) => {
1572
- const ta = parsedTime(a.updatedAt);
1573
- const tb = parsedTime(b.updatedAt);
1574
- if (ta === null && tb === null) return 0;
1575
- if (ta === null) return 1;
1576
- if (tb === null) return -1;
1577
- return tb - ta;
1578
- }),
1579
- [threads]
1580
- );
1581
- const trimmed = query.trim().toLowerCase();
1582
- const visible = useMemo(
1583
- () => trimmed ? (
1584
- // Match the title as displayed, so searching "untitled" finds the
1585
- // rows that render as "Untitled conversation".
1586
- sorted.filter(
1587
- (t) => (t.title ?? "Untitled conversation").toLowerCase().includes(trimmed)
1588
- )
1589
- ) : sorted,
1590
- [sorted, trimmed]
1591
- );
1592
- return /* @__PURE__ */ jsxs("div", { className: "flex h-full flex-col", children: [
1593
- /* @__PURE__ */ jsx2("div", { className: "border-border border-b p-2", children: /* @__PURE__ */ jsxs("div", { className: "relative", children: [
1594
- /* @__PURE__ */ jsx2(Search, { className: "-translate-y-1/2 pointer-events-none absolute top-1/2 left-2.5 h-3.5 w-3.5 text-muted-foreground" }),
1595
- /* @__PURE__ */ jsx2(
1596
- "input",
1597
- {
1598
- type: "search",
1599
- value: query,
1600
- onChange: (e) => setQuery(e.target.value),
1601
- placeholder: "Search conversations",
1602
- "aria-label": "Search conversations",
1603
- className: "w-full rounded-md border border-border bg-surface-container-high py-1.5 pr-2 pl-8 text-foreground text-sm placeholder:text-muted-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
1604
- }
1605
- )
1606
- ] }) }),
1607
- /* @__PURE__ */ jsx2("div", { className: "min-h-0 flex-1 overflow-y-auto", children: visible.length === 0 ? /* @__PURE__ */ jsx2("p", { className: "px-3 py-6 text-center text-muted-foreground text-xs", children: !loaded ? "Loading\u2026" : trimmed ? "No conversations match your search." : "No past conversations yet." }) : /* @__PURE__ */ jsx2("ul", { className: "py-1", children: visible.map((t) => {
1608
- const active = t.id === activeThreadId;
1609
- const ms = parsedTime(t.updatedAt);
1610
- const busyActive = active && activeBusy;
1611
- const title = t.title ?? "Untitled conversation";
1612
- return /* @__PURE__ */ jsxs(
1613
- "li",
1614
- {
1615
- className: `group flex items-center transition-colors hover:bg-muted/60 ${active ? "bg-primary/10" : ""}`,
1616
- children: [
1617
- /* @__PURE__ */ jsxs(
1618
- "button",
1619
- {
1620
- type: "button",
1621
- onClick: () => onSelect(t.id),
1622
- className: "flex min-w-0 flex-1 flex-col gap-0.5 px-3 py-2 text-left",
1623
- children: [
1624
- /* @__PURE__ */ jsx2(
1625
- "span",
1626
- {
1627
- className: `truncate text-sm ${active ? "font-medium text-foreground" : "text-foreground"}`,
1628
- children: title
1629
- }
1630
- ),
1631
- ms != null && /* @__PURE__ */ jsx2("span", { className: "text-[11px] text-muted-foreground", children: timeAgo(ms) })
1632
- ]
1633
- }
1634
- ),
1635
- canRemove && /* @__PURE__ */ jsx2(
1636
- "button",
1637
- {
1638
- type: "button",
1639
- onClick: () => onDelete(t.id),
1640
- disabled: busyActive,
1641
- "aria-label": `Delete conversation: ${title}`,
1642
- title: busyActive ? "Can't delete while this conversation is active" : "Delete conversation",
1643
- className: "shrink-0 p-2 text-muted-foreground opacity-0 transition [@media(hover:none)]:opacity-100 hover:text-destructive focus-visible:opacity-100 group-hover:opacity-100 disabled:cursor-not-allowed disabled:opacity-30",
1644
- children: /* @__PURE__ */ jsx2(Trash2, { className: "h-3.5 w-3.5" })
1645
- }
1646
- )
1647
- ]
1648
- },
1649
- t.id
1650
- );
1651
- }) }) })
1652
- ] });
1653
- }
1654
-
1655
- // src/assistant/build-timeline.tsx
1656
- import { jsx as jsx3 } from "react/jsx-runtime";
1657
- var TOOL_STATUS = {
1658
- running: "running",
1659
- ok: "success",
1660
- failed: "error"
1661
- };
1662
- function assistantIsThinking(state) {
1663
- if (state.status !== "streaming") return false;
1664
- const streaming = state.streamingId ? state.messages.find((m) => m.id === state.streamingId) : void 0;
1665
- return !streaming || streaming.text === "";
1666
- }
1667
- function formatUsd(usd) {
1668
- return `$${usd.toFixed(usd < 0.01 ? 4 : 2)}`;
1669
- }
1670
- function messageItem(m, streamingId) {
1671
- switch (m.role) {
1672
- case "user":
1673
- return { id: m.id, kind: "message", role: "user", content: m.text };
1674
- case "assistant": {
1675
- const isStreaming = m.id === streamingId;
1676
- if (m.text === "" && isStreaming) return null;
1677
- return {
1678
- id: m.id,
1679
- kind: "message",
1680
- role: "assistant",
1681
- content: m.text,
1682
- isStreaming
1683
- };
1684
- }
1685
- case "status":
1686
- return { id: m.id, kind: "status", label: m.text };
1687
- case "tool": {
1688
- const name = m.tool?.name ?? "tool";
1689
- const status = TOOL_STATUS[m.tool?.status ?? "running"];
1690
- const call = {
1691
- id: m.id,
1692
- type: "unknown",
1693
- label: describeToolActivity(name),
1694
- status,
1695
- // The tool message's text carries the error text on failure.
1696
- detail: status === "error" && m.text ? m.text : void 0
1697
- };
1698
- return { id: m.id, kind: "tool", call };
1699
- }
1700
- }
1701
- }
1702
- function buildAssistantTimeline(state, renderProposal) {
1703
- const items = [];
1704
- for (const m of state.messages) {
1705
- const item = messageItem(m, state.streamingId);
1706
- if (item) items.push(item);
1707
- }
1708
- if (assistantIsThinking(state) && state.reasoning) {
1709
- items.push({
1710
- id: "reasoning",
1711
- kind: "custom",
1712
- content: /* @__PURE__ */ jsx3("p", { className: "whitespace-pre-wrap px-1 text-muted-foreground text-xs italic", children: state.reasoning })
1713
- });
1714
- }
1715
- if (state.status !== "streaming" && state.usage && state.usage.costUsd != null) {
1716
- items.push({
1717
- id: "usage",
1718
- kind: "status",
1719
- label: state.usage.replayed ? "Replayed from a previous turn \u2014 no charge." : `This turn cost ${formatUsd(state.usage.costUsd)}.`
1720
- });
1721
- }
1722
- for (const p of state.pendingProposals) {
1723
- items.push({
1724
- id: `proposal-${p.callId}`,
1725
- kind: "custom",
1726
- content: renderProposal(p)
1727
- });
1728
- }
1729
- return items;
1730
- }
1731
-
1732
- // src/assistant/ProposalCard.tsx
1733
- import { useState as useState4 } from "react";
1734
- import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
1735
- function ProposalCard({
1736
- proposal,
1737
- confirming,
1738
- onConfirm,
1739
- onCancel,
1740
- navigate,
1741
- renderGraph
1742
- }) {
1743
- const view = describeProposal(proposal);
1744
- const [tab, setTab] = useState4("graph");
1745
- const isWorkflow = view.preview?.kind === "workflow";
1746
- const showGraph = isWorkflow && !!renderGraph;
1747
- return /* @__PURE__ */ jsxs2("div", { className: "rounded-lg border border-primary/40 bg-card p-3 text-sm", children: [
1748
- /* @__PURE__ */ jsx4("p", { className: "font-medium text-foreground", children: view.title }),
1749
- /* @__PURE__ */ jsx4("p", { className: "text-muted-foreground text-xs", children: "Confirm to run this action on your account." }),
1750
- view.fields.length > 0 && /* @__PURE__ */ jsx4("dl", { className: "mt-2 space-y-1", children: view.fields.map((f) => /* @__PURE__ */ jsxs2("div", { className: "flex gap-2 text-xs", children: [
1751
- /* @__PURE__ */ jsx4("dt", { className: "shrink-0 text-muted-foreground", children: f.label }),
1752
- /* @__PURE__ */ jsx4("dd", { className: "truncate text-foreground", title: f.value, children: f.value })
1753
- ] }, f.label)) }),
1754
- view.skills && view.skills.length > 0 && /* @__PURE__ */ jsxs2("div", { className: "mt-2", children: [
1755
- /* @__PURE__ */ jsx4("p", { className: "text-muted-foreground text-xs", children: "New skills" }),
1756
- /* @__PURE__ */ jsx4("ul", { className: "mt-1 space-y-0.5", children: view.skills.map((s) => /* @__PURE__ */ jsxs2("li", { className: "text-foreground text-xs", children: [
1757
- /* @__PURE__ */ jsx4("span", { className: "font-medium", children: s.name }),
1758
- s.description ? /* @__PURE__ */ jsxs2("span", { className: "text-muted-foreground", children: [
1759
- " \u2014 ",
1760
- s.description
1761
- ] }) : null
1762
- ] }, s.name)) })
1763
- ] }),
1764
- view.preview && /* @__PURE__ */ jsxs2("div", { className: "mt-2", children: [
1765
- /* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between", children: [
1766
- /* @__PURE__ */ jsx4("p", { className: "text-muted-foreground text-xs", children: view.preview.label }),
1767
- showGraph && /* @__PURE__ */ jsxs2("div", { className: "flex gap-2 text-xs", children: [
1768
- /* @__PURE__ */ jsx4(
1769
- "button",
1770
- {
1771
- type: "button",
1772
- onClick: () => setTab("graph"),
1773
- className: tab === "graph" ? "text-foreground" : "text-muted-foreground",
1774
- children: "Graph"
1775
- }
1776
- ),
1777
- /* @__PURE__ */ jsx4(
1778
- "button",
1779
- {
1780
- type: "button",
1781
- onClick: () => setTab("yaml"),
1782
- className: tab === "yaml" ? "text-foreground" : "text-muted-foreground",
1783
- children: "YAML"
1784
- }
1785
- )
1786
- ] })
1787
- ] }),
1788
- showGraph && tab === "graph" ? /* @__PURE__ */ jsx4("div", { className: "mt-1 h-64 overflow-hidden rounded border border-border", children: renderGraph?.(view.preview.content) }) : /* @__PURE__ */ jsx4("pre", { className: "mt-1 max-h-48 overflow-auto rounded border border-border bg-muted/50 p-2 text-xs", children: /* @__PURE__ */ jsx4("code", { children: view.preview.content }) })
1789
- ] }),
1790
- proposal.requirements && proposal.requirements.length > 0 && /* @__PURE__ */ jsxs2("div", { className: "mt-3 rounded border border-border p-2", children: [
1791
- /* @__PURE__ */ jsx4("p", { className: "text-muted-foreground text-xs", children: "Integrations" }),
1792
- /* @__PURE__ */ jsx4("ul", { className: "mt-1 space-y-1", children: proposal.requirements.map((r) => /* @__PURE__ */ jsx4(
1793
- RequirementRow,
1794
- {
1795
- req: r,
1796
- navigate
1797
- },
1798
- `${r.provider}-${r.kind ?? "integration"}`
1799
- )) }),
1800
- /* @__PURE__ */ jsx4("p", { className: "mt-1 text-muted-foreground text-xs", children: "Connect the items above, then confirm \u2014 your proposal stays here until you do." })
1801
- ] }),
1802
- proposal.retryError && /* @__PURE__ */ jsx4("p", { role: "alert", className: "mt-2 text-destructive text-xs", children: proposal.retryError }),
1803
- /* @__PURE__ */ jsxs2("div", { className: "mt-3 flex gap-2", children: [
1804
- /* @__PURE__ */ jsx4(
1805
- "button",
1806
- {
1807
- type: "button",
1808
- onClick: onConfirm,
1809
- disabled: confirming || !proposal.proposalId,
1810
- className: "rounded bg-primary px-3 py-1.5 text-primary-foreground text-sm disabled:opacity-50",
1811
- children: confirming ? "Confirming\u2026" : "Confirm"
1812
- }
1813
- ),
1814
- /* @__PURE__ */ jsx4(
1815
- "button",
1816
- {
1817
- type: "button",
1818
- onClick: onCancel,
1819
- disabled: confirming,
1820
- className: "rounded border border-border px-3 py-1.5 text-foreground text-sm disabled:opacity-50",
1821
- children: "Cancel"
1822
- }
1823
- )
1824
- ] })
1825
- ] });
1826
- }
1827
- function openConnect(target, navigate) {
1828
- if (target.startsWith("//")) return;
1829
- let url;
1830
- try {
1831
- url = new URL(target, window.location.origin);
1832
- } catch {
1833
- return;
1834
- }
1835
- if (url.protocol !== "http:" && url.protocol !== "https:") return;
1836
- if (/^[a-z][a-z0-9+.-]*:/i.test(target)) {
1837
- window.open(url.href, "_blank", "noopener,noreferrer");
1838
- } else if (navigate) {
1839
- navigate(target);
1840
- } else {
1841
- window.location.assign(url.href);
1842
- }
1843
- }
1844
- function RequirementRow({
1845
- req,
1846
- navigate
1847
- }) {
1848
- const label = providerLabel(req.provider);
1849
- const isApp = req.kind === "github_app";
1850
- const kindLabel = isApp ? `${label} App` : label;
1851
- const statusText = req.connected ? isApp ? "installed" : "connected" : isApp ? "not installed" : "not connected";
1852
- const canConnect = !req.connected && req.connectUrl !== null;
1853
- const target = req.connectUrl ?? "/app/integrations";
1854
- return /* @__PURE__ */ jsxs2("li", { className: "flex items-center justify-between gap-2 text-xs", children: [
1855
- /* @__PURE__ */ jsxs2("span", { className: "flex min-w-0 items-center gap-2", children: [
1856
- /* @__PURE__ */ jsx4(
1857
- ProviderIcon,
1858
- {
1859
- id: req.provider,
1860
- displayName: label,
1861
- size: 16,
1862
- className: "rounded"
1863
- }
1864
- ),
1865
- /* @__PURE__ */ jsx4("span", { className: "truncate text-foreground", children: kindLabel }),
1866
- /* @__PURE__ */ jsxs2("span", { className: "flex shrink-0 items-center gap-1", children: [
1867
- /* @__PURE__ */ jsx4(
1868
- "span",
1869
- {
1870
- "aria-hidden": "true",
1871
- className: `h-1.5 w-1.5 rounded-full ${req.connected ? "bg-primary" : "border border-muted-foreground"}`
1872
- }
1873
- ),
1874
- /* @__PURE__ */ jsx4(
1875
- "span",
1876
- {
1877
- className: req.connected ? "text-primary" : "text-muted-foreground",
1878
- children: statusText
1879
- }
1880
- )
1881
- ] })
1882
- ] }),
1883
- canConnect && /* @__PURE__ */ jsxs2(
1884
- "button",
1885
- {
1886
- type: "button",
1887
- onClick: () => openConnect(target, navigate),
1888
- className: "shrink-0 text-primary",
1889
- children: [
1890
- isApp ? "Install" : "Connect",
1891
- " \u2192"
1892
- ]
1893
- }
1894
- )
1895
- ] });
1896
- }
1897
-
1898
- // src/assistant/usePanelPrefs.ts
1899
- import { useCallback as useCallback3, useEffect as useEffect4, useRef as useRef3, useState as useState5 } from "react";
1900
- var MIN_PANEL_WIDTH = 360;
1901
- var DEFAULT_PANEL_WIDTH = 448;
1902
- var MAX_PANEL_WIDTH_FRACTION = 0.95;
1903
- var MIN_FONT_SCALE = 0.875;
1904
- var MAX_FONT_SCALE = 1.5;
1905
- var DEFAULT_FONT_SCALE = 1;
1906
- var FONT_SCALE_STEP = 0.125;
1907
- var WIDTH_KEY = "assistant.panel.width";
1908
- var FONT_SCALE_KEY = "assistant.panel.fontScale";
1909
- function readNumber(key) {
1910
- try {
1911
- const raw = window.localStorage.getItem(key);
1912
- if (raw == null || raw.trim() === "") return null;
1913
- const n = Number(raw);
1914
- return Number.isFinite(n) ? n : null;
1915
- } catch {
1916
- return null;
1917
- }
1918
- }
1919
- function writeNumber(key, value) {
1920
- try {
1921
- window.localStorage.setItem(key, String(value));
1922
- } catch {
1923
- }
1924
- }
1925
- function maxPanelWidth() {
1926
- if (typeof window === "undefined") return 9999;
1927
- return Math.round(window.innerWidth * MAX_PANEL_WIDTH_FRACTION);
1928
- }
1929
- function clampWidth(value) {
1930
- return Math.min(
1931
- Math.max(Math.round(value), MIN_PANEL_WIDTH),
1932
- maxPanelWidth()
1933
- );
1934
- }
1935
- function clampScale(value) {
1936
- const stepped = Math.round(value / FONT_SCALE_STEP) * FONT_SCALE_STEP;
1937
- return Math.min(Math.max(stepped, MIN_FONT_SCALE), MAX_FONT_SCALE);
1938
- }
1939
- function usePanelWidth() {
1940
- const [width, setWidthState] = useState5(DEFAULT_PANEL_WIDTH);
1941
- const [maxWidth, setMaxWidth] = useState5(() => maxPanelWidth());
1942
- const desiredRef = useRef3(DEFAULT_PANEL_WIDTH);
1943
- useEffect4(() => {
1944
- setMaxWidth(maxPanelWidth());
1945
- const stored = readNumber(WIDTH_KEY);
1946
- if (stored != null) {
1947
- desiredRef.current = Math.max(Math.round(stored), MIN_PANEL_WIDTH);
1948
- setWidthState(clampWidth(stored));
1949
- }
1950
- }, []);
1951
- useEffect4(() => {
1952
- const onResize = () => {
1953
- setMaxWidth(maxPanelWidth());
1954
- setWidthState(clampWidth(desiredRef.current));
1955
- };
1956
- window.addEventListener("resize", onResize);
1957
- return () => window.removeEventListener("resize", onResize);
1958
- }, []);
1959
- const setWidth = useCallback3((next) => {
1960
- const clamped = clampWidth(next);
1961
- desiredRef.current = clamped;
1962
- writeNumber(WIDTH_KEY, clamped);
1963
- setWidthState(clamped);
1964
- }, []);
1965
- const previewWidth = useCallback3((next) => {
1966
- setWidthState(clampWidth(next));
1967
- }, []);
1968
- const nudgeWidth = useCallback3((deltaPx) => {
1969
- const clamped = clampWidth(desiredRef.current + deltaPx);
1970
- desiredRef.current = clamped;
1971
- writeNumber(WIDTH_KEY, clamped);
1972
- setWidthState(clamped);
1973
- }, []);
1974
- return { width, maxWidth, setWidth, previewWidth, nudgeWidth };
1975
- }
1976
- function useFontScale() {
1977
- const [scale, setScaleState] = useState5(DEFAULT_FONT_SCALE);
1978
- useEffect4(() => {
1979
- const stored = readNumber(FONT_SCALE_KEY);
1980
- if (stored != null) setScaleState(clampScale(stored));
1981
- }, []);
1982
- const step = useCallback3((delta) => {
1983
- setScaleState((prev) => {
1984
- const clamped = clampScale(prev + delta);
1985
- writeNumber(FONT_SCALE_KEY, clamped);
1986
- return clamped;
1987
- });
1988
- }, []);
1989
- return {
1990
- scale,
1991
- increase: () => step(FONT_SCALE_STEP),
1992
- decrease: () => step(-FONT_SCALE_STEP),
1993
- canIncrease: scale < MAX_FONT_SCALE - 1e-9,
1994
- canDecrease: scale > MIN_FONT_SCALE + 1e-9
1995
- };
1996
- }
1997
- function useIsDesktop() {
1998
- const [isDesktop, setIsDesktop] = useState5(
1999
- () => typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia("(min-width: 768px)").matches : true
2000
- );
2001
- useEffect4(() => {
2002
- if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
2003
- return;
2004
- }
2005
- const mq = window.matchMedia("(min-width: 768px)");
2006
- const update = () => setIsDesktop(mq.matches);
2007
- update();
2008
- mq.addEventListener("change", update);
2009
- return () => mq.removeEventListener("change", update);
2010
- }, []);
2011
- return isDesktop;
2012
- }
2013
-
2014
- // src/assistant/AssistantPanel.tsx
2015
- import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
2016
- var EMPTY_STATE = "Ask me to create a workflow, check your usage, or manage your API keys.";
2017
- function defaultFormatMoney(usd) {
2018
- if (usd == null) return "\u2014";
2019
- return new Intl.NumberFormat("en-US", {
2020
- style: "currency",
2021
- currency: "USD"
2022
- }).format(usd);
2023
- }
2024
- function toPickerModels(models, selected) {
2025
- const mapped = models.models.map((m) => ({
2026
- id: m.slug,
2027
- name: m.label,
2028
- ...m.contextTokens != null ? { context_length: m.contextTokens } : {}
2029
- }));
2030
- for (const slug of [models.default, selected]) {
2031
- if (slug && !mapped.some((m) => m.id === slug)) {
2032
- mapped.push({ id: slug, name: slug });
2033
- }
2034
- }
2035
- return mapped;
2036
- }
2037
- function nextModelSelection(id, defaultSlug) {
2038
- if (defaultSlug != null && id === defaultSlug) return null;
2039
- return id || null;
2040
- }
2041
- function AssistantPanel({
2042
- chat,
2043
- userId,
2044
- onClose,
2045
- navigate,
2046
- balanceUsd = null,
2047
- formatMoney = defaultFormatMoney,
2048
- renderGraph,
2049
- renderTranscript
2050
- }) {
2051
- const models = useAssistantModels();
2052
- const threads = useAssistantThreads(userId);
2053
- const font = useFontScale();
2054
- const [view, setView] = useState6("chat");
2055
- const historyButtonRef = useRef4(null);
2056
- const logRef = useRef4(null);
2057
- const pickerModels = useMemo2(
2058
- () => toPickerModels(models, chat.selectedModel),
2059
- [models, chat.selectedModel]
2060
- );
2061
- const pickerValue = chat.selectedModel ?? models.default ?? "";
2062
- const { state } = chat;
2063
- const chatRef = useRef4(chat);
2064
- chatRef.current = chat;
2065
- useEffect5(() => {
2066
- if (view !== "history") return;
2067
- const search = logRef.current?.querySelector(
2068
- 'input[type="search"]'
2069
- );
2070
- (search ?? logRef.current)?.focus();
2071
- }, [view]);
2072
- useEffect5(() => {
2073
- if (view !== "history") return;
2074
- const onKeyDownCapture = (e) => {
2075
- if (e.key !== "Escape") return;
2076
- const target = e.target;
2077
- if (!logRef.current?.contains(target) && !historyButtonRef.current?.contains(target)) {
2078
- return;
2079
- }
2080
- e.stopImmediatePropagation();
2081
- setView("chat");
2082
- historyButtonRef.current?.focus();
2083
- };
2084
- document.addEventListener("keydown", onKeyDownCapture, true);
2085
- return () => document.removeEventListener("keydown", onKeyDownCapture, true);
2086
- }, [view]);
2087
- const effectiveBalance = state.usage?.balanceUsd ?? balanceUsd;
2088
- const errorView = state.error ? presentError(state.error.code, state.error.message) : null;
2089
- const low = isLowBalance(effectiveBalance) && !errorView;
2090
- const streaming = state.status === "streaming";
2091
- const firstUserText = state.messages.find((m) => m.role === "user")?.text.trim();
2092
- const titleChars = firstUserText ? Array.from(firstUserText) : [];
2093
- const conversationTitle = firstUserText ? titleChars.length > 60 ? `${titleChars.slice(0, 60).join("")}\u2026` : firstUserText : null;
2094
- const renderProposal = (proposal) => /* @__PURE__ */ jsx5(
2095
- ProposalCard,
2096
- {
2097
- proposal,
2098
- confirming: proposal.proposalId ? chat.confirmingIds.has(proposal.proposalId) : false,
2099
- onConfirm: () => chat.confirm(proposal),
2100
- onCancel: () => chat.cancel(proposal),
2101
- navigate,
2102
- renderGraph
2103
- }
2104
- );
2105
- const isThinking = assistantIsThinking(state);
2106
- const showHistory = () => {
2107
- threads.refresh();
2108
- setView("history");
2109
- };
2110
- const toggleHistory = () => {
2111
- if (view === "history") setView("chat");
2112
- else showHistory();
2113
- };
2114
- const deleteThread = async (threadId) => {
2115
- const pre = chatRef.current.state;
2116
- if (pre.threadId === threadId && pre.status !== "idle") return;
2117
- if (!window.confirm("Delete this conversation? This can't be undone.")) {
2118
- return;
2119
- }
2120
- const res = await threads.remove(threadId);
2121
- const live = chatRef.current.state;
2122
- if (res.ok && live.threadId === threadId && live.status === "idle") {
2123
- chatRef.current.reset();
2124
- }
2125
- };
2126
- return /* @__PURE__ */ jsxs3("div", { className: "relative flex h-full flex-col bg-background", children: [
2127
- /* @__PURE__ */ jsx5("div", { className: "border-border border-b", children: /* @__PURE__ */ jsxs3("div", { className: "flex items-center justify-between gap-2 px-4 pt-3 pb-2.5", children: [
2128
- /* @__PURE__ */ jsxs3("div", { className: "flex min-w-0 flex-col", children: [
2129
- /* @__PURE__ */ jsxs3("div", { className: "flex items-baseline gap-2", children: [
2130
- /* @__PURE__ */ jsx5("span", { className: "font-medium text-foreground text-sm", children: "Assistant" }),
2131
- /* @__PURE__ */ jsx5(
2132
- "span",
2133
- {
2134
- "aria-label": "Your credit balance",
2135
- className: "text-muted-foreground text-xs",
2136
- children: formatMoney(effectiveBalance)
2137
- }
2138
- )
2139
- ] }),
2140
- conversationTitle && /* @__PURE__ */ jsx5(
2141
- "span",
2142
- {
2143
- className: "truncate text-muted-foreground text-xs",
2144
- title: conversationTitle,
2145
- children: conversationTitle
2146
- }
2147
- )
2148
- ] }),
2149
- /* @__PURE__ */ jsxs3("div", { className: "flex shrink-0 items-center gap-1", children: [
2150
- /* @__PURE__ */ jsxs3(
2151
- "div",
2152
- {
2153
- className: "flex items-center overflow-hidden rounded-md border border-border",
2154
- role: "group",
2155
- "aria-label": "Text size",
2156
- children: [
2157
- /* @__PURE__ */ jsx5(
2158
- "button",
2159
- {
2160
- type: "button",
2161
- onClick: font.decrease,
2162
- disabled: !font.canDecrease,
2163
- "aria-label": "Decrease text size",
2164
- className: "px-1.5 py-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:opacity-40 disabled:hover:bg-transparent",
2165
- children: /* @__PURE__ */ jsx5(Minus, { className: "h-3.5 w-3.5" })
2166
- }
2167
- ),
2168
- /* @__PURE__ */ jsx5(
2169
- "button",
2170
- {
2171
- type: "button",
2172
- onClick: font.increase,
2173
- disabled: !font.canIncrease,
2174
- "aria-label": "Increase text size",
2175
- className: "border-border border-l px-1.5 py-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:opacity-40 disabled:hover:bg-transparent",
2176
- children: /* @__PURE__ */ jsx5(Plus, { className: "h-3.5 w-3.5" })
2177
- }
2178
- )
2179
- ]
2180
- }
2181
- ),
2182
- /* @__PURE__ */ jsx5(
2183
- "button",
2184
- {
2185
- ref: historyButtonRef,
2186
- type: "button",
2187
- onClick: toggleHistory,
2188
- "aria-label": "Chat history",
2189
- "aria-pressed": view === "history",
2190
- className: `rounded-md p-1.5 transition-colors hover:bg-muted hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring ${view === "history" ? "bg-muted text-foreground" : "text-muted-foreground"}`,
2191
- children: /* @__PURE__ */ jsx5(History, { className: "h-4 w-4" })
2192
- }
2193
- ),
2194
- /* @__PURE__ */ jsx5(
2195
- "button",
2196
- {
2197
- type: "button",
2198
- onClick: () => {
2199
- chat.reset();
2200
- setView("chat");
2201
- },
2202
- "aria-label": "New chat",
2203
- title: "New chat",
2204
- className: "rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring",
2205
- children: /* @__PURE__ */ jsx5(MessageSquarePlus, { className: "h-4 w-4" })
2206
- }
2207
- ),
2208
- /* @__PURE__ */ jsx5(
2209
- "button",
2210
- {
2211
- type: "button",
2212
- onClick: onClose,
2213
- "aria-label": "Close assistant",
2214
- className: "rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring",
2215
- children: /* @__PURE__ */ jsx5(X, { className: "h-4 w-4" })
2216
- }
2217
- )
2218
- ] })
2219
- ] }) }),
2220
- /* @__PURE__ */ jsx5(
2221
- "div",
2222
- {
2223
- ref: logRef,
2224
- tabIndex: -1,
2225
- "aria-label": "Conversation",
2226
- role: view === "chat" ? "log" : void 0,
2227
- "aria-live": view === "chat" ? "polite" : void 0,
2228
- className: "min-h-0 flex-1 overflow-y-auto focus:outline-none",
2229
- children: view === "history" ? /* @__PURE__ */ jsx5(
2230
- AssistantHistory,
2231
- {
2232
- threads: threads.threads,
2233
- loaded: threads.loaded,
2234
- activeThreadId: state.threadId,
2235
- activeBusy: state.status !== "idle",
2236
- canRemove: threads.canRemove,
2237
- onSelect: (id) => {
2238
- chat.switchThread(id);
2239
- setView("chat");
2240
- },
2241
- onDelete: (id) => void deleteThread(id)
2242
- }
2243
- ) : (
2244
- // The text-size control zooms the transcript only — not the history
2245
- // view's search box and buttons. `zoom` scales every descendant
2246
- // uniformly regardless of which renderer draws the conversation; an
2247
- // inline `font-size` would not (the transcript's text utilities set
2248
- // absolute rem sizes), and `transform: scale` would break the scroll
2249
- // container by keeping the original layout box.
2250
- /* @__PURE__ */ jsx5("div", { className: "px-2 py-3", style: { zoom: font.scale }, children: renderTranscript ? renderTranscript({
2251
- messages: state.messages,
2252
- reasoning: state.reasoning,
2253
- streamingId: state.streamingId,
2254
- model: state.model,
2255
- isStreaming: streaming,
2256
- isThinking,
2257
- pendingProposals: state.pendingProposals,
2258
- usage: state.usage,
2259
- renderProposal
2260
- }) : /* @__PURE__ */ jsx5(
2261
- AgentTimeline,
2262
- {
2263
- items: buildAssistantTimeline(state, renderProposal),
2264
- isThinking,
2265
- emptyState: /* @__PURE__ */ jsx5("p", { className: "px-4 py-8 text-center text-muted-foreground text-sm", children: EMPTY_STATE })
2266
- }
2267
- ) })
2268
- )
2269
- }
2270
- ),
2271
- errorView && /* @__PURE__ */ jsxs3(
2272
- "div",
2273
- {
2274
- role: "alert",
2275
- className: "mx-4 mb-2 rounded-lg border border-destructive/40 bg-destructive/5 px-3 py-2 text-sm",
2276
- children: [
2277
- /* @__PURE__ */ jsx5("p", { className: "text-foreground", children: errorView.message }),
2278
- errorView.cta && /* @__PURE__ */ jsxs3(
2279
- "button",
2280
- {
2281
- type: "button",
2282
- onClick: () => navigate?.(errorView.cta?.to ?? ""),
2283
- className: "mt-1 text-primary text-xs",
2284
- children: [
2285
- errorView.cta.label,
2286
- " \u2192"
2287
- ]
2288
- }
2289
- )
2290
- ]
2291
- }
2292
- ),
2293
- low && /* @__PURE__ */ jsxs3(
2294
- "div",
2295
- {
2296
- role: "status",
2297
- className: "mx-4 mb-2 rounded-lg border border-border bg-muted/40 px-3 py-2 text-sm",
2298
- children: [
2299
- /* @__PURE__ */ jsx5("p", { className: "text-foreground", children: "Your credit balance is running low." }),
2300
- /* @__PURE__ */ jsx5(
2301
- "button",
2302
- {
2303
- type: "button",
2304
- onClick: () => navigate?.("/app/billing"),
2305
- className: "mt-1 text-primary text-xs",
2306
- children: "Add credits \u2192"
2307
- }
2308
- )
2309
- ]
2310
- }
2311
- ),
2312
- /* @__PURE__ */ jsxs3("div", { className: "border-border border-t p-2", children: [
2313
- streaming && /* @__PURE__ */ jsx5("div", { className: "px-2 pb-1.5", "aria-label": "Assistant is working", children: /* @__PURE__ */ jsx5(ThinkingIndicator, {}) }),
2314
- /* @__PURE__ */ jsx5("div", { className: "mb-1.5 flex items-center px-1", children: pickerModels.length > 0 ? /* @__PURE__ */ jsx5(
2315
- ModelPicker,
2316
- {
2317
- variant: "pill",
2318
- label: "Model",
2319
- value: pickerValue,
2320
- onChange: (id) => chat.setModel(nextModelSelection(id, models.default)),
2321
- models: pickerModels
2322
- }
2323
- ) : /* @__PURE__ */ jsx5("span", { className: "px-1 text-muted-foreground text-xs", children: "Default model" }) }),
2324
- /* @__PURE__ */ jsx5(
2325
- ChatInput,
2326
- {
2327
- onSend: (message) => {
2328
- setView("chat");
2329
- chat.send(message);
2330
- },
2331
- onCancel: chat.stop,
2332
- isStreaming: streaming,
2333
- disabled: chat.restoring || state.status === "awaiting_confirm",
2334
- placeholder: "Message the assistant\u2026",
2335
- inputLabel: null,
2336
- idleStatus: null,
2337
- streamingStatus: null
2338
- }
2339
- )
2340
- ] })
2341
- ] });
2342
- }
2343
-
2344
- // src/assistant/launcher.tsx
2345
- import {
2346
- createContext as createContext2,
2347
- useCallback as useCallback4,
2348
- useContext as useContext2,
2349
- useMemo as useMemo3,
2350
- useState as useState7
2351
- } from "react";
2352
- import { jsx as jsx6 } from "react/jsx-runtime";
2353
- var AssistantLauncherContext = createContext2(null);
2354
- function AssistantLauncherProvider({
2355
- children
2356
- }) {
2357
- const [open, setOpen] = useState7(false);
2358
- const [seed, setSeed] = useState7(null);
2359
- const openAssistant = useCallback4((next) => {
2360
- if (next != null) setSeed(next);
2361
- setOpen(true);
2362
- }, []);
2363
- const closeAssistant = useCallback4(() => setOpen(false), []);
2364
- const clearSeed = useCallback4(() => setSeed(null), []);
2365
- const value = useMemo3(
2366
- () => ({ open, seed, openAssistant, closeAssistant, clearSeed }),
2367
- [open, seed, openAssistant, closeAssistant, clearSeed]
2368
- );
2369
- return /* @__PURE__ */ jsx6(AssistantLauncherContext.Provider, { value, children });
2370
- }
2371
- function useAssistantLauncher() {
2372
- const ctx = useContext2(AssistantLauncherContext);
2373
- if (!ctx) {
2374
- throw new Error(
2375
- "useAssistantLauncher must be used within an AssistantLauncherProvider"
2376
- );
2377
- }
2378
- return ctx;
2379
- }
2380
-
2381
- // src/assistant/ResizeHandle.tsx
2382
- import { useRef as useRef5 } from "react";
2383
- import { jsx as jsx7 } from "react/jsx-runtime";
2384
- function ResizeHandle({
2385
- width,
2386
- maxWidth,
2387
- onPreview,
2388
- onCommit,
2389
- onNudge
2390
- }) {
2391
- const dragRef = useRef5(null);
2392
- const onPointerDown = (e) => {
2393
- if (e.button !== 0) return;
2394
- e.preventDefault();
2395
- e.currentTarget.setPointerCapture(e.pointerId);
2396
- dragRef.current = {
2397
- startX: e.clientX,
2398
- startWidth: width,
2399
- lastWidth: width
2400
- };
2401
- };
2402
- const onPointerMove = (e) => {
2403
- const drag = dragRef.current;
2404
- if (!drag) return;
2405
- const next = drag.startWidth + (drag.startX - e.clientX);
2406
- if (Math.abs(next - drag.lastWidth) < 1) return;
2407
- drag.lastWidth = next;
2408
- onPreview(next);
2409
- };
2410
- const endDrag = (e) => {
2411
- const drag = dragRef.current;
2412
- if (!drag) return;
2413
- dragRef.current = null;
2414
- if (e.currentTarget.hasPointerCapture(e.pointerId)) {
2415
- e.currentTarget.releasePointerCapture(e.pointerId);
2416
- }
2417
- onCommit(drag.lastWidth);
2418
- };
2419
- const onKeyDown = (e) => {
2420
- const STEP = 24;
2421
- if (e.key === "ArrowLeft") {
2422
- e.preventDefault();
2423
- onNudge(STEP);
2424
- } else if (e.key === "ArrowRight") {
2425
- e.preventDefault();
2426
- onNudge(-STEP);
2427
- }
2428
- };
2429
- return (
2430
- // biome-ignore lint/a11y/useSemanticElements: a focusable drag handle is an ARIA window-splitter (role=separator); no native HTML element provides this.
2431
- /* @__PURE__ */ jsx7(
2432
- "div",
2433
- {
2434
- role: "separator",
2435
- "aria-orientation": "vertical",
2436
- "aria-label": "Resize assistant panel",
2437
- "aria-valuemin": MIN_PANEL_WIDTH,
2438
- "aria-valuemax": maxWidth,
2439
- "aria-valuenow": width,
2440
- tabIndex: 0,
2441
- onPointerDown,
2442
- onPointerMove,
2443
- onPointerUp: endDrag,
2444
- onPointerCancel: endDrag,
2445
- onKeyDown,
2446
- className: "group absolute inset-y-0 left-0 z-10 flex w-2 -translate-x-1/2 cursor-ew-resize touch-none items-center justify-center focus:outline-none",
2447
- children: /* @__PURE__ */ jsx7(
2448
- "span",
2449
- {
2450
- "aria-hidden": "true",
2451
- className: "h-10 w-1 rounded-full bg-border transition-colors group-hover:bg-primary group-focus:bg-primary"
2452
- }
2453
- )
2454
- }
2455
- )
2456
- );
2457
- }
2458
-
2459
- // src/assistant/AssistantDock.tsx
2460
- import { Fragment, jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
2461
- function focusableWithin(container) {
2462
- const selector = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
2463
- return Array.from(container.querySelectorAll(selector)).filter(
2464
- (el) => el.getClientRects().length > 0
2465
- );
2466
- }
2467
- function AssistantDock({
2468
- userId,
2469
- navigate,
2470
- balanceUsd = null,
2471
- formatMoney,
2472
- renderGraph,
2473
- onWorkflowMutation,
2474
- renderTranscript
2475
- }) {
2476
- const { open, openAssistant, closeAssistant } = useAssistantLauncher();
2477
- const chat = useAssistantChat(userId, { onWorkflowMutation });
2478
- const isDesktop = useIsDesktop();
2479
- const { width, maxWidth, setWidth, previewWidth, nudgeWidth } = usePanelWidth();
2480
- const launcherRef = useRef6(null);
2481
- const dialogRef = useRef6(null);
2482
- const returnFocusRef = useRef6(null);
2483
- const wasOpenRef = useRef6(false);
2484
- useEffect6(() => {
2485
- if (!open) return;
2486
- const onKeyDown = (e) => {
2487
- if (e.key === "Escape") closeAssistant();
2488
- };
2489
- document.addEventListener("keydown", onKeyDown);
2490
- return () => document.removeEventListener("keydown", onKeyDown);
2491
- }, [open, closeAssistant]);
2492
- useEffect6(() => {
2493
- if (open) {
2494
- if (!wasOpenRef.current && !returnFocusRef.current) {
2495
- returnFocusRef.current = document.activeElement;
2496
- }
2497
- wasOpenRef.current = true;
2498
- const el = dialogRef.current;
2499
- if (el) (focusableWithin(el)[0] ?? el).focus();
2500
- } else if (wasOpenRef.current) {
2501
- wasOpenRef.current = false;
2502
- const target = returnFocusRef.current?.isConnected ? returnFocusRef.current : launcherRef.current;
2503
- target?.focus();
2504
- returnFocusRef.current = null;
2505
- }
2506
- }, [open]);
2507
- const openDialog = () => {
2508
- returnFocusRef.current = document.activeElement;
2509
- openAssistant();
2510
- };
2511
- if (!open) {
2512
- return /* @__PURE__ */ jsx8(
2513
- "button",
2514
- {
2515
- ref: launcherRef,
2516
- type: "button",
2517
- onClick: openDialog,
2518
- "aria-label": "Open assistant",
2519
- className: "fixed right-4 bottom-4 z-40 flex h-12 w-12 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg transition-colors hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-ring",
2520
- children: /* @__PURE__ */ jsx8(MessageSquare, { className: "h-6 w-6" })
2521
- }
2522
- );
2523
- }
2524
- const trapTab = (e) => {
2525
- if (e.key !== "Tab") return;
2526
- const dialog = dialogRef.current;
2527
- if (!dialog) return;
2528
- const focusables = focusableWithin(dialog);
2529
- if (focusables.length === 0) {
2530
- e.preventDefault();
2531
- dialog.focus();
2532
- return;
2533
- }
2534
- const first = focusables[0];
2535
- const last = focusables[focusables.length - 1];
2536
- const active = document.activeElement;
2537
- const inside = active instanceof Node && dialog.contains(active);
2538
- if (e.shiftKey) {
2539
- if (!inside || active === first || active === dialog) {
2540
- e.preventDefault();
2541
- last.focus();
2542
- }
2543
- } else if (!inside || active === last) {
2544
- e.preventDefault();
2545
- first.focus();
2546
- }
2547
- };
2548
- return /* @__PURE__ */ jsxs4(Fragment, { children: [
2549
- /* @__PURE__ */ jsx8(
2550
- "div",
2551
- {
2552
- "aria-hidden": "true",
2553
- className: "fixed inset-0 z-40 bg-black/40",
2554
- onClick: () => closeAssistant()
2555
- }
2556
- ),
2557
- /* @__PURE__ */ jsxs4(
2558
- "div",
2559
- {
2560
- ref: dialogRef,
2561
- role: "dialog",
2562
- "aria-label": "Assistant",
2563
- "aria-modal": "true",
2564
- tabIndex: -1,
2565
- onKeyDown: trapTab,
2566
- style: isDesktop ? { width: `${width}px` } : void 0,
2567
- className: "fixed inset-y-0 right-0 z-50 flex w-full flex-col border-border border-l shadow-xl focus:outline-none",
2568
- children: [
2569
- /* @__PURE__ */ jsx8(
2570
- AssistantPanel,
2571
- {
2572
- chat,
2573
- userId,
2574
- onClose: () => closeAssistant(),
2575
- navigate,
2576
- balanceUsd,
2577
- formatMoney,
2578
- renderGraph,
2579
- renderTranscript
2580
- },
2581
- userId ?? "anon"
2582
- ),
2583
- isDesktop && /* @__PURE__ */ jsx8(
2584
- ResizeHandle,
2585
- {
2586
- width,
2587
- maxWidth,
2588
- onPreview: previewWidth,
2589
- onCommit: setWidth,
2590
- onNudge: nudgeWidth
2591
- }
2592
- )
2593
- ]
2594
- }
2595
- )
2596
- ] });
2597
- }
2598
- export {
2599
- AssistantClientProvider,
2600
- AssistantDock,
2601
- AssistantLauncherProvider,
2602
- AssistantPanel,
2603
- ProposalCard,
2604
- createAssistantClient,
2605
- useAssistantChat,
2606
- useAssistantClient,
2607
- useAssistantLauncher,
2608
- useAssistantModels,
2609
- useAssistantThreads
2610
- };